概述
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 階段,分別為:dispatchLayoutStep1
、dispatchLayoutStep2
和 dispatchLayoutStep3
。這裡先直接了當的告知結論: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 中,而 mInPreLayout
和 mRunPredictiveAnimations
初始值都是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 不支援初始動畫。
這裡不去一一分析每一個變數的賦值時機,從除錯可知這裡 mRunSimpleAnimations
和 mRunPredictiveAnimations
會被賦值為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
的其他程式碼,當可以執行預測動畫時,會呼叫 LayoutManager
的 onLayoutChildren
方法。
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.mInfinite
或 remainingSpace
和 layoutState.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;
}
layoutChunkResult
在 fill
過程被當作引數傳入 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
的核心填充邏輯呼叫的 layoutChunk
。layoutChunk
將獲取填充的 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
的取值由 mPreLayoutPosition
和 mPosition
共同作用。這兩個變數的取值,其可能會在多處被修改。在初始呼叫 notifyItemRangeRemoved
通知 RecyclerView 的 Item 項被刪除時,在 offsetPositionRecordsForRemove
中 mPosition
會被修正為 Item 已經被刪除的正確位置,這個我們在 ViewHolder 資訊更新 一節中有過闡述。
item3 由於不存在於 mAttachedScap
和各級快取中,因此需要被建立。同時在 tryGetViewHolderForPositionByDeadline
中會將 mPosition
和 mPreLayoutPosition
進行正確賦值。
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
原始碼中,在函式體結尾處會呼叫 clearOldPositions
將 mPreLayoutPosition
重置。因此在 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,跟隨文章中闡述的呼叫鏈實際除錯走一把,感受各個變數值變更的過程,加快理解。