概述
RecyclerView 的預(yù)布局用于 Item 動(dòng)畫中,也叫做預(yù)測(cè)動(dòng)畫。其用于當(dāng) Item 項(xiàng)進(jìn)行變化時(shí)執(zhí)行的一次布局過程(如添加或刪除 Item 項(xiàng)),使 ItemAnimator 體驗(yàn)更加友好。
考慮以下 Item 項(xiàng)刪除場(chǎng)景,屏幕內(nèi)的 RecyclerView 列表包含兩個(gè) Item 項(xiàng):item1 和 item2。當(dāng)刪除 item2 時(shí),item3 從底部平滑出現(xiàn)在 item2 的位置。
+-------+ +-------+
| | <-----+ | | <-----+
| item1 | | | item1 | |
| | | | | |
+-------+ screen ----> +-------+ screen
| | | | | |
| item2 | | | item3 | |
| | <-----+ | | <-----+
+-------+ +-------+
上述效果是如何實(shí)現(xiàn)的?我們知道 RecyclerView 只會(huì)布局屏幕內(nèi)可見的 Item ,對(duì)于屏幕外的 item3,如何知道其要運(yùn)行的動(dòng)畫軌跡呢?要形成軌跡,至少需要知道起始點(diǎn),而 item3 的終點(diǎn)位置是很明確的,也就是被刪除的 item2 位置。那起點(diǎn)是如何確定的呢? 對(duì)于這種情況,Recyclerview 會(huì)進(jìn)行兩次布局,第一次被稱為 pre-layout,也就是預(yù)布局,其會(huì)將不可見的 item3 也加載進(jìn)布局內(nèi),得到 [item1, item2, item3] 的布局信息。之后再執(zhí)行一次 post-layout,得到 [item1, item3] 的布局信息,比對(duì)兩次 item3 的布局信息,也就確定了 item3 的動(dòng)畫軌跡了。
以下分析過程我們先定義一個(gè)大前提:LayoutManager 為 LinearLayoutManager,場(chǎng)景為上述描述的 item 刪除場(chǎng)景,屏幕能夠同時(shí)容納兩個(gè) Item。
預(yù)布局
我們知道 RecyclerView 有三個(gè)重要的 layout 階段,分別為:dispatchLayoutStep1、dispatchLayoutStep2 和 dispatchLayoutStep3。這里先直接了當(dāng)?shù)母嬷Y(jié)論:pre-layout 發(fā)生于 dispatchLayoutStep1 階段,而 post-layout 則發(fā)生于 dispatchLayoutStep2 階段。
在執(zhí)行 item2 的刪除時(shí),我們通過調(diào)用 Adapter#notifyItemRemoved 來通知 RecyclerView 發(fā)生了變化,其調(diào)用鏈如下:
RecyclerView.Adapter#notifyItemRemoved
RecyclerView.RecyclerViewDataObserver#onItemRangeRemoved
AdapterHelper#onItemRangeRemoved
RecyclerView.RecyclerViewDataObserver#triggerUpdateProcessor
RecyclerView#requestLayout
RecyclerView 中的變更操作會(huì)被封裝為 UpdateOp 操作,這里刪除動(dòng)作被封裝為一個(gè) UpdateOp,添加到 mPendingUpdates 中等待處理。其處理時(shí)機(jī)為dispatchLayoutStep1 階段,根據(jù) 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;
}
調(diào)用鏈最終會(huì)走到 RecyclerView#requestLayout 方法,進(jìn)而觸發(fā) RecyclerView#onLayout 方法的調(diào)用。onLayout 做的事比較簡(jiǎn)單,直接調(diào)用 dispatchLayout 將布局事件分發(fā)下去,然后將 mFirstLayoutComplete 賦值為true,也就是只要執(zhí)行過一次 dispatchLayout 那么這個(gè)值就會(huì)為 true,這個(gè)值在后續(xù)分析中會(huì)用到。
// 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 根據(jù)狀態(tài)會(huì)調(diào)用 layout 的三個(gè)重要階段,保證 layout 三個(gè)重要階段至少被運(yùn)行一次。
// 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 中根據(jù) 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 由多個(gè)變量共同決定,從代碼中我們知道只有 mFirstLayoutComplete 為 true 之后才有可能開始運(yùn)行 ItemAnimator 和預(yù)測(cè)動(dòng)畫(決定了是否執(zhí)行 pre-layout),而 mFirstLayoutComplete 只有完成一次 onLayout 才會(huì)被賦值為 true,因此可以得知另一個(gè)信息:RecyclerView 不支持初始動(dòng)畫。 這里不去一一分析每一個(gè)變量的賦值時(shí)機(jī),從調(diào)試可知這里 mRunSimpleAnimations 和 mRunPredictiveAnimations 會(huì)被賦值為true(或者從現(xiàn)象反推,表項(xiàng)刪除是帶有動(dòng)畫的,因此這里也必須為 true 才能支持表項(xiàng)刪除的 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();
}
繼續(xù)查看 dispatchLayoutStep1 的其他代碼,當(dāng)可以執(zhí)行預(yù)測(cè)動(dòng)畫時(shí),會(huì)調(diào)用 LayoutManager 的 onLayoutChildren 方法。
private void dispatchLayoutStep1() {
...
processAdapterUpdatesAndSetAnimationFlags();
if (mState.mRunPredictiveAnimations) {
...
mLayout.onLayoutChildren(mRecycler, mState);
}
...
mState.mLayoutStep = State.STEP_LAYOUT;
}
查看 onLayoutChildren 代碼,其注釋信息如下:
布局 Adapter 的所有相關(guān)子視圖。LayoutManager 負(fù)責(zé) Item 動(dòng)畫的行為。默認(rèn)情況下,RecyclerView 有一個(gè)非空的 ItemAnimator,并且啟用簡(jiǎn)單的 item 動(dòng)畫。這意味著 Adapter 上的添加/刪除操作會(huì)伴隨有相關(guān)動(dòng)畫出現(xiàn)。如果 LayoutManager 的
supportsPredictiveItemAnimations()(默認(rèn)值)返回 false,并在onLayoutChildren(RecyclerView.Recycler, RecyclerView.State)運(yùn)行正常的布局操作, RecyclerView 也有足夠的信息以簡(jiǎn)單的方式運(yùn)行這些動(dòng)畫。 當(dāng) LayoutManager 想要擁有用戶體驗(yàn)更加友好的 ItemAnimator,那么 LayoutManager 應(yīng)該讓supportsPredictiveItemAnimations()返回 true 并向onLayoutChildren( RecyclerView.Recycler、RecyclerView.State)添加額外邏輯。支持預(yù)測(cè)動(dòng)畫意味著onLayoutChildren(RecyclerView.Recycler, RecyclerView.State)將被調(diào)用兩次; 一次作為“預(yù)”布局來確定 Item 在實(shí)際布局之前的位置,并再次進(jìn)行“實(shí)際”布局。在預(yù)布局階段,Item 將記住它們的預(yù)布局位置,以便能夠被布局正確。 此外,移除的項(xiàng)目將從 scrap 列表中返回,以幫助確定其他項(xiàng)目的正確放置。 這些刪除的項(xiàng)目不應(yīng)添加到子列表中,而應(yīng)用于幫助計(jì)算其他視圖的正確位置,包括以前不在屏幕上的視圖(稱為 APPEARING view),但可以確定其預(yù)布局屏幕外位置 給出有關(guān)預(yù)布局刪除視圖的額外信息。 第二次布局是真正的布局,其中僅使用未刪除的視圖。 此過程中唯一的附加要求是,如果supportsPredictiveItemAnimations()返回 true,請(qǐng)注意哪些視圖在布局之前存在于子列表中,哪些視圖在布局之后不存在(稱為 DISAPPEARING view),并定位/布局這些視圖,而不考慮 RecyclerView 的實(shí)際邊界。這使得動(dòng)畫系統(tǒng)能夠知道將這些消失的視圖動(dòng)畫化到的位置。 RecyclerView 的默認(rèn) LayoutManager 實(shí)現(xiàn)已經(jīng)處理了所有這些動(dòng)畫要求。 RecyclerView 的客戶端可以直接使用這些布局管理器之一,也可以查看它們的onLayoutChildren()實(shí)現(xiàn),以了解它們?nèi)绾谓忉?APPEARING 和 DISAPPEARING 視圖。
簡(jiǎn)單總結(jié)為:為了在 Item 項(xiàng)變更時(shí)能夠獲得更好的動(dòng)畫體驗(yàn),onLayoutChildren 會(huì)被調(diào)用兩次,一次用于 pre-layout(預(yù)布局),一次用于 post-layout(實(shí)際布局)。查看代碼調(diào)用時(shí)機(jī),onLayoutchildren 一次在 dispatchLayoutStep1 調(diào)用,一次在 dispatchLayoutStep2 調(diào)用。
onLayoutChildren 中會(huì)調(diào)用 fill 函數(shù)進(jìn)行布局填充。能否繼續(xù)填充由 layoutState.mInfinite 或 remainingSpace 和 layoutState.hasMore(state) 共同決定。 layoutState.mInfinite 表示無限填充,不適用于我們分析的case,因此關(guān)鍵在于 remainingSpace 值的變更。在 pre-layout 中會(huì)多布局一次,也就是說相比較于 post-layout,remainingSpace 在 pre-layout 少消費(fèi)了一次。 remainingSpace 的變更受三個(gè)變量控制,由于處在 pre-layout 階段,因此 state.isPreLayout 為 true,layoutState.mScrapList 此處還未被賦值,因此關(guān)注 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 過程被當(dāng)作參數(shù)傳入 layoutChunk ,因此我們需要關(guān)注下是否是這里導(dǎo)致了 layoutChunkResult 的成員變量改變了。 查看 layoutChunk 源碼,當(dāng) Item 項(xiàng)被標(biāo)記為 Remove 時(shí),會(huì)將 mIgnoreConsumed 變量置為 true,因此在 fill 過程會(huì)忽略被刪除的 Item 項(xiàng)的布局占用。
// 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 項(xiàng)被刪除,那么在什么時(shí)候 ViewHolder 被標(biāo)記為刪除狀態(tài)呢? 在 預(yù)布局 一節(jié)中我們知道調(diào)用最終走到 requestLayout 中,進(jìn)入觸發(fā) onLayout 方法。在布局過程中,關(guān)于 ViewHolder 信息更新的調(diào)用鏈路如下:
RecyclerView#dispatchLayoutStep1
RecyclerView#processAdapterUpdatesAndSetAnimationFlags
AdapterHelper#preProcess
AdapterHelper#applyRemove
AdapterHelper#postponeAndUpdateViewHolders
AdapterHelper#Callback#offsetPositionsForRemovingLaidOutOrNewView
RecyclerView#offsetPositionRecordsForRemove
ViewHolder#offsetPosition
ViewHolder#flagRemovedAndOffsetPosition
postponeAndUpdateViewHolders 會(huì)調(diào)用 Adapterhelper#Callback 接口的方法,這個(gè)接口在 AdapterHelper 實(shí)例化的時(shí)候進(jìn)行實(shí)現(xiàn)。最終會(huì)走到 offsetPositionRecordsForRemove 方法,這里會(huì)對(duì) ViewHolder 相關(guān)的 position 和 flag 變量進(jìn)行修改。
AdapterHelper#Callback 接口實(shí)現(xiàn)的地方:
// RecyclerView#initAdapterManager
void initAdapterManager() {
mAdapterHelper = new AdapterHelper(new AdapterHelper.Callback() {
...
}
}
回看 dispatchLayoutStep1 階段的 processAdapterUpdatesAndSetAnimationFlags 調(diào)用,在執(zhí)行 pre-layout 時(shí)會(huì)調(diào)用 mAdapterHelper.preProcess()。
// RecyclerView#processAdapterUpdatesAndSetAnimationFlags
private void processAdapterUpdatesAndSetAnimationFlags() {
...
if (predictiveItemAnimationsEnabled()) {
mAdapterHelper.preProcess();
} else {
mAdapterHelper.consumeUpdatesInOnePass();
}
...
}
preProcess 根據(jù) mPendingUpdates 中存儲(chǔ)的 UpdateOp 來決定執(zhí)行相關(guān)動(dòng)作。這與我們上述講到的 RecyclerView 中的變更操作會(huì)被封裝為 UpdateOp 操作,添加到 mPendingUpdates 中等待處理 相呼應(yīng),這里就是對(duì)應(yīng)的處理邏輯。(額外說一下 AdapterHelper 就是用來存儲(chǔ)和處理 UpdateOp 的相關(guān)工具類,根據(jù)保存的 UpdateOp 列表,計(jì)算 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();
}
根據(jù)上述的調(diào)用鏈關(guān)系,由于中間的調(diào)用過程都是一些比較簡(jiǎn)單的邏輯中轉(zhuǎn),我們直接查看 RecyclerView#offsetPositionRecordsForRemove 代碼的相關(guān)邏輯。 由于有 Item 項(xiàng)被刪除了,那么它后面的 Item 項(xiàng)的位置信息就需要被更新。這里分兩個(gè)分支邏輯進(jìn)行處理:
- 被刪除的 Item 項(xiàng)調(diào)用
flagRemovedAndOffsetPosition - 被刪除的 Item 項(xiàng)之后的 Item 項(xiàng)調(diào)用
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 的標(biāo)志位添加上了 FLAG_REMOVED 的刪除標(biāo)志。
// ViewHolder#flagRemovedAndOffsetPosition
void flagRemovedAndOffsetPosition(int mNewPosition, int offset, boolean applyToPreLayout) {
addFlags(ViewHolder.FLAG_REMOVED);
offsetPosition(offset, applyToPreLayout);
mPosition = mNewPosition;
}
此外對(duì)于 ViewHolder 的 position 相關(guān)信息也會(huì)被更新。
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;
}
}
預(yù)布局和后布局的差異
根據(jù)上述的分析我們知道,onLayoutChildren 會(huì)被調(diào)用兩次,一次用于 pre-layout,一次用于 post-layout。那么他們的區(qū)別在哪,為何會(huì)造成 pre-layout 的布局快照為 [item1, item2, item3], 而 post-layout 的布局快照為 [item1, item3] 呢?
回看 onLayoutChildren 源碼,在進(jìn)行 fill 填充時(shí),會(huì)先調(diào)用 detachAndScrapAttachedViews 將屏幕內(nèi)的 Item 項(xiàng)先進(jìn)行 detach 放置到 mAttachedScrap 中保存。此時(shí) mAttachedScrap 中保存著 item1 和 item2。
// LinearLayoutManager#onLayoutChildren
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
detachAndScrapAttachedViews(recycler);
...
fill(recycler, mLayoutState, state, false);
...
}
detachAndScrapAttachedViews 遍歷列表中的子 View,將他們都暫時(shí) 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);
}
}
在上述分析中,我們知道刪除一個(gè) Item 項(xiàng),其會(huì)被添加上 FLAG_REMOVED 的標(biāo)記位,因此對(duì)于 scrapView 的邏輯,其會(huì)走入第一個(gè)分支邏輯,將 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 的核心填充邏輯調(diào)用的 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 內(nèi)部執(zhí)行邏輯即為 RecyclerView 緩存復(fù)用的核心流程。調(diào)用鏈如下:
LayoutState#next
LinearLayoutManager#Recycler#getViewForPosition
LinearLayoutManager#Recycler#tryGetViewHolderForPositionByDeadline
RecyclerView#Recycler#getChangedScrapViewForPosition
RecyclerView#Recycler#getScrapOrHiddenOrCachedHolderForPosition
RecyclerView#RecycledViewPool#getRecycledView
RecyclerView#Adapter#createViewHolder
我們重點(diǎn)關(guān)注 getScrapOrHiddenOrCachedHolderForPosition, 因?yàn)樗藦?mAttachedScap 列表獲取 ViewHolder 的相關(guān)邏輯,而 mAttachedScap 我們上述講過,onLayoutChildren 過程 detach 的 ViewHolder 會(huì)存放在這里。
// 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;
}
對(duì)于是否能夠復(fù)用 mAttachedScap 中的 ViewHolder 取決于多個(gè)變量的共同作用。
-
holder.wasReturnedFromScrap(): 由于 ViewHolder 剛被加入到mAttachedScap中,因此其還沒有被標(biāo)記上FLAG_RETURNED_FROM_SCRAP標(biāo)志。另外,當(dāng)能夠復(fù)用時(shí),被標(biāo)記了FLAG_RETURNED_FROM_SCRAP標(biāo)志,其也會(huì)在RecyclerView#LayoutManager#addViewInt中被清除掉。 -
holder.isInvalid():Item 項(xiàng)在刪除過程沒有被標(biāo)記上FLAG_INVALID標(biāo)志。 -
mState.mInPreLayout || !holder.isRemoved()是否處于預(yù)布局或不被刪除
這里重點(diǎn)講一下 holder.getLayoutPosition() == position
// RecyclerView#ViewHolder#getLayoutPosition
public final int getLayoutPosition() {
return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition;
}
只有當(dāng)位置不變時(shí),才能被復(fù)用。因?yàn)閺?mAttachedScap 中復(fù)用的 ViewHolder 不會(huì)再進(jìn)行 bind 操作。 對(duì)于 getLayoutPosition 的取值由 mPreLayoutPosition 和 mPosition 共同作用。這兩個(gè)變量的取值,其可能會(huì)在多處被修改。在初始調(diào)用 notifyItemRangeRemoved 通知 RecyclerView 的 Item 項(xiàng)被刪除時(shí),在 offsetPositionRecordsForRemove 中 mPosition 會(huì)被修正為 Item 已經(jīng)被刪除的正確位置,這個(gè)我們?cè)?ViewHolder 信息更新 一節(jié)中有過闡述。
item3 由于不存在于 mAttachedScap 和各級(jí)緩存中,因此需要被創(chuàng)建。同時(shí)在 tryGetViewHolderForPositionByDeadline 中會(huì)將 mPosition 和 mPreLayoutPosition 進(jìn)行正確賦值。 isBound 表示 ViewHolder 是否完成布局,對(duì)于剛通過 createViewHolder 創(chuàng)建的 ViewHolder 其為 false, 因此會(huì)走入第二個(gè)分支邏輯,進(jìn)行數(shù)據(jù)綁定以及 position 數(shù)據(jù)的更新。 注意 offsetPosition 的相關(guān)計(jì)算,其調(diào)用 mAdapterHelper.findPositionOffset(position) 得到。作用與上述講到的 offsetPositionRecordsForRemove 作用類似。AdapterHelper 會(huì)根據(jù) 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);
}
...
}
此時(shí) pre-layout 的中 ViewHolder 的 position 信息如下:
| mPosition | mPreLayoutPosition | |
|---|---|---|
| item1 | 0 | 0 |
| item2 | 0 | 1 |
| item3 | 1 | 2 |
其得到的布局快照為 [item1, item2, item3]
在 dispatchLayoutStep1 源碼中,在函數(shù)體結(jié)尾處會(huì)調(diào)用 clearOldPositions 將 mPreLayoutPosition 重置。因此在 dispatchLayoutStep2 中進(jìn)行 post-layout 過程中調(diào)用 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;
}
此時(shí) post-layout 中 ViewHolder 的 position 信息如下:
| mPosition | mPreLayoutPosition | |
|---|---|---|
| item1 | 0 | -1 |
| item2 | 0 | -1 |
| item3 | 1 | -1 |
因此 getScrapOrHiddenOrCachedHolderForPosition 只有 item1 和 item3 能夠命中 mAttachedScap 的 ViewHolder,形成 [item1, item3] 的布局快照。
以上分析有很多細(xì)節(jié)點(diǎn)可能沒有很詳盡地闡述,因?yàn)?RecyclerView 當(dāng)中應(yīng)用的概念過于復(fù)雜。如果發(fā)散開來,形成的篇幅很大,且不易把握主題。 對(duì)于 預(yù)布局和后布局的差異 這一節(jié)中,整體脈絡(luò)涉及到 Item布局、 ViewHolder 緩存復(fù)用以及增刪變更帶來的 Position 等信息的變化,內(nèi)容多信息量大,不易快速掌握。建議寫一個(gè)精簡(jiǎn) demo,跟隨文章中闡述的調(diào)用鏈實(shí)際調(diào)試走一把,感受各個(gè)變量值變更的過程,加快理解。