详解RecyclerView的预布局
2023/10/7 5:02:56
本文主要是介绍详解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 阶段,分别为: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,跟随文章中阐述的调用链实际调试走一把,感受各个变量值变更的过程,加快理解。
这篇关于详解RecyclerView的预布局的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-22怎么实现ansible playbook 备份代码中命名包含时间戳功能?-icode9专业技术文章分享
- 2024-11-22ansible 的archive 参数是什么意思?-icode9专业技术文章分享
- 2024-11-22ansible 中怎么只用archive 排除某个目录?-icode9专业技术文章分享
- 2024-11-22exclude_path参数是什么作用?-icode9专业技术文章分享
- 2024-11-22微信开放平台第三方平台什么时候调用数据预拉取和数据周期性更新接口?-icode9专业技术文章分享
- 2024-11-22uniapp 实现聊天消息会话的列表功能怎么实现?-icode9专业技术文章分享
- 2024-11-22在Mac系统上将图片中的文字提取出来有哪些方法?-icode9专业技术文章分享
- 2024-11-22excel 表格中怎么固定一行显示不滚动?-icode9专业技术文章分享
- 2024-11-22怎么将 -rwxr-xr-x 修改为 drwxr-xr-x?-icode9专业技术文章分享
- 2024-11-22在Excel中怎么将小数向上取整到最接近的整数?-icode9专业技术文章分享