概述

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

详解RecyclerView的预布局的更多相关文章

  1. Android 布局学习之——Layout(布局)详解二(常见布局和布局参数)

    [Android布局学习系列]   1.Android 布局学习之——Layout(布局)详解一   2.Android 布局学习之——Layout(布局)详解二(常见布局和布局参数)   3.And ...

  2. Xamarin+Prism开发详解五:页面布局基础知识

    说实在的研究Xamarin到现在,自己就没设计出一款好的UI,基本都在研究后台逻辑之类的!作为Xamarin爱好者,一些简单的页面布局知识还是必备的. 布局常见标签: StackLayout Abso ...

  3. 详解C/C++预处理器

     C/C++编译系统编译程序的过程为预处理.编译.链接.预处理器是在程序源文件被编译之前根据预处理指令对程序源文件进行处理的程序.预处理器指令以#号开头标识,末尾不包含分号.预处理命令不是C/C++语 ...

  4. QDockWidget嵌套布局详解-实现Visual Studio布局

    概述 许多工程软件,如Qt Creator,VS,matlab等,都是使用dock布局窗口,这样用户可以自定义界面,自由组合窗口. Qt的嵌套布局由QDockWidget完成,用Qt Creator拖 ...

  5. IOS详解TableView——对话聊天布局的实现

    上篇博客介绍了如何使用UITableView实现类似QQ的好友界面布局.这篇讲述如何利用自定义单元格来实现聊天界面的布局. 借助单元格实现聊天布局难度不大,主要要解决的问题有两个: 1.自己和其他人说 ...

  6. 详解CSS的Flex布局

    本文由云+社区发表 Flex是Flexible Box 的缩写,意为"弹性布局",是CSS3的一种布局模式.通过Flex布局,可以很优雅地解决很多CSS布局的问题.下面会分别介绍容 ...

  7. Android五大布局详解——TableLayout(表格布局)

    TableLayout 前面所学的LinearLayout和RelativeLayout两大布局已经完全适用于各种开发条件下,其他的布局仅供参考学习,毕竟知识就是力量,以后的开发过程中万一遇到也能游刃 ...

  8. Android五大布局详解——FrameLayout(帧布局)

    FrameLayout 这个布局相对前面两节介绍的布局就简单了很多,因此它的应用场景也就特别的少.这种布局没有方便的定位方式,所有的控件都会默认摆放在布局的左上角.新建UILayoutTestThre ...

  9. Android五大布局详解——RelativeLayout(相对布局)

    RelativeLayout 接着上一篇,本篇我将介绍RelativeLayout(相对布局)的一些知识点. RelativeLayout 这是一个非常常用的布局,相比于上节所学到的LinearLay ...

  10. Android五大布局详解——LinearLayout(线性布局)

    Android五大布局 本篇开始介绍Android的五大布局的知识,一个丰富的界面显示总是要有众多的控件来组成的,那么怎样才能让这些控件能够按你的想法进行摆放,从而自定义你所想要的用户界面呢?这就牵涉 ...

随机推荐

  1. 怎么把 session 中的实体类转换回来

    例子 : 如上比如user user1=new user(): user1.id=1: user1.name="张三": session["user1"]=us ...

  2. VSCode设置第三方字体

    最近需要写C,所以下了一个VSCode IDE嘛 当然是美观最重要了(不是 个人比较喜欢JetBrains家的字体 在IDEA中主要使用的是 JetBrains Mono 想着把VSCode的字体也设 ...

  3. ubuntu22.04安装vsftp遇到的问题

    问题 FileZilla连接文件服务器时出现"无法读取文件目录",随后出现"20秒后无活动,连接超时"."无法连接到服务器"文件目录无法读取 ...

  4. 二分查找非递归Algorithm(java)

    二分查找的适用条件 二分查找只适用于有序的数列中进行查找(比如数字和字母等),将数列排序后再进行查找 二分查找的运行时间为对数时间O(LONG2 N) 二分查找非递归的实现 /** * @param ...

  5. ChatGPT 1.0.0安卓分析,仅限国内分享

    ChatGPT 1.0.0安卓分析,仅限国内分享 博客园首发,本文将对ChatGpt Android版本1.0.0 APK进行静态解包分析和抓包分析,从ChatGpt Android APK功能的设计 ...

  6. 解决pyinstaller生成的exe文件,在部分电脑无法运行的问题

    下载vc_redist.x64.exe并在不能运行的电脑上运行即可. vc_redist.x64.exe的作用:一款Visual C++的运行库,里面包含了一些Visual C++的库函数.Visua ...

  7. PLE-实践小结-2308-cnblogs

    某场景介绍 前状:三模型,权重融合 解决问题:融合目标行为,充分利用样本信息,节省资源开销. 当前效果 主场景人均真实曝光+0.26%,不显著:子场景人均真实曝光+0.35%,不显著 千曝互动+2.6 ...

  8. 华为云GaussDB(for Influx)单机版上线,企业降本增效利器来了

    本文分享自华为云社区<华为云GaussDB(for Influx)单机版上线,企业降本增效利器来了>,作者:GaussDB 数据库 . 1.背景 华为云GaussDB(for Influx ...

  9. 基于Supabase开发公众号接口

    在<开源BaaS平台Supabase介绍>一文中我们对什么是BaaS以及一个优秀的BaaS平台--Supabase做了一些介绍.在这之后,出于探究的目的,我利用一些空闲时间基于Micros ...

  10. ATtiny88初体验(二):呼吸灯

    ATtiny88初体验(二):呼吸灯 前面的"点灯"实验实现了间隔点亮/熄灭LED,但是间隔时间和亮度都没法控制,为了解决这个问题,可以使用ATtiny88的定时器模块. ATti ...