RecyclerView進階之層疊列表(下)

大頭呆發表於2017-12-13

前言

昨天寫了RecyclerView進階之層疊列表(上),不過只實現了基本的效果。今天看到很多人點贊,於是我趁熱打鐵,把這個控制元件寫完成吧。沒看過前篇的同學,先移步熟悉下吧。下篇的主要內容就是實現層疊列表邊緣的層疊動畫和RecyclerView的回收複用,也是這個控制元件實現的難點所在。

層疊動畫

關於這個,先上圖吧:

RecyclerView進階之層疊列表(下)
對比系統通知欄的滑動效果,細心的同學就發現了,無論是頂部還是底部,ItemView靠近邊緣的時候並沒有變慢,從而產生一個多層層疊的效果,所以我們先來實現這個效果。 先定義兩個動畫引數:

private @FloatRange(from = 0.01, to = 1.0)
float edgePercent = 0.5f;//觸發邊緣動畫距離百分比

private @IntRange(from = 1)
int slowTimes = 5;//到達此距離後放慢倍數
複製程式碼

然後在滾動時重新佈局列表各個ItemView位置的方法中處理動畫,關鍵點是在ItemView到達邊界臨界點時,將原本的偏移值除以一個倍數,使其移動速度變慢:

   private void addAndLayoutViewVertical(RecyclerView.Recycler recycler, RecyclerView.State state, int offset) {
       //反向遍歷Recycler中儲存的View取出來
      for (int i = itemCount - 1; i >= 0; i--) {
        Rect mTmpRect = allItemRects.get(i);
        int bottomOffset = mTmpRect.bottom - offset;
        int topOffset = mTmpRect.top - offset;
        if (i != itemCount - 1) {//除最後一個外的底部慢速動畫
            if (displayHeight - bottomOffset <= height * edgePercent) {
                //到達邊界觸發距離
                int edgeDist = (int) (displayHeight - height * edgePercent);
                //到達邊界後速度放慢到原來5分之一,計算出實際需要的底部位置
                int bottom = edgeDist + (bottomOffset - edgeDist) / slowTimes;
                //當然這個位置不能超過底部
                bottom=Math.min(bottom,displayHeight);
                realBottomOffset = bottom;
            }
        } else {
             // 如果是最後一個就不需要動畫了,因為已經在底部了
            realBottomOffset = totalHeight > displayHeight ? displayHeight : totalHeight;
        }
    }
   }
複製程式碼

上面是底部邊界動畫的處理,頂部邊界動畫的處理也是一樣的。現在我們已經可以看到有層疊的效果了:

RecyclerView進階之層疊列表(下)
橫向的看起來效果也不錯:
RecyclerView進階之層疊列表(下)

回收複用功能

這個功能如果沒有實現的話,就對不起RecyclerView的名號了。下面都只貼了核心程式碼:

  @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    //在每次觸發滾動的時候,把所有View從RecyclerView移除掉,這樣recyclerView就沒有itemview了
        detachAndScrapAttachedViews(recycler);
   //從新佈局位置、顯示View
        addAndLayoutViewVertical(recycler, state, verticalScrollOffset); 
        return tempDy;
    }
複製程式碼

上面已經吧所有的itemView移除了,現在遍歷所有的itemView,判斷是否在螢幕中顯示,是的話就重新新增進去,不是的話就跳過。因為有邊緣層疊動畫,我們放慢了速度,但是比較的仍是原來滾動的距離,overFlyingDist= slowTimes * height;

 private void addAndLayoutViewVertical(RecyclerView.Recycler recycler, RecyclerView.State state, int offset) {
        int itemCount = getItemCount();
        if (itemCount <= 0 || state.isPreLayout()) {
            return;
        }
        int displayHeight = getVerticalSpace();
        for (int i = itemCount - 1; i >= 0; i--) {
            Rect mTmpRect = allItemRects.get(i);
            // 遍歷Recycler中儲存的View取出來
            int bottomOffset = mTmpRect.bottom - offset;
            int topOffset = mTmpRect.top - offset;
            boolean needAdd = true;//是否
            if (bottomOffset - displayHeight >= overFlyingDist) {
            //超過了底部最大距離
                needAdd = false;
            }
            if (topOffset < -overFlyingDist && i != 0 && topOverFlying
                    || topOffset < -overFlyingDist && !topOverFlying) {
             //超過了頂部最大距離
                needAdd = false;
            }
            if (needAdd) {
            //開始佈局
           ..........
           }
    Log.d(TAG, "childCount = " + getChildCount() + "  itemCount= " + itemCount);
    }
複製程式碼

列印日誌看下結果,即使把列表總數加到1000項,recyclerView中的itemView數量也始終維持在實際顯示數量附近,順便也解決了因為Itemview沒有回收,兩邊陰影層疊在一起形成黑邊的問題:

RecyclerView進階之層疊列表(下)

完善功能

到這已經基本實現了我們所期望的功能。但是和系統自帶的三個layoutManager相比,還欠缺很多基本的對外操作方法,所以我們來依照LinearLayoutManager實現幾個同名方法:

findViewByPosition(int position)

@Override
    public View findViewByPosition(int position) {
        final int childCount = getChildCount();
        if (childCount == 0) {
            return null;
        }
        final int firstChild = getPosition(getChildAt(0));
        final int viewPosition = position - firstChild;
        if (viewPosition >= 0 && viewPosition < childCount) {
            final View child = getChildAt(viewPosition);
            if (getPosition(child) == position) {
                return child; // in pre-layout, this may not match
            }
        }
        return super.findViewByPosition(position);
    }
複製程式碼

findViewByPosition(int position)

requestLayout()方法會呼叫onLayoutChildren(),offsetUseful 用來標識onLayoutChildren()時是否將引數重置

 @Override
    public void scrollToPosition(int position) {
        Rect mTmpRect = allItemRects.get(position);
        if (mTmpRect != null) {
            offsetUseful = true;
            if (orientation == OrientationHelper.VERTICAL) {
                verticalScrollOffset = mTmpRect.top;
            } else {
                horizontalScrollOffset = mTmpRect.left;
            }
        }
        requestLayout();
    }

複製程式碼

RecyclerView進階之層疊列表(下)

好了至於別的方法,大家如果感興趣的話自己去看原始碼吧!這個控制元件還有待完善的地方,我會持續補充,歡迎大家提Issues,或者給個star!

Github地址

相關文章