自定義LayoutManager實現滾動畫廊控件

轉自RecycerView系列之六實現滾動畫廊控件

1、滾動畫廊控件

本節實現的效果如下圖所示:


滾動畫廊.gif

2、實現橫向布局

2.1、開啟橫向滾動

自定義LayoutManager之復用與回收二中已經介紹了如何通過自定義LayoutManager實現垂直滾動的效果,由于本文中效果是中橫向滾動的,故在此基礎上進行修改。
首先刪除canScrollVertically()scrollVerticallyBy函數,改為:

@Override
public boolean canScrollHorizontally() {
    return true;
}

@Override
public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
    …………
}

在經過上面修改后,可以成功運行,但是布局依然是豎直布局的,很明顯需要修改onLayoutChildren()進行橫向布局。

2.2、實現橫向布局

最關鍵的問題就是,我們在初始化布局時,會通過mItemRects來保存所有item的位置,所以這里需要修改成橫向布局的計算方式。

int offsetX = 0;

for (int i = 0; i < getItemCount(); i++) {
    Rect rect = new Rect(offsetX, 0, offsetX + mItemWidth, mItemHeight);
    mItemRects.put(i, rect);
    mHasAttachedItems.put(i, false);
    offsetX += mItemWidth;
}

然后在獲取visibleCount時,需要修改為:

int visibleCount = getHorizontalSpace() / mItemWidth;

同時,在onLayoutChildren最后,有個計算mTotalHeight的邏輯,我們需要改為計算totalWidth的邏輯:

@Override
public void onLayoutChildren(Recycler recycler, RecyclerView.State state) {
    …………
    mTotalWidth = Math.max(offsetX, getHorizontalSpace());
}

private int getHorizontalSpace() {
    return getWidth() - getPaddingLeft() - getPaddingRight();
}

同時,在getVisibleArea函數也需要修改,因為我們現在已經是橫向滾動了,已經不再是豎向滾動了,所以可見區域應該是橫向滾動后的可見區域:

private Rect getVisibleArea() {
    Rect result = new Rect(getPaddingLeft() + mSumDx, getPaddingTop(), getWidth() - getPaddingRight() + mSumDx, getHeight()-getPaddingBottom());
    return result;
}

onLayoutChildren函數中的其它代碼不需要更改,此時onLayoutChildren的代碼如下:

public void onLayoutChildren(Recycler recycler, RecyclerView.State state) {
    if (getItemCount() == 0) {//沒有Item,界面空著吧
        detachAndScrapAttachedViews(recycler);
        return;
    }
    mHasAttachedItems.clear();
    mItemRects.clear();

    detachAndScrapAttachedViews(recycler);

    //將item的位置存儲起來
    View childView = recycler.getViewForPosition(0);
    measureChildWithMargins(childView, 0, 0);
    mItemWidth = getDecoratedMeasuredWidth(childView);
    mItemHeight = getDecoratedMeasuredHeight(childView);

    int visibleCount = getVerticalSpace() / mItemWidth;


    //定義水平方向的偏移量
    int offsetX = 0;

    for (int i = 0; i < getItemCount(); i++) {
        Rect rect = new Rect(offsetX, 0, offsetX + mItemWidth, mItemHeight);
        mItemRects.put(i, rect);
        mHasAttachedItems.put(i, false);
        offsetX += mItemWidth;
    }

    Rect visibleRect = getVisibleArea();
    for (int i = 0; i < visibleCount; i++) {
        insertView(i, visibleRect, recycler, false);
    }

    //如果所有子View的寬度和沒有填滿RecyclerView的寬度,
    // 則將寬度設置為RecyclerView的寬度
    mTotalWidth = Math.max(offsetX, getHorizontalSpace());
}

private int getHorizontalSpace() {
    return getWidth() - getPaddingLeft() - getPaddingRight();
}

private Rect getVisibleArea() {
    Rect result = new Rect(getPaddingLeft() + mSumDx, getPaddingTop(), getWidth() - getPaddingRight() + mSumDx, getHeight()-getPaddingBottom());
    return result;
}

修改后的效果如下圖所示:


image.png

到此已經實現了橫向的布局,下面修改下橫向滾動的邏輯。

2.3、實現橫向滾動

橫向滾動是放在scrollHorizontallyBy中處理,主要的修改如下:

  • 1、邊界處理修改
 int travel = dx;
    //如果滑動到最頂部
    if (mSumDx + dx < 0) {
        travel = -mSumDx;
    } else if (mSumDx + dx > mTotalWidth - getHorizontalSpace()) {
        //如果滑動到最底部
        travel = mTotalWidth - getHorizontalSpace() - mSumDx;
    }

邊界的處理和垂直的處理邏輯大致相同,只需要進行簡單的修改。

  • 2、在回收越界時,已經在屏幕上的item重新Layout的修改:
//回收越界子View
for (int i = getChildCount() - 1; i >= 0; i--) {
    View child = getChildAt(i);
    int position = getPosition(child);
    Rect rect = mItemRects.get(position);

    if (!Rect.intersects(rect, visibleRect)) {
        removeAndRecycleView(child, recycler);
        mHasAttachedItems.put(position, false);
    } else {
        layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);
        mHasAttachedItems.put(position, true);
    }
}

這里只需要修改layoutDecoratedWithMargins函數即可,在布局時,根據mSumDx布局item的left和right坐標:layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);,因為是橫向布局,所以top和bottom都不變。

  • 3、在移動后需要處理空白區域的填充,同樣涉及到layout操作,故需要處理。
private void insertView(int pos, Rect visibleRect, Recycler recycler, boolean firstPos) {
    Rect rect = mItemRects.get(pos);
    if (Rect.intersects(visibleRect, rect) && !mHasAttachedItems.get(pos)) {
        View child = recycler.getViewForPosition(pos);
        if (firstPos) {
            addView(child, 0);
        } else {
            addView(child);
        }
        measureChildWithMargins(child, 0, 0);
        layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);

        mHasAttachedItems.put(pos, true);
    }
}

到此就實現了橫向的滾動效果了,效果如下:


橫向滾動.gif

完整的scrollHorizontallyBy代碼如下

public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
    if (getChildCount() <= 0) {
        return dx;
    }

    int travel = dx;
    //如果滑動到最頂部
    if (mSumDx + dx < 0) {
        travel = -mSumDx;
    } else if (mSumDx + dx > mTotalWidth - getHorizontalSpace()) {
        //如果滑動到最底部
        travel = mTotalWidth - getHorizontalSpace() - mSumDx;
    }

    mSumDx += travel;

    Rect visibleRect = getVisibleArea();

    //回收越界子View
    for (int i = getChildCount() - 1; i >= 0; i--) {
        View child = getChildAt(i);
        int position = getPosition(child);
        Rect rect = mItemRects.get(position);

        if (!Rect.intersects(rect, visibleRect)) {
            removeAndRecycleView(child, recycler);
            mHasAttachedItems.put(position, false);
        } else {
            layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);
            mHasAttachedItems.put(position, true);
        }
    }

    //填充空白區域
    View lastView = getChildAt(getChildCount() - 1);
    View firstView = getChildAt(0);
    if (travel >= 0) {
        int minPos = getPosition(firstView);
        for (int i = minPos; i < getItemCount(); i++) {
            insertView(i, visibleRect, recycler, false);
        }
    } else {
        int maxPos = getPosition(lastView);
        for (int i = maxPos; i >= 0; i--) {
            insertView(i, visibleRect, recycler, true);
        }
    }
    return travel;
}
private void insertView(int pos, Rect visibleRect, Recycler recycler, boolean firstPos) {
    Rect rect = mItemRects.get(pos);
    if (Rect.intersects(visibleRect, rect) && !mHasAttachedItems.get(pos)) {
        View child = recycler.getViewForPosition(pos);
        if (firstPos) {
            addView(child, 0);
        } else {
            addView(child);
        }
        measureChildWithMargins(child, 0, 0);
        layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);

        mHasAttachedItems.put(pos, true);
    }
}

2.4、實現卡片疊加

從最終的效果圖中可以看出,我們兩個卡片之間并不是并排排列的,而是疊加在一起的。在這個例子中,兩個卡片之間疊加的部分是半個卡片的大小。所以,我們需要修改排列卡片的代碼,使卡片疊加起來。

首先,申請一個變量,保存兩個卡片之間的距離:

private int mIntervalWidth;

private int getIntervalWidth() {
    return mItemWidth / 2;
}

然后在onLayoutChildren中,首先給mIntervalWidth初始化,然后在計算每個卡片的起始位置時,offsetX每次位移距離,改為offsetX += mIntervalWidth,具體代碼如下:

mIntervalWidth = getIntervalWidth();

//定義水平方向的偏移量
int offsetX = 0;

for (int i = 0; i < getItemCount(); i++) {
    Rect rect = new Rect(offsetX, 0, offsetX + mItemWidth, mItemHeight);
    mItemRects.put(i, rect);
    mHasAttachedItems.put(i, false);
    offsetX += mIntervalWidth;
}

這里需要注意的是,在計算每個卡片的位置時Rect(offsetX, 0, offsetX + mItemWidth, mItemHeight),在這個Rect的right位置,不能改為offsetX + mIntervalWidth,因為我們只是更改了卡片布局時的起始位置,并沒有更改卡片的大小,所以每個卡片的長度和寬度是不能變的。

然后在初始化時插入item時,在計算visibleCount時,需要改為int visibleCount = getHorizontalSpace() / mIntervalWidth,代碼如下:

int visibleCount = getHorizontalSpace() / mIntervalWidth;
Rect visibleRect = getVisibleArea();
for (int i = 0; i < visibleCount; i++) {
    insertView(i, visibleRect, recycler, false);
}

因為在scrollHorizontallyBy中處理滾動時,每個卡片的位置都是直接從mItemRects中取的,所以,我們并不需要在修改滾動時的代碼。

到這里,就實現了卡片疊加的功能,效果如下圖所示:


卡片疊加.gif

2.5、修改卡片的起始位置

到現在,我們卡片都還是在最左側開始展示的,但在開篇的效果圖中可以看出,在初始化時,第一個item是在最屏幕中間顯示的,這是怎么做到的呢?

首先,我們需要先申請一個變量mStartX,來保存卡片后移的距離。

很明顯,這里也只是改變每個卡片的布局位置,所以我們也只需要在onLayoutChildren中,在mItemRects中初始化每個item位置時,將每個item后移mStartX就可以了。

所以核心代碼如下:

private int mStartX;

public void onLayoutChildren(Recycler recycler, RecyclerView.State state) {

    …………
    mStartX = getWidth()/2 - mIntervalWidth;

    //定義水平方向的偏移量
    int offsetX = 0;
    for (int i = 0; i < getItemCount(); i++) {
        Rect rect = new Rect(mStartX + offsetX, 0, mStartX + offsetX + mItemWidth, mItemHeight);
        mItemRects.put(i, rect);
        mHasAttachedItems.put(i, false);
        offsetX += mIntervalWidth;
    }
    …………
}

首先,是mStartX的初始化,因為我們需要第一個卡片的中間位置在屏幕正中間的位置,從下圖中明顯可以看出,mStartX的值應該是:mStartX = getWidth()/2 - mIntervalWidth;

image.png

然后,在計算每個item的rect時,將每個item后移mStartX距離:new Rect(mStartX + offsetX, 0, mStartX + offsetX + mItemWidth, mItemHeight)

就這樣,我們就完成了移動初始化位置的功能,效果如下圖所示:


修改初始位置.gif

2.6、更改默認顯示順序

2.6.1、 更改默認顯示順序的原理

現在,我們每個item的顯示順序還是后一個卡片壓在前一個卡片上顯示的,這是因為,在RecyclerView繪制時,先繪制的第一個item,然后再繪制第二個item,然后再繪制第三個item,……,默認就是這樣的繪制順序。即越往前的item越優先繪制。繪制原理示圖如下:


image.png

這里顯示的三個item繪制次序,很明顯,正是由于后面的item把前面的item疊加部分蓋住了,才造成了現在的每個item只顯示出一半的情況。

那如果我們更改下顯示順序,將兩邊的先繪制,將屏幕中間的Item(當前選中的item)最后繪制,就會成為這個情況:


image.png

形成的效果就是本節開篇的效果。

那關鍵的部分來:要怎么更改Item的繪制順序呢?

其實,只需要重寫RecyclerView的getChildDrawingOrder方法即可。

該方法的詳細聲明如下:

protected int getChildDrawingOrder(int childCount, int i)
  • childCount:表示當前屏幕上可見的item的個數
  • i:表示item的索引,一般而言,i的值就是在list中可見item的順序,通過getChildAt(i)即可得到當前item的視圖。
  • return int:返回值表示當前item的繪制順序,返回值越小,越先繪制,返回值越大,越最后繪制。很顯然,要實現我們開篇的效果,中間item的返回值應該是最大的,才能讓它最后繪制,以顯示在最上面。

需要注意的是,默認情況下,即便重寫getChildDrawingOrder函數,代碼也不會執行到getChildDrawingOrder里面的,我們需要在RecyclerView初始化時,顯式調用setChildrenDrawingOrderEnabled(true);開啟重新排序。

所以開啟重新排序,總共需要有兩步:

  • 1.調用setChildrenDrawingOrderEnabled(true);開啟重新排序
  • 2.在getChildDrawingOrder中重新返回每個item的繪制順序

2.6.2、重寫RecyclerView

因為我們要重寫getChildDrawingOrder,所以我們必須重寫RecylcerView:

public class RecyclerCoverFlowView extends RecyclerView {
    public RecyclerCoverFlowView(Context context) {
        super(context);
        init();
    }

    public RecyclerCoverFlowView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public RecyclerCoverFlowView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init(){
        setChildrenDrawingOrderEnabled(true); //開啟重新排序
    }

    /**
     * 獲取LayoutManger,并強制轉換為CoverFlowLayoutManger
     */
    public CoverFlowLayoutManager getCoverFlowLayout() {
        return ((CoverFlowLayoutManager)getLayoutManager());
    }

    @Override
    protected int getChildDrawingOrder(int childCount, int i) {
       return super.getChildDrawingOrder(childCount, i);
    }
}

在這里,我們主要做了兩步:

在初始化時,使用setChildrenDrawingOrderEnabled(true);開啟重新排序
因為后面,我們需要用到我們自定義的LayoutManager,所以我們額外提供了一個函數public CoverFlowLayoutManager getCoverFlowLayout(),以供后續使用
接下來,我們就來看看如何在getChildDrawingOrder中返回對應item的繪制順序。

2.6.3、計算繪制順序原理

下圖展示了位置索引與繪圖順序的關系:


image.png

在這個圖中,總共有7個Item,帶有圓圈的0,1,2,3,4,5,6是當前在屏幕中顯示的item位置索引,它的值也是默認的繪圖順序,默認的繪圖順序就是越靠前的item越先繪制。
要想達到圖上所示的效果,它的繪圖順序可以是0,1,2,6,5,4,3;因為數值代表的是繪制順序,所以值越大的越后繪制,所以左側的三個的順序是0,1,2;所以,第一個item先繪制,然后第二個item蓋在第一個上面;再然后,第三個item再繪制,它會蓋在第二個item的上面。所以這樣就保證的中間卡片左側部分的疊加效果。右側三個的繪制順序是5,4,3;所以最后一個item先繪制,然后是倒數第二個,最后是倒數第三個;同樣,右側三個也可以保證圖中的疊加效果。最中間的Item繪制順序為6,所以最后繪制,所以它會蓋在所有item的上面顯示出來。
注意:我這里講到這個效果的繪圖順序時,說的是“可以是”,而不是“必須是”!,只要保證下面兩點,所有的繪圖順序都是正確的:

繪圖順序的返回值范圍在0到childCount-1之間,其中(childCount表示當前屏幕中的可見item個數)
此繪圖順序在疊加后,可以保證最終效果
所以,如果我們把繪圖順序改為3,4,5,6,2,1,0;同樣是可以達到上面的效果的。

為了方便計算規則,我們使用0,1,2,6,5,4,3的繪圖順序。

很明顯,我們需要先找到所有在顯示item的中間位置,中間位置的繪圖順序是count -1;

然后中間位置之前的繪圖順序和它的排列排序相同,在getChildDrawingOrder函數中,排列順序是i,那么繪圖順序也是i;

最難的部分是中間位置之后的部分,它們的繪圖順序怎么算。

很明顯,最后一個item的繪圖順序始終是center(指屏幕顯示的中間item的索引,這里是3)。倒數第二個的繪圖順序是center+1,倒數第三個的繪圖順序是center+2;從這個計算中可以看出,后面的item的繪圖順序總是center+m,而m的值就是當前的item和最后一個item所間隔的個數。那當前item和最后一個item間隔的個數怎么算呢?它等于count - 1 - i;不知道大家能不能理解,count-1正常顯示順序下最后一個item的索引,也就是當前可見的item中的最大的索引,而i是屏幕中顯示的item的索引,也就是上圖圓圈內的數值。所以,中間后面的item的繪圖順序的計算方法是center + count - 1- i;

需要非常注意的是,這里的i是指屏幕中顯示item的索引,總是從0開始的,并不是指在Adapter中所有item中的索引值。它的意義與getChildAt(i)中的i是一樣的。

所以總結來講:

  • 中間位置的繪圖順序為order = count -1;
  • 中間位置之前的item的繪圖順序為 order = i;
  • 中間位置之后的item的繪圖順序為 order = center + count - i - i;

2.6.4、重寫getChildDrawingOrder

在理解了如何計算繪圖順序以后,現在就開始寫代碼了,在上面總結中,可以看到,這里count和 i 都是getChildDrawingOrder中現成的,唯一缺少的就是center值。center值是當前可見item中間位置從0開始的索引。我們可以通過中間位置的position減去第一個可見的item的position得到。

所以,我們需要在CoverFlowLayoutManager中添加一個函數(獲取中間item的positon–指在adapter中的position):

public int getCenterPosition(){
    int pos = (int) (mSumDx / getIntervalWidth());
    int more = (int) (mSumDx % getIntervalWidth());
    if (more > getIntervalWidth() * 0.5f) pos++;
    return pos;
}

因為我們每個item的間隔都是getIntervalWidth(),所以通過mSumDx / getIntervalWidth()就可以知道當前移到了多少個item了。因為我們已經將第一個item移到了中間,所以這里的pos就是移動mSumDx以后,中間位置item的索引。
但是又因為我們通過mSumDx / getIntervalWidth()取整數時,它的結果是向下取整的。所以,但是我們想要在中間item移動時,超過一半就切換到下一個item顯示。所以我們需要做一個兼容處理:

int more = (int) (mSumDx % getIntervalWidth());
if (more > getIntervalWidth() * 0.5f) pos++;

利用(int) (mSumDx % getIntervalWidth())得到當前正在移動的item移動過的距離,如果more大于半個item的話,那就讓pos++,將下一個item標記為center,從而讓它最后繪制,顯示在最上層。

在得到中間位置的position之后,我們還需要得到第一個可見的item的position:

public int getFirstVisiblePosition() {
    if (getChildCount() <= 0){
        return 0;
    }

    View view = getChildAt(0);
    int pos = getPosition(view);
    
    return pos;
}

這里的原理也非常簡單,就是利用getChildAt(0)得到當前在顯示的,第一個可見的item的View,然后通過getPosition(View)得到這個view在Adapter中的position。

接下來,我們就重寫getChildDrawingOrder,根據原理可得如下代碼:

protected int getChildDrawingOrder(int childCount, int i) {
    int center = getCoverFlowLayout().getCenterPosition()
            - getCoverFlowLayout().getFirstVisiblePosition(); //計算正在顯示的所有Item的中間位置
    int order;

    if (i == center) {
        order = childCount - 1;
    } else if (i > center) {
        order = center + childCount - 1 - i;
    } else {
        order = i;
    }
    return order;
}

在獲得繪圖順序的原理理解了之后,上面的代碼就沒有難度了,這里就不再細講了。到這里,我們就實現了通過更改繪圖順序的方式,讓當前選中的item在中間全部展示出來。

這樣,我們修改繪制順序的代碼就完成了,效果如下圖所示:


繪圖順序.gif

2.7、 添加滾動縮放功能

2.7.1、代碼實現

在講解《RecyclerView回收實現方式二》時,我們就已經實現了,在滾動時讓Item旋轉的功能,其實非常簡單,只需要在layoutDecoratedWithMargins后,調用setRotate系列函數即可,同樣的,我們先寫一個針對剛添加的ChildView進行縮放的函數:

private void handleChildView(View child,int moveX){
    float radio = computeScale(moveX);

    child.setScaleX(radio);
    child.setScaleY(radio);
}

private float computeScale(int x) {
    float scale = 1 -Math.abs(x * 1.0f / (8f*getIntervalWidth()));
    if (scale < 0) scale = 0;
    if (scale > 1) scale = 1;
    return scale;
}

在這兩個函數中,handleChildView函數非常容易理解,就是先通過computeScale(moveX)計算出一個要縮放的值,然后調用setScale系列函數來縮放

這里先實現效果,至于computeScale(moveX)里的公式是如何得來的,我們最后再講解,這里先用著。

接著,我們需要把handleChildView放在所有的layoutDecoratedWithMargins后,進行對剛布局的view進行縮放:

public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) {

    …………

    //回收越界子View
    for (int i = getChildCount() - 1; i >= 0; i--) {
        …………
        if (!Rect.intersects(rect, visibleRect)) {
            removeAndRecycleView(child, recycler);
            mHasAttachedItems.put(position, false);
        } else {
            layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);
            handleChildView(child,rect.left - mStartX - mSumDx);
            mHasAttachedItems.put(position, true);
        }
    }
    …………
}

private void insertView(int pos, Rect visibleRect, Recycler recycler, boolean firstPos) {
    …………
    if (Rect.intersects(visibleRect, rect) && !mHasAttachedItems.get(pos)) {
        …………
        measureChildWithMargins(child, 0, 0);
        layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);
        handleChildView(child,rect.left - mStartX - mSumDx);
        mHasAttachedItems.put(pos, true);
        
    }
}

到這里,我們就實現了開篇的效果:


縮放.gif

2.7.2、縮放系數計算原理

我們要實現在卡片滑動時平滑的縮放,所以,在滑動過程中得到的縮放因子肯定是要連續的,所以它的函數必定是可以用直線或者曲線表示的。

在這里,我直接用一條直線來計算滾動過程中縮放因子,此直線如下圖所示:


  • Y軸:表示圖片的縮放比例
  • X軸:表示item距離中心點的距離。很明顯,當中間的item的左上角在mStartX上時,此時距離中心點的距離為0,應該是最大狀態,縮放因子應該是1.我這里假設在相距一個間距(getIntervalWidth())時,大小變為7/8,當然這個值,大家都可以隨意定。

所以(0,1)、(1,7/8)這兩個點就形成了一條直線(兩點連成一條線),現在是要利用三角形相似,求出來這條直線的公式。

image.png

這里根據三角形相似求出來公式倒是難度不大,但需要注意的是,x軸上的單位是getIntervalWidth(),所以在x軸上1實際代表的是1*getIntervalWidth();

公式求出來以后,就是輸入X值,得到對應的縮放因子。那值要怎么得到呢?

我們知道X的意思是當前item與startX的間距。當間距是0時,得到1。所以x值是:rect.left - mSumDx - mStartX;

其中rect.left - mSumDx表示的是當前item在屏幕上位置。所以rect.left - mSumDx - mStartX表示的是當前item在屏幕上與mStartX的距離。

這樣,縮放系數的計算原理就講完了,當然大家也可以使用其它的縮放公式,而且也并不一定是用直線,也可以用曲線,無論用什么公式,但一定要保證是線,不能斷,一旦出現斷裂的情況,就會導致縮放不順暢,會出現突然變大或者突然變小的情況。現在,大家就可以根據自己的知識儲備自由發揮了。

2.8、bug修復

這里看似效果效果實現的非常完美,但是,當你滑動到底的時候,問題來了:


image.png

從圖中可以看到,在滑動到底的時候,停留在了倒數第二個Item被選中的狀態,應該讓最后一個item被選中,才是真正的到底。那怎么解決呢?

還記得嗎?我們在講解《自定義LayoutManager》中,在剛寫好LinearLayoutManager時,到頂和到底后都是可以繼續上滑和下滑的。我們為了到頂和到底時,不讓它繼續滑動,特地添加了邊界判斷:

public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) {
    int travel = dx;
    //如果滑動到最頂部
    if (mSumDx + dx < 0) {
        travel = -mSumDx;
    } else if (mSumDx + dx > mTotalWidth - getHorizontalSpace()) {
        //如果滑動到最底部
        travel = mTotalWidth - getHorizontalSpace() - mSumDx;
    }
    …………
}

很明顯,正是到底的時候,我們添加了判斷,讓它停留在了最后一個Item在邊界的狀態。所以,在這里,我們需要對到底判斷加以調整,讓它可滑動到最后一個item被選中的狀態為止。

首先,我們需要求出來最長能滾動的距離,因為每個item之間的間距是getIntervalWidth(),當一個item滾動距離超過getIntervalWidth()時,就會切換到下一個item被選中,所以一個item最長的滾動距離其實是getIntervalWidth(),所以最大的滾動距離是:

private int getMaxOffset() {
    return (getItemCount() - 1) * getIntervalWidth();
}

同樣,我們使用在《自定義LayoutManager》中計算較正travel的方法:

travel + mSumDx = getMaxOffset();
=> travel = getMaxOffset() - mSumDx;

所以,我們把邊界判斷的代碼改為:

public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) {
    int travel = dx;
    //如果滑動到最頂部
    if (mSumDx + dx < 0) {
        travel = -mSumDx;
    } else  if (mSumDx + dx > getMaxOffset()) {
        //如果滑動到最底部
        travel = getMaxOffset()  - mSumDx;
    }
    …………
}

現在修復了以后,到底之后就正常了,效果如下圖所示:


image.png
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容