RecyclerView重新整理機制

susion發表於2018-12-12

前面分析了RecyclerView的基本結構 本文繼續來看一下RecyclerView是如何完成UI的重新整理以及在滑動時子View的新增邏輯

本文會從原始碼分析兩件事 :

  1. adapter.notifyXXX()時RecyclerView的UI重新整理的邏輯,即子View是如何新增到RecyclerView中的。
  2. 在資料存在的情況下,滑動RecyclerView子View是如何新增到RecyclerView並滑動的。

本文不會涉及到RecyclerView的動畫,動畫的實現會專門在一篇文章中分析。

adapter.notifyDataSetChanged()引起的重新整理

我們假設RecyclerView在初始狀態是沒有資料的,然後往資料來源中加入資料後,呼叫adapter.notifyDataSetChanged()來引起RecyclerView的重新整理:

data.addAll(datas)
adapter.notifyDataSetChanged()
複製程式碼

用圖描述就是下面兩個狀態的轉換:

RecyclerView重新整理機制

接下來就來分析這個變化的原始碼,在上一篇文章中已經解釋過,adapter.notifyDataSetChanged()時,會引起RecyclerView重新佈局(requestLayout),RecyclerViewonMeasure就不看了,核心邏輯不在這裡。因此從onLayout()方法開始看:

RecyclerView.onLayout

這個方法直接呼叫了dispatchLayout:

void dispatchLayout() {
    ...
    if (mState.mLayoutStep == State.STEP_START) {
        dispatchLayoutStep1();
        dispatchLayoutStep2();
    } else if (資料變化 || 佈局變化) {
        dispatchLayoutStep2();
    }
    dispatchLayoutStep3();
}
複製程式碼

上面我裁剪掉了一些程式碼,可以看到整個佈局過程總共分為3步, 下面是這3步對應的方法:

STEP_START ->  dispatchLayoutStep1()
STEP_LAYOUT -> dispatchLayoutStep2()
STEP_ANIMATIONS -> dispatchLayoutStep2(), dispatchLayoutStep3()
複製程式碼

第一步STEP_START主要是來儲存當前子View的狀態並確定是否要執行動畫。這一步就不細看了。 而第3步STEP_ANIMATIONS是來執行動畫的,本文也不分析了,本文主要來看一下第二步STEP_LAYOUT,即dispatchLayoutStep2():

dispatchLayoutStep2()

先來看一下這個方法的大致執行邏輯:

private void dispatchLayoutStep2() {  
    startInterceptRequestLayout(); //方法執行期間不能重入
    ...
    //設定好初始狀態
    mState.mItemCount = mAdapter.getItemCount();
    mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
    mState.mInPreLayout = false;

    mLayout.onLayoutChildren(mRecycler, mState); //呼叫佈局管理器去佈局

    mState.mStructureChanged = false;
    mPendingSavedState = null;
    ...
    mState.mLayoutStep = State.STEP_ANIMATIONS; //接下來執行佈局的第三步

    stopInterceptRequestLayout(false);
}
複製程式碼

這裡有一個mState,它是一個RecyclerView.State物件。顧名思義它是用來儲存RecyclerView狀態的一個物件,主要是用在LayoutManager、Adapter等元件之間共享RecyclerView狀態的。可以看到這個方法將佈局的工作交給了mLayout。這裡它的例項是LinearLayoutManager,因此接下來看一下LinearLayoutManager.onLayoutChildren():

LinearLayoutManager.onLayoutChildren()

這個方法也挺長的,就不展示具體原始碼了。不過佈局邏輯還是很簡單的:

  1. 確定錨點(Anchor)View, 設定好AnchorInfo
  2. 根據錨點View確定有多少佈局空間mLayoutState.mAvailable可用
  3. 根據當前設定的LinearLayoutManager的方向開始擺放子View

接下來就從原始碼來看這三步。

確定錨點View

錨點View大部分是通過updateAnchorFromChildren方法確定的,這個方法主要是獲取一個View,把它的資訊設定到AnchorInfo中 :

mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout   // 即和你是否在 manifest中設定了佈局 rtl 有關

private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) {
    ...
    View referenceChild = anchorInfo.mLayoutFromEnd
            ? findReferenceChildClosestToEnd(recycler, state) //如果是從end(尾部)位置開始佈局,那就找最接近end的那個位置的View作為錨點View
            : findReferenceChildClosestToStart(recycler, state); //如果是從start(頭部)位置開始佈局,那就找最接近start的那個位置的View作為錨點View

    if (referenceChild != null) {
        anchorInfo.assignFromView(referenceChild, getPosition(referenceChild)); 
        ...
        return true;
    }
    return false;
}
複製程式碼

即, 如果是start to end, 那麼就找最接近start(RecyclerView頭部)的View作為佈局的錨點View。如果是end to start (rtl), 就找最接近end的View作為佈局的錨點。

AnchorInfo最重要的兩個屬性時mCoordinatemPosition,找到錨點View後就會通過anchorInfo.assignFromView()方法來設定這兩個屬性:

public void assignFromView(View child, int position) {
    if (mLayoutFromEnd) {
        mCoordinate = mOrientationHelper.getDecoratedEnd(child) + mOrientationHelper.getTotalSpaceChange();
    } else {
        mCoordinate = mOrientationHelper.getDecoratedStart(child);  
    }
    mPosition = position;
}
複製程式碼
  • mCoordinate其實就是錨點ViewY(X)座標去掉RecyclerView的padding。
  • mPosition其實就是錨點View的位置。

確定有多少佈局空間可用並擺放子View

當確定好AnchorInfo後,需要根據AnchorInfo來確定RecyclerView當前可用於佈局的空間,然後來擺放子View。以佈局方向為start to end (正常方向)為例, 這裡的錨點View其實是RecyclerView最頂部的View:

    // fill towards end  (1)
    updateLayoutStateToFillEnd(mAnchorInfo); //確定AnchorView到RecyclerView的底部的佈局可用空間
    ...
    fill(recycler, mLayoutState, state, false); //填充view, 從 AnchorView 到RecyclerView的底部
    endOffset = mLayoutState.mOffset; 

    // fill towards start (2)
    updateLayoutStateToFillStart(mAnchorInfo); //確定AnchorView到RecyclerView的頂部的佈局可用空間
    ...
    fill(recycler, mLayoutState, state, false); //填充view,從 AnchorView 到RecyclerView的頂部
複製程式碼

上面我標註了(1)和(2), 1次佈局是由這兩部分組成的, 具體如下圖所示 :

RecyclerView重新整理機制

然後我們來看一下fill towards end的實現:

fill towards end

確定可用佈局空間

fill之前,需要先確定從錨點ViewRecyclerView底部有多少可用空間。是通過updateLayoutStateToFillEnd方法:

updateLayoutStateToFillEnd(anchorInfo.mPosition, anchorInfo.mCoordinate);

void updateLayoutStateToFillEnd(int itemPosition, int offset) {
    mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset;
    ...
    mLayoutState.mCurrentPosition = itemPosition;
    mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END;
    mLayoutState.mOffset = offset;
    mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
}
複製程式碼

mLayoutStateLinearLayoutManager用來儲存佈局狀態的一個物件。mLayoutState.mAvailable就是用來表示有多少空間可用來佈局mOrientationHelper.getEndAfterPadding() - offset其實大致可以理解為RecyclerView的高度。所以這裡可用佈局空間mLayoutState.mAvailable就是RecyclerView的高度

擺放子view

接下來繼續看LinearLayoutManager.fill()方法,這個方法是佈局的核心方法,是用來向RecyclerView中新增子View的方法:

int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
    final int start = layoutState.mAvailable;  //前面分析,其實就是RecyclerView的高度
    ...
    int remainingSpace = layoutState.mAvailable + layoutState.mExtra;  //extra 是你設定的額外佈局的範圍, 這個一般不推薦設定
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult; //儲存佈局一個child view後的結果
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { //有剩餘空間的話,就一直新增 childView
        layoutChunkResult.resetInternal();
        ...
        layoutChunk(recycler, state, layoutState, layoutChunkResult);   //佈局子View的核心方法
        ...
        layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection; // 一次 layoutChunk 消耗了多少空間
        ...
        子View的回收工作
    }
    ...
}
複製程式碼

這裡我們不看子View回收邏輯,會在單獨的一篇文章中講。 即這個方法的核心是呼叫layoutChunk()來不斷消耗layoutState.mAvailable,直到消耗完畢。繼續看一下layoutChunk()方法, 這個方法的主要邏輯是:

  1. Recycler中獲取一個View
  2. 新增到RecyclerView
  3. 調整View的佈局引數,呼叫其measure、layout方法。
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
        View view = layoutState.next(recycler);  //這個方法會向 recycler view 要一個holder 
        ...
        if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) { //根據佈局方向,新增到不同的位置
            addView(view);   
        } else {
            addView(view, 0);
        }
        measureChildWithMargins(view, 0, 0);    //呼叫view的measure
        
        ...measure後確定佈局引數 left/top/right/bottom

        layoutDecoratedWithMargins(view, left, top, right, bottom); //呼叫view的layout
        ...
    }
複製程式碼

到這裡其實就完成了上面的fill towards end:

    updateLayoutStateToFillEnd(mAnchorInfo); //確定佈局可用空間
    ...
    fill(recycler, mLayoutState, state, false); //填充view
複製程式碼

fill towards start就是從錨點ViewRecyclerView頂部來擺放子View,具體邏輯類似fill towards end,就不細看了。

RecyclerView滑動時的重新整理邏輯

接下來我們再來分析一下在不載入新的資料情況下,RecyclerView在滑動時是如何展示子View的,即下面這種狀態 :

RecyclerView重新整理機制

下面就來分析一下3、4號和12、13號是如何展示的。

RecyclerViewOnTouchEvent對滑動事件做了監聽,然後派發到scrollStep()方法:

void scrollStep(int dx, int dy, @Nullable int[] consumed) {
    startInterceptRequestLayout(); //處理滑動時不能重入
    ...
    if (dx != 0) {
        consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
    }
    if (dy != 0) {
        consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
    }
    ...
    stopInterceptRequestLayout(false);

    if (consumed != null) { //記錄消耗
        consumed[0] = consumedX;
        consumed[1] = consumedY;
    }
}
複製程式碼

即把滑動的處理交給了mLayout, 這裡繼續看LinearLayoutManager.scrollVerticallyBy, 它直接呼叫了scrollBy(), 這個方法就是LinearLayoutManager處理滾動的核心方法。

LinearLayoutManager.scrollBy

int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
    final int absDy = Math.abs(dy);
    updateLayoutState(layoutDirection, absDy, true, state); //確定可用佈局空間
    final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false); //擺放子View
    ....
    final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
    mOrientationHelper.offsetChildren(-scrolled); // 滾動 RecyclerView
    ...
}
複製程式碼

這個方法的主要執行邏輯是:

  1. 根據佈局方向和滑動的距離來確定可用佈局空間mLayoutState.mAvailable
  2. 呼叫fill()來擺放子View
  3. 滾動RecyclerView

fill()的邏輯這裡我們就不再看了,因此我們主要看一下1 和 3

根據佈局方向和滑動的距離來確定可用佈局空間

以向下滾動為為例,看一下updateLayoutState方法:

// requiredSpace是滑動的距離;  canUseExistingSpace是true
void updateLayoutState(int layoutDirection, int requiredSpace,boolean canUseExistingSpace, RecyclerView.State state) {

    if (layoutDirection == LayoutState.LAYOUT_END) { //滾動方法為向下
        final View child = getChildClosestToEnd(); //獲得RecyclerView底部的View
        ...
        mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection; //view的位置
        mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child); //view的偏移 offset
        scrollingOffset = mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.getEndAfterPadding();
    } else {
       ...
    }
    
    mLayoutState.mAvailable = requiredSpace;  
    if (canUseExistingSpace)  mLayoutState.mAvailable -= scrollingOffset;
    mLayoutState.mScrollingOffset = scrollingOffset;
}
複製程式碼

所以可用的佈局空間就是滑動的距離。那mLayoutState.mScrollingOffset是什麼呢?

上面方法它的值是mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.getEndAfterPadding();,其實就是(childView的bottom + childView的margin) - RecyclerView的Padding。 什麼意思呢? 看下圖:

RecyclerView重新整理機制

RecyclerView的padding我沒標註,不過相信上圖可以讓你理解: 滑動佈局可用空間mLayoutState.mAvailable。同時mLayoutState.mScrollingOffset就是滾動的距離 - mLayoutState.mAvailable

所以 consumed也可以理解:

int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false);   
複製程式碼

fill()就不看了。子View擺放完畢後就要滾動佈局展示剛剛擺放好的子View。這是依靠的mOrientationHelper.offsetChildren(-scrolled), 繼續看一下是如何執行RecyclerView的滾動的

滾動RecyclerView

對於RecyclerView的滾動,最終呼叫到了RecyclerView.offsetChildrenVertical():

//dy這裡就是滾動的距離
public void offsetChildrenVertical(@Px int dy) {
    final int childCount = mChildHelper.getChildCount();
    for (int i = 0; i < childCount; i++) {
        mChildHelper.getChildAt(i).offsetTopAndBottom(dy);
    }
}
複製程式碼

可以看到邏輯很簡單,就是改變當前子View佈局的top和bottom來達到滾動的效果。

本文就分析到這裡。接下來會繼續分析RecyclerView的複用邏輯。 原始碼看的可能不是十分的細緻,如果有錯誤歡迎指出。

歡迎關注我的Android進階計劃。看更多幹貨

相關文章