Android RecyclerView的預(yù)布局詳解

概述

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、dispatchLayoutStep2dispatchLayoutStep3。這里先直接了當(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 中,而 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 由多個(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)試可知這里 mRunSimpleAnimationsmRunPredictiveAnimations 會(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)用 LayoutManageronLayoutChildren 方法。

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.mInfiniteremainingSpacelayoutState.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;
}

layoutChunkResultfill 過程被當(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 的取值由 mPreLayoutPositionmPosition 共同作用。這兩個(gè)變量的取值,其可能會(huì)在多處被修改。在初始調(diào)用 notifyItemRangeRemoved 通知 RecyclerView 的 Item 項(xiàng)被刪除時(shí),在 offsetPositionRecordsForRemovemPosition 會(huì)被修正為 Item 已經(jīng)被刪除的正確位置,這個(gè)我們?cè)?ViewHolder 信息更新 一節(jié)中有過闡述。

item3 由于不存在于 mAttachedScap 和各級(jí)緩存中,因此需要被創(chuàng)建。同時(shí)在 tryGetViewHolderForPositionByDeadline 中會(huì)將 mPositionmPreLayoutPosition 進(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)用 clearOldPositionsmPreLayoutPosition 重置。因此在 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è)變量值變更的過程,加快理解。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容