RecyclerView
应该是我们使用非常频繁的一个组件 我们也有必要学习分析一下RecyclerView#onLayout
工作流程 对我们日后分析优化RecyclerView
也会有帮助
美其名曰 知其然也知所以然
预布局是什么?
这个问题其实我们可以先阅读完下面的代码 然后再来回想这个问题 所以我先给出结论
首先预布局就是先布局一次(🤣凑个字数) 然后会形成一个快照(pre-layout
) 如item1234
然后再布局一次(post-layout
) 形成另外一张快照 item134
这样我们其实就知道了整个动画轨迹 就可以生成动画
上面看不懂 没关系 看下图👇
下面分别是三种状态
- 初始状态
- pre-layout(预布局阶段 生成一个快照 详细代码我们会在下面分析)
- post-layout(布局阶段 生成另一个快照)
然后我们就知道了item3的初始位置和终止位置 就可以生成动画并执行
源码调试手段
因为我们分析源码的过程中 经常有很多个分支 不知道如何是好😑 所以就需要断点调试手段了
首先我们启动一个AVD模拟器 然后AVD的版本号一定要和compileSdkVersion
版本号是一样的 不然会出现代码行数不一致导致调试不了问题
然后我们就可以愉快的找一个调试点开始调试啦
RecyclerView#onLayout流程
先从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()
方法 继续看一下
void dispatchLayout() {
if (mAdapter == null) {
Log.e(TAG, "No adapter attached; skipping layout");
// leave the state in START
return;
}
if (mLayout == null) {
Log.e(TAG, "No layout manager attached; skipping layout");
// leave the state in START
return;
}
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
// First 2 steps are done in onMeasure but looks like we have to run again due to
// changed size.
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
// always make sure we sync them (to ensure mode is exact)
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
我们发现会按照调用顺序
dispatchLayoutStep1()-> dispatchLayoutStep2()-> dispatchLayoutStep3()
我们挨个分析一下各方法功能 然后最后会做一个总结 朋友们也可以先点击✈️先看一眼结论
开始分析吧 任重而道远
private void dispatchLayoutStep1() {
......
//设置mInPreLayout 预布局标志位
mState.mInPreLayout = mState.mRunPredictiveAnimations;
mState.mLayoutStep = State.STEP_LAYOUT;
......
//调用LayoutManager.onLayoutChildren方法
mLayout.onLayoutChildren(mRecycler, mState);
......
}
我们看到这里设置了mInPreLayout
我们草率的先认定它是预布局的标志位 当然结果也是对的🤡
全局搜索一下mInPreLayout
的赋值 发现只有这里两处对mInPreLayout
赋值
一处是onMeasure()
时
if (mState.mRunPredictiveAnimations) {
mState.mInPreLayout = true;
}
那么mState.mRunPredictiveAnimations
一定是true 不然就没办法判断了
我们断点验证一下 发现
mState.mRunPredictiveAnimations
确实是true
LinearLayoutManager#onLayoutChildren()
我们这里以LinearLayoutManager来分析 其他几个LayoutManger其实也差不太多
根据注释 我们看到onLayoutChildren
主要做了四件事情
- 检查children 找到第一个需要变化(删除或者添加)的position
- 从底向上填充布局
- 从上向下填充布局
- 滚动布局来填充
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// layout algorithm:
// 1) by checking children and other variables, find an anchor coordinate and an anchor
// item position.
// 2) fill towards start, stacking from bottom
// 3) fill towards end, stacking from top
// 4) scroll to fulfill requirements like stack from bottom.
// create layout state
.......
//将所有view先回收
detachAndScrapAttachedViews(recycler);
final int firstLayoutDirection;
//从下往上
if (mAnchorInfo.mLayoutFromEnd) {
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForStart;
//填充布局
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
final int firstElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForEnd += mLayoutState.mAvailable;
}
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForEnd;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
// end could not consume all. add more items towards start
extraForStart = mLayoutState.mAvailable;
updateLayoutStateToFillStart(firstElement, startOffset);
mLayoutState.mExtraFillSpace = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
} else {
//从上往下 和上面一样的流程
......
}
......
}
下面就到了我们熟悉的fill()
方法 我们可能在很多文章都看到过这个方法 会依次调用layoutChunk
来对布局进行填充 这边我们需要重点关注一下预布局相关的不同点🤣 毕竟我们这篇文章的主题是分析预布局的啊
我们看一下fill()
方法 这边我们会重点关注一下remainSpace
我们之前一直在提的 预布局会形成一张快照 postLayout
也会形成另一张快照 fill()
方法中会对两次布局做不同的处理
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// max offset we should set is mFastScroll + available
final int start = layoutState.mAvailable;
......
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;//可用剩余布局空间
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
//只要remainingSpace>0 并且position<itemsCount
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
//layout item
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (layoutChunkResult.mFinished) {
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
//这里我们重点看一下 只有Post-Layut 或者mIgnoreConsumed为false remainingSpace才会减少 ①
//否则remainingSpace不会减少
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;
}
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
if (stopOnFocusable && layoutChunkResult.mFocusable) {
break;
}
}
return start - layoutState.mAvailable;
}
我们将上面的代码精简了一下 并且写了一些注释 我们发现
他是一个while
循环 直到remainingSpace < 0
或者 position > itemsCount
才会布局
我们上面提了好几次的 Pre-Layout
会形成一张1234
的快照 这张快照包含即将要删除的item2 以及删除item2之后填充的item4
这里就会产生一个疑问 他是如何计算Pre-Layout
的高度呢?
我们看到最开始代码int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
👏哈哈哈哈哈哈哈哈 结果出来了 就是layoutState.mExtraFillSpace
然而现实给我好好的上了一课🙅 调试发现上面的变量为0
答案:其实是上面注释①的地方 只有非Pre-Layout
或者mIgnoreConsumed
为false
remainingSpace
才会减少
我搜了一下mIgnoreConsumed
什么时候会为false
// Consume the available space if the view is not removed OR changed
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
只有当需要remvoe
或者需要需要change
才会ignore
所以 朋友们 👏结果很明显了啊 预布局的时候 remainingSpace
会忽略需要remove
的item 所以会形成一张1234
的快照
dispatchLayoutStep1()
终于分析完了 分析源码的过程总是枯燥又带着一点收获啊
dispatchLayoutStep1()
小总结
dispatchLayoutStep1()
也就是我们今天需要分析的Pre-Layout
的流程 生成了一张即将消失的内容+即将显示内容
的快照
然后我们接着分析一下dispatchLayoustStep2()
也就是Post-Layout
分析一下 Post-Layout💪
我们先看一眼dispatchLayoutStep2
的源码
private void dispatchLayoutStep2() {
// Step 2: Run layout
//将mInPreLayout置为false
//这里表示预布局阶段已经正式结束了
mState.mInPreLayout = false;
//又到了熟悉的onLayoutChildren()环节
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = false;
mPendingSavedState = null;
// onLayoutChildren may have caused client code to disable item animations; re-check
mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
mState.mLayoutStep = State.STEP_ANIMATIONS;
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
}
上面的源码比较简短 我们发现dispatchLayoutStep2()
方法中 首先mState.mInPreLayout = false;
表示Pre-Layout
已经结束了 然后进入了我们熟悉的onLayoutChildren
环节
我们上面刚刚分析过onLayoutChildren
因为state.isPreLayout()
一定为false 所以remainingSpace
一定会扣除所有空间
所以 结论出现了! dispatchLayoutStep2
会生成一张item134
的快照
等等! 我写到这里的时候 突然引发了我一个另一个疑惑 上面所说的并不能证明dispatchLayoutStep2
会生成一张item134
的快照?只能说明会生成一张item1234
?
咋回事啊? 然后我又去彻底翻了一遍源码 终于找到了根源所在 :-(
我们之前没有分析layoutChunk
方法 这个方法在fill()
中 会从recycler
中获取合适的ViewHolder
和View
然后添加到RecyclerView
中
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
//根据当前position 获取View
View view = layoutState.next(recycler);
......
}
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
//根据position获取View
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
这里会根据mCurrentPosition获取对应的ViewHolder
然后将View
添加到RecyclerView
但是我们还是没办法得到 为什么Post-Layout
会生成item134
呢?
Post-Layout
和Pre-Layout
还有两个地方有区别
public int getItemCount() {
return mInPreLayout
? (mPreviousLayoutItemCount - mDeletedInvisibleItemCountSincePreviousLayout)
: mItemCount;
}
我们发现预布局和后布局的ItemCount
是不一样的
另外就是我们在fill()
之前 会先回收所有的ViewHolder
想看scrapView
代码可以直接点击跳转看一下
scrapView
会将所有的ViewHolder
放入mAttachedScrap
中
而mAttachedScrap
中存在position
分别为0、0、1、2的四个表项 如下图(被Remove的ViewHolder 的 position
会置为0)
根据上面两个条件 我们会发现 在fill()
的过程中 只会生成item134
快照
终于分析出来为啥是item134
了 凌晨一点还在码字 脑子转的太累了😶
如果你还想问 那什么时候把删除的position置为0的? 我只能猜测是(不能保证)processAdapterUpdatesAndSetAnimationFlags();
然后就只能靠你自己分析了 我已经分析不动了😷
<span id = "dispatch方法">小总结</span>
1. onLayoutChildren()
- dispatchLayoutStep1()
预布局阶段 生成布局快照(remainingSpace忽略即将remove的item) - dispatchLayoutStep2()
post-layout
阶段 生成布局快照(不带即将remove的item) - dispatchLayoutStep3()
执行动画 因为上面两个步骤已经生成了开始状态和结束状态的快照 所以我们可以拿到动画的起始位置和终止位置
思考另外一个问题,RecyclerView删除动画是如何执行的?
我们上面已经分析了dispatchLayoutStep1()
和dispatchLayoutStep2
已经得到了两次快照所有item
的位置信息 接下来就可以开始执行动画了 执行动画的方法是dispatchLayoutStep3()
代码流程等我睡醒再分析一下吧😪 太困了
<span id = "scrap"> detachAndScrapAttachedViews ()</span>
我们看到onLayoutChildren
的时候 会先调用detachAndScrapAttachedViews
将所有View detach
然后再fill()
我们分析一下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);
}
}
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
//如果isInvalid() 就调用removeViewAt
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
//detachView
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
这里又不知道会走哪个分支了😠 不过 Debug
是我们的好伙伴啊🆙 我们断点发现 是走下面分支的 我们继续往下分析😂(分析源码就是这样 点到为止)
上面的detachViewAt(index);
主要是将view从children中双向删除 会调用到ViewGroup
中removeFromArray
不是我们这里的重点呐 我们先跳过 我们分析一下recycler.scrapView(view);
然后会将所有的View回收到mAttachedScrap中
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
//如果holder没有失效 没有被移除 则放入mAttachedScrap中
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 {
//只有当holder需要改变才会放入mChangedScrap中
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
总结
能坚持看完的都是勇士 分析一篇源码也真的是很累啊 不过还是受益良多 分析完预布局源码 现在对RecyclerView
的布局流程认知更加清晰了 对以后优化和分析ReyclerView
也会很有帮助 如果有对RecyclerView
复用机制感兴趣的朋友 可以阅读我另外一篇文章啊 链接贴在下面啦