詳解RecyclerView的預佈局

zxzhang發表於2023-10-07

概述

RecyclerView 的預佈局用於 Item 動畫中,也叫做預測動畫。其用於當 Item 項進行變化時執行的一次佈局過程(如新增或刪除 Item 項),使 ItemAnimator 體驗更加友好。

考慮以下 Item 項刪除場景,螢幕內的 RecyclerView 列表包含兩個 Item 項:item1 和 item2。當刪除 item2 時,item3 從底部平滑出現在 item2 的位置。

+-------+                       +-------+           
|       | <-----+               |       | <-----+   
| item1 |       |               | item1 |       |   
|       |       |               |       |       |   
+-------+     screen   ---->    +-------+     screen
|       |       |               |       |       |   
| item2 |       |               | item3 |       |   
|       | <-----+               |       | <-----+   
+-------+                       +-------+           

上述效果是如何實現的?我們知道 RecyclerView 只會佈局螢幕內可見的 Item ,對於螢幕外的 item3,如何知道其要執行的動畫軌跡呢?要形成軌跡,至少需要知道起始點,而 item3 的終點位置是很明確的,也就是被刪除的 item2 位置。那起點是如何確定的呢?
對於這種情況,Recyclerview 會進行兩次佈局,第一次被稱為 pre-layout,也就是預佈局,其會將不可見的 item3 也載入進佈局內,得到 [item1, item2, item3] 的佈局資訊。之後再執行一次 post-layout,得到 [item1, item3] 的佈局資訊,比對兩次 item3 的佈局資訊,也就確定了 item3 的動畫軌跡了。

以下分析過程我們先定義一個大前提:LayoutManager 為 LinearLayoutManager,場景為上述描述的 item 刪除場景,螢幕能夠同時容納兩個 Item。

預佈局

我們知道 RecyclerView 有三個重要的 layout 階段,分別為:dispatchLayoutStep1dispatchLayoutStep2dispatchLayoutStep3。這裡先直接了當的告知結論:pre-layout 發生於 dispatchLayoutStep1 階段,而 post-layout 則發生於 dispatchLayoutStep2 階段。

在執行 item2 的刪除時,我們透過呼叫 Adapter#notifyItemRemoved 來通知 RecyclerView 發生了變化,其呼叫鏈如下:

RecyclerView.Adapter#notifyItemRemoved
	RecyclerView.RecyclerViewDataObserver#onItemRangeRemoved
		AdapterHelper#onItemRangeRemoved
		RecyclerView.RecyclerViewDataObserver#triggerUpdateProcessor
			RecyclerView#requestLayout

RecyclerView 中的變更操作會被封裝為 UpdateOp 操作,這裡刪除動作被封裝為一個 UpdateOp,新增到 mPendingUpdates 中等待處理。其處理時機為dispatchLayoutStep1 階段,根據 mPendingUpdates 中的 UpdateOp 來更新列表和 ViewHolder 的資訊。

// AdapterHelper#onItemRangeRemoved
boolean onItemRangeRemoved(int positionStart, int itemCount) {
	if (itemCount < 1) {
		return false;
	}
	mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount, null));
	mExistingUpdateTypes |= UpdateOp.REMOVE;
	return mPendingUpdates.size() == 1;
}

呼叫鏈最終會走到 RecyclerView#requestLayout 方法,進而觸發 RecyclerView#onLayout 方法的呼叫。onLayout 做的事比較簡單,直接呼叫 dispatchLayout 將佈局事件分發下去,然後將 mFirstLayoutComplete 賦值為true,也就是隻要執行過一次 dispatchLayout 那麼這個值就會為 true,這個值在後續分析中會用到。

// RecyclerView#onLayout
@Override  
protected void onLayout(boolean changed, int l, int t, int r, int b) {  
    TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);  
    dispatchLayout();  
    TraceCompat.endSection();  
    mFirstLayoutComplete = true;  
}

dispatchLayout 根據狀態會呼叫 layout 的三個重要階段,保證 layout 三個重要階段至少被執行一次。

// RecyclerView#dispatchLayout
void dispatchLayout() {

    ...

    if (mState.mLayoutStep == State.STEP_START) {
        dispatchLayoutStep1();
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else if (mAdapterHelper.hasUpdates()
            || needsRemeasureDueToExactSkip
            || mLayout.getWidth() != getWidth()
            || mLayout.getHeight() != getHeight()) {
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else {
        // always make sure we sync them (to ensure mode is exact)
        mLayout.setExactMeasureSpecsFrom(this);
    }
    dispatchLayoutStep3();
}

dispatchLayoutStep1 中根據 mRunPredictiveAnimations 的值來決定是否處於 pre-layout 中,而 mInPreLayoutmRunPredictiveAnimations 初始值都是false,因此我們需要找到 mRunPredictiveAnimations 被賦值的地方。

// RecyclerView#dispatchLayoutStep1
private void dispatchLayoutStep1() {

    ...
    mState.mInPreLayout = mState.mRunPredictiveAnimations;
    ...
}

// RecyclerView#State
public static class State {
    boolean mInPreLayout = false;
    
    boolean mRunPredictiveAnimations = false;
}

mRunPredictiveAnimations 由多個變數共同決定,從程式碼中我們知道只有 mFirstLayoutComplete 為 true 之後才有可能開始執行 ItemAnimator 和預測動畫(決定了是否執行 pre-layout),而 mFirstLayoutComplete 只有完成一次 onLayout 才會被賦值為 true,因此可以得知另一個資訊:RecyclerView 不支援初始動畫。
這裡不去一一分析每一個變數的賦值時機,從除錯可知這裡 mRunSimpleAnimationsmRunPredictiveAnimations 會被賦值為true(或者從現象反推,表項刪除是帶有動畫的,因此這裡也必須為 true 才能支援表項刪除的 ItemAnimator)。

// RecyclerView#processAdapterUpdatesAndSetAnimationFlags
private void processAdapterUpdatesAndSetAnimationFlags() {
    ...
    boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;

    mState.mRunSimpleAnimations = mFirstLayoutComplete
            && mItemAnimator != null
            && (mDataSetHasChangedAfterLayout
            || animationTypeSupported
            || mLayout.mRequestedSimpleAnimations)
            && (!mDataSetHasChangedAfterLayout
            || mAdapter.hasStableIds());
    mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
            && animationTypeSupported
            && !mDataSetHasChangedAfterLayout
            && predictiveItemAnimationsEnabled();
}

繼續檢視 dispatchLayoutStep1 的其他程式碼,當可以執行預測動畫時,會呼叫 LayoutManageronLayoutChildren 方法。

private void dispatchLayoutStep1() {
    ...
    processAdapterUpdatesAndSetAnimationFlags();
    
    if (mState.mRunPredictiveAnimations) {
        ...
        mLayout.onLayoutChildren(mRecycler, mState);
    }
    ...
    mState.mLayoutStep = State.STEP_LAYOUT;
}

檢視 onLayoutChildren 程式碼,其註釋資訊如下:

佈局 Adapter 的所有相關子檢視。LayoutManager 負責 Item 動畫的行為。預設情況下,RecyclerView 有一個非空的 ItemAnimator,並且啟用簡單的 item 動畫。這意味著 Adapter 上的新增/刪除操作會伴隨有相關動畫出現。如果 LayoutManager 的 supportsPredictiveItemAnimations() (預設值)返回 false,並在 onLayoutChildren(RecyclerView.Recycler, RecyclerView.State) 執行正常的佈局操作, RecyclerView 也有足夠的資訊以簡單的方式執行這些動畫。
當 LayoutManager 想要擁有使用者體驗更加友好的 ItemAnimator,那麼 LayoutManager 應該讓 supportsPredictiveItemAnimations() 返回 true 並向 onLayoutChildren( RecyclerView.Recycler、RecyclerView.State) 新增額外邏輯。支援預測動畫意味著 onLayoutChildren(RecyclerView.Recycler, RecyclerView.State) 將被呼叫兩次; 一次作為“預”佈局來確定 Item 在實際佈局之前的位置,並再次進行“實際”佈局。在預佈局階段,Item 將記住它們的預佈局位置,以便能夠被佈局正確。 此外,移除的專案將從 scrap 列表中返回,以幫助確定其他專案的正確放置。 這些刪除的專案不應新增到子列表中,而應用於幫助計算其他檢視的正確位置,包括以前不在螢幕上的檢視(稱為 APPEARING view),但可以確定其預佈局螢幕外位置 給出有關預佈局刪除檢視的額外資訊。
第二次佈局是真正的佈局,其中僅使用未刪除的檢視。 此過程中唯一的附加要求是,如果 supportsPredictiveItemAnimations() 返回 true,請注意哪些檢視在佈局之前存在於子列表中,哪些檢視在佈局之後不存在(稱為 DISAPPEARING view),並定位/佈局這些檢視,而不考慮 RecyclerView 的實際邊界。這使得動畫系統能夠知道將這些消失的檢視動畫化到的位置。
RecyclerView 的預設 LayoutManager 實現已經處理了所有這些動畫要求。 RecyclerView 的客戶端可以直接使用這些佈局管理器之一,也可以檢視它們的 onLayoutChildren() 實現,以瞭解它們如何解釋 APPEARING 和 DISAPPEARING 檢視。

簡單總結為:為了在 Item 項變更時能夠獲得更好的動畫體驗,onLayoutChildren 會被呼叫兩次,一次用於 pre-layout(預佈局),一次用於 post-layout(實際佈局)。檢視程式碼呼叫時機,onLayoutchildren 一次在 dispatchLayoutStep1 呼叫,一次在 dispatchLayoutStep2 呼叫。

onLayoutChildren 中會呼叫 fill 函式進行佈局填充。能否繼續填充由 layoutState.mInfiniteremainingSpacelayoutState.hasMore(state) 共同決定。
layoutState.mInfinite 表示無限填充,不適用於我們分析的case,因此關鍵在於 remainingSpace 值的變更。在 pre-layout 中會多佈局一次,也就是說相比較於 post-layout,remainingSpace 在 pre-layout 少消費了一次。
remainingSpace 的變更受三個變數控制,由於處在 pre-layout 階段,因此 state.isPreLayout 為 true,layoutState.mScrapList 此處還未被賦值,因此關注 layoutChunkResult.mIgnoreConsumed 變數。

// LinearLayoutManager#onLayoutChildren
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    fill(recycler, mLayoutState, state, false);
    ...
}

// LinearLayoutManager#fill
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
    ...
    int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        layoutChunkResult.resetInternal();
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        if (layoutChunkResult.mFinished) {
            break;
        }
        layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
        if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                || !state.isPreLayout()) {
            layoutState.mAvailable -= layoutChunkResult.mConsumed;
            // we keep a separate remaining space because mAvailable is important for recycling
            remainingSpace -= layoutChunkResult.mConsumed;
        }
        ...
    }
    return start - layoutState.mAvailable;
}

layoutChunkResultfill 過程被當作引數傳入 layoutChunk ,因此我們需要關注下是否是這裡導致了 layoutChunkResult 的成員變數改變了。
檢視 layoutChunk 原始碼,當 Item 項被標記為 Remove 時,會將 mIgnoreConsumed 變數置為 true,因此在 fill 過程會忽略被刪除的 Item 項的佈局佔用。

// LinearLayoutManager#layoutChunkResult
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
    ...
    if (params.isItemRemoved() || params.isItemChanged()) {
        result.mIgnoreConsumed = true;
    }
}

// RecyclerView#LayoutParams
public boolean isItemRemoved() {  
    return mViewHolder.isRemoved();  
}

// RecyclerView#ViewHolder
boolean isRemoved() {  
    return (mFlags & FLAG_REMOVED) != 0;  
}

ViewHolder 資訊更新

我們透過 notifyItemRemoved 來通知 RecyclerView Item 項被刪除,那麼在什麼時候 ViewHolder 被標記為刪除狀態呢?
預佈局 一節中我們知道呼叫最終走到 requestLayout 中,進入觸發 onLayout 方法。在佈局過程中,關於 ViewHolder 資訊更新的呼叫鏈路如下:

RecyclerView#dispatchLayoutStep1
	RecyclerView#processAdapterUpdatesAndSetAnimationFlags
		AdapterHelper#preProcess
			AdapterHelper#applyRemove
				AdapterHelper#postponeAndUpdateViewHolders
					AdapterHelper#Callback#offsetPositionsForRemovingLaidOutOrNewView
						RecyclerView#offsetPositionRecordsForRemove
							ViewHolder#offsetPosition
							ViewHolder#flagRemovedAndOffsetPosition

postponeAndUpdateViewHolders 會呼叫 Adapterhelper#Callback 介面的方法,這個介面在 AdapterHelper 例項化的時候進行實現。最終會走到 offsetPositionRecordsForRemove 方法,這裡會對 ViewHolder 相關的 position 和 flag 變數進行修改。

AdapterHelper#Callback 介面實現的地方:

// RecyclerView#initAdapterManager
void initAdapterManager() {  
    mAdapterHelper = new AdapterHelper(new AdapterHelper.Callback() {
        ...
    }
}

回看 dispatchLayoutStep1 階段的 processAdapterUpdatesAndSetAnimationFlags 呼叫,在執行 pre-layout 時會呼叫 mAdapterHelper.preProcess()

// RecyclerView#processAdapterUpdatesAndSetAnimationFlags
private void processAdapterUpdatesAndSetAnimationFlags() {
    ...
	if (predictiveItemAnimationsEnabled()) {  
	    mAdapterHelper.preProcess();  
	} else {  
	    mAdapterHelper.consumeUpdatesInOnePass();  
	}
	...
}

preProcess 根據 mPendingUpdates 中儲存的 UpdateOp 來決定執行相關動作。這與我們上述講到的 RecyclerView 中的變更操作會被封裝為 UpdateOp 操作,新增到 mPendingUpdates 中等待處理 相呼應,這裡就是對應的處理邏輯。(額外說一下 AdapterHelper 就是用來儲存和處理 UpdateOp 的相關工具類,根據儲存的 UpdateOp 列表,計算 position 等資訊)

// AdapterHelper#preProcess
void preProcess() {
    mOpReorderer.reorderOps(mPendingUpdates);
    final int count = mPendingUpdates.size();
    for (int i = 0; i < count; i++) {
        UpdateOp op = mPendingUpdates.get(i);
        switch (op.cmd) {
            ...
            case UpdateOp.REMOVE:
                applyRemove(op);
                break;
            ...
        }
        if (mOnItemProcessedCallback != null) {
            mOnItemProcessedCallback.run();
        }
    }
    mPendingUpdates.clear();
}

根據上述的呼叫鏈關係,由於中間的呼叫過程都是一些比較簡單的邏輯中轉,我們直接檢視 RecyclerView#offsetPositionRecordsForRemove 程式碼的相關邏輯。
由於有 Item 項被刪除了,那麼它後面的 Item 項的位置資訊就需要被更新。這裡分兩個分支邏輯進行處理:

  • 被刪除的 Item 項呼叫 flagRemovedAndOffsetPosition
  • 被刪除的 Item 項之後的 Item 項呼叫 offsetPosition
void offsetPositionRecordsForRemove(int positionStart, int itemCount,
		boolean applyToPreLayout) {
	final int positionEnd = positionStart + itemCount;
	final int childCount = mChildHelper.getUnfilteredChildCount();
	for (int i = 0; i < childCount; i++) {
		final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
		if (holder != null && !holder.shouldIgnore()) {
			if (holder.mPosition >= positionEnd) {
				holder.offsetPosition(-itemCount, applyToPreLayout);
				mState.mStructureChanged = true;
			} else if (holder.mPosition >= positionStart) {
				holder.flagRemovedAndOffsetPosition(positionStart - 1, -itemCount,
						applyToPreLayout);
				mState.mStructureChanged = true;
			}
		}
	}
	mRecycler.offsetPositionRecordsForRemove(positionStart, itemCount, applyToPreLayout);
	requestLayout();
}

flagRemovedAndOffsetPosition 中將 ViewHolder 的標誌位新增上了 FLAG_REMOVED 的刪除標誌。

// ViewHolder#flagRemovedAndOffsetPosition
void flagRemovedAndOffsetPosition(int mNewPosition, int offset, boolean applyToPreLayout) {
	addFlags(ViewHolder.FLAG_REMOVED);
	offsetPosition(offset, applyToPreLayout);
	mPosition = mNewPosition;
}

此外對於 ViewHolder 的 position 相關資訊也會被更新。

void offsetPosition(int offset, boolean applyToPreLayout) {
	if (mOldPosition == NO_POSITION) {
		mOldPosition = mPosition;
	}
	if (mPreLayoutPosition == NO_POSITION) {
		mPreLayoutPosition = mPosition;
	}
	if (applyToPreLayout) {
		mPreLayoutPosition += offset;
	}
	mPosition += offset;
	if (itemView.getLayoutParams() != null) {
		((LayoutParams) itemView.getLayoutParams()).mInsetsDirty = true;
	}
}

預佈局和後佈局的差異

根據上述的分析我們知道,onLayoutChildren 會被呼叫兩次,一次用於 pre-layout,一次用於 post-layout。那麼他們的區別在哪,為何會造成 pre-layout 的佈局快照為 [item1, item2, item3], 而 post-layout 的佈局快照為 [item1, item3] 呢?

回看 onLayoutChildren 原始碼,在進行 fill 填充時,會先呼叫 detachAndScrapAttachedViews 將螢幕內的 Item 項先進行 detach 放置到 mAttachedScrap 中儲存。此時 mAttachedScrap 中儲存著 item1 和 item2。

// LinearLayoutManager#onLayoutChildren
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    detachAndScrapAttachedViews(recycler);
    ...
    fill(recycler, mLayoutState, state, false);
    ...
}

detachAndScrapAttachedViews 遍歷列表中的子 View,將他們都暫時 detach 掉。

// RecyclerView#detachAndScrapAttachedViews
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
	final int childCount = getChildCount();
	for (int i = childCount - 1; i >= 0; i--) {
		final View v = getChildAt(i);
		scrapOrRecycleView(recycler, i, v);
	}
}

// RecyclerView#scrapOrRecycleView
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
	final ViewHolder viewHolder = getChildViewHolderInt(view);
	if (viewHolder.shouldIgnore()) {
		if (DEBUG) {
			Log.d(TAG, "ignoring view " + viewHolder);
		}
		return;
	}
	if (viewHolder.isInvalid() && !viewHolder.isRemoved()
			&& !mRecyclerView.mAdapter.hasStableIds()) {
		removeViewAt(index);
		recycler.recycleViewHolderInternal(viewHolder);
	} else {
		detachViewAt(index);
		recycler.scrapView(view);
		mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
	}
}

在上述分析中,我們知道刪除一個 Item 項,其會被新增上 FLAG_REMOVED 的標記位,因此對於 scrapView 的邏輯,其會走入第一個分支邏輯,將 Viewholder 加入到 mAttachedScap 中。

// RecyclerView#Recycler#scrapView
void scrapView(View view) {  
    final ViewHolder holder = getChildViewHolderInt(view);  
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)  
            || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {  
        if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {  
            throw new IllegalArgumentException("Called scrap view with an invalid view."  
                    + " Invalid views cannot be reused from scrap, they should rebound from"  
                    + " recycler pool." + exceptionLabel());  
        }  
        holder.setScrapContainer(this, false);  
        mAttachedScrap.add(holder);  
    } else {  
        if (mChangedScrap == null) {  
            mChangedScrap = new ArrayList<ViewHolder>();  
        }  
        holder.setScrapContainer(this, true);  
        mChangedScrap.add(holder);  
    }  
}

回到 fill 原始碼,fill 的核心填充邏輯呼叫的 layoutChunklayoutChunk 將獲取填充的 View 委託給了 layoutState.next 方法。

// LinearLayoutManager#layoutChunk
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,  
        LayoutState layoutState, LayoutChunkResult result) {  
    View view = layoutState.next(recycler);
    ...
}

LayoutState#next 內部執行邏輯即為 RecyclerView 快取複用的核心流程。呼叫鏈如下:

LayoutState#next
	LinearLayoutManager#Recycler#getViewForPosition
		LinearLayoutManager#Recycler#tryGetViewHolderForPositionByDeadline
			RecyclerView#Recycler#getChangedScrapViewForPosition
			RecyclerView#Recycler#getScrapOrHiddenOrCachedHolderForPosition
			RecyclerView#RecycledViewPool#getRecycledView
			RecyclerView#Adapter#createViewHolder

我們重點關注 getScrapOrHiddenOrCachedHolderForPosition, 因為它包含了從 mAttachedScap 列表獲取 ViewHolder 的相關邏輯,而 mAttachedScap 我們上述講過,onLayoutChildren 過程 detach 的 ViewHolder 會存放在這裡。

// RecyclerView#Recycler#getScrapOrHiddenOrCachedHolderForPosition
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
    final int scrapCount = mAttachedScrap.size();

    // Try first for an exact, non-invalid match from scrap.
    for (int i = 0; i < scrapCount; i++) {
        final ViewHolder holder = mAttachedScrap.get(i);
        if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
                && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
            return holder;
        }
    }

    ...
    
    return null;
}

對於是否能夠複用 mAttachedScap 中的 ViewHolder 取決於多個變數的共同作用。

  • holder.wasReturnedFromScrap(): 由於 ViewHolder 剛被加入到 mAttachedScap 中,因此其還沒有被標記上 FLAG_RETURNED_FROM_SCRAP 標誌。另外,當能夠複用時,被標記了 FLAG_RETURNED_FROM_SCRAP 標誌,其也會在 RecyclerView#LayoutManager#addViewInt 中被清除掉。
  • holder.isInvalid():Item 項在刪除過程沒有被標記上 FLAG_INVALID 標誌。
  • mState.mInPreLayout || !holder.isRemoved() 是否處於預佈局或不被刪除

這裡重點講一下 holder.getLayoutPosition() == position

// RecyclerView#ViewHolder#getLayoutPosition
public final int getLayoutPosition() {
	return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition;
}

只有當位置不變時,才能被複用。因為從 mAttachedScap 中複用的 ViewHolder 不會再進行 bind 操作。
對於 getLayoutPosition 的取值由 mPreLayoutPositionmPosition 共同作用。這兩個變數的取值,其可能會在多處被修改。在初始呼叫 notifyItemRangeRemoved 通知 RecyclerView 的 Item 項被刪除時,在 offsetPositionRecordsForRemovemPosition 會被修正為 Item 已經被刪除的正確位置,這個我們在 ViewHolder 資訊更新 一節中有過闡述。

item3 由於不存在於 mAttachedScap 和各級快取中,因此需要被建立。同時在 tryGetViewHolderForPositionByDeadline 中會將 mPositionmPreLayoutPosition 進行正確賦值。
isBound 表示 ViewHolder 是否完成佈局,對於剛透過 createViewHolder 建立的 ViewHolder 其為 false, 因此會走入第二個分支邏輯,進行資料繫結以及 position 資料的更新。
注意 offsetPosition 的相關計算,其呼叫 mAdapterHelper.findPositionOffset(position) 得到。作用與上述講到的 offsetPositionRecordsForRemove 作用類似。AdapterHelper 會根據 UpdateOp 來為 ViewHolder 提供正確的 position 資訊。

// RecyclerView#ViewHolder#tryGetViewHolderForPositionByDeadline
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
    ...
    if (holder == null) {
		...
		holder = mAdapter.createViewHolder(RecyclerView.this, type);
	}
    ...
    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        // do not update unless we absolutely have to.
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        if (DEBUG && holder.isRemoved()) {
            throw new IllegalStateException("Removed holder should be bound and it should"
                    + " come here only in pre-layout. Holder: " + holder
                    + exceptionLabel());
        }
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    ...
}

此時 pre-layout 的中 ViewHolder 的 position 資訊如下:

mPosition mPreLayoutPosition
item1 0 0
item2 0 1
item3 1 2

其得到的佈局快照為 [item1, item2, item3]

dispatchLayoutStep1 原始碼中,在函式體結尾處會呼叫 clearOldPositionsmPreLayoutPosition 重置。因此在 dispatchLayoutStep2 中進行 post-layout 過程中呼叫 getLayoutPosition 的值由 mPosition 決定。

// RecyclerView#dispatchLayoutStep1
private void dispatchLayoutStep1() {
    ...
    if (mState.mRunPredictiveAnimations) {
        mLayout.onLayoutChildren(mRecycler, mState);
        ...
        clearOldPositions();
    }
    ...
}

// RecyclerView#clearOldPositions
void clearOldPositions() {
	final int childCount = mChildHelper.getUnfilteredChildCount();
	for (int i = 0; i < childCount; i++) {
		final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
		if (!holder.shouldIgnore()) {
			holder.clearOldPosition();
		}
	}
	mRecycler.clearOldPositions();
}

// RecyclerView#ViewHolder#clearOldPosition
void clearOldPosition() {  
    mOldPosition = NO_POSITION;  
    mPreLayoutPosition = NO_POSITION;  
}

此時 post-layout 中 ViewHolder 的 position 資訊如下:

mPosition mPreLayoutPosition
item1 0 -1
item2 0 -1
item3 1 -1

因此 getScrapOrHiddenOrCachedHolderForPosition 只有 item1 和 item3 能夠命中 mAttachedScap 的 ViewHolder,形成 [item1, item3] 的佈局快照。

以上分析有很多細節點可能沒有很詳盡地闡述,因為 RecyclerView 當中應用的概念過於複雜。如果發散開來,形成的篇幅很大,且不易把握主題。
對於 預佈局和後佈局的差異 這一節中,整體脈絡涉及到 Item佈局、 ViewHolder 快取複用以及增刪變更帶來的 Position 等資訊的變化,內容多資訊量大,不易快速掌握。建議寫一個精簡 demo,跟隨文章中闡述的呼叫鏈實際除錯走一把,感受各個變數值變更的過程,加快理解。

相關文章