详解RecyclerView的预布局

概述

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,跟随文章中阐述的调用链实际调试走一把,感受各个变量值变更的过程,加快理解。

posted @ 2023-10-07 00:42  zxzhang  阅读(336)  评论(0编辑  收藏  举报