RecyclerView刷新機制

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

本文會從源碼分析兩件事 :github

  1. adapter.notifyXXX()時RecyclerView的UI刷新的邏輯,即子View是如何添加到RecyclerView中的。
  2. 在數據存在的狀況下,滑動RecyclerView子View是如何添加到RecyclerView並滑動的。

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

adapter.notifyDataSetChanged()引發的刷新

咱們假設RecyclerView在初始狀態是沒有數據的,而後往數據源中加入數據後,調用adapter.notifyDataSetChanged()來引發RecyclerView的刷新:源碼分析

data.addAll(datas)
adapter.notifyDataSetChanged()
複製代碼

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

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

RecyclerView.onLayout

這個方法直接調用了dispatchLayout:動畫

void dispatchLayout() {
    ...
    if (mState.mLayoutStep == State.STEP_START) {
        dispatchLayoutStep1();
        dispatchLayoutStep2();
    } else if (數據變化 || 佈局變化) {
        dispatchLayoutStep2();
    }
    dispatchLayoutStep3();
}
複製代碼

上面我裁剪掉了一些代碼,能夠看到整個佈局過程總共分爲3步, 下面是這3步對應的方法:ui

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

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

dispatchLayoutStep2()

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

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次佈局是由這兩部分組成的, 具體以下圖所示 :

而後咱們來看一下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的,即下面這種狀態 :

下面就來分析一下三、4號和十二、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的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進階計劃。看更多幹貨

相關文章
相關標籤/搜索