RecyclerView 表项动画是怎么实现的?RecyclerView 在做表项动画时会布局几次?pre-layout 是什么意思?我带着这么多疑问在茫茫源码中苦苦搜寻,本想直接给出答案,却发现过程也很值得回味,且听我慢慢道来。
这是 RecyclerView 动画原理的第一篇,系列文章目录如下:
引子
最初听到pre-layout
这个概念是从一道面试题:RecyclerView 为什么要预布局?
下面这个场景就要用到预布局:
列表中有两个表项(1、2),删除 2,此时 3 会从屏幕底部平滑地移入并占据原来 2 的位置。
这是怎么做到的?RecyclerView
如何知道表项 3 的动画轨迹?虽然动画的终点已经有了(表项 2 的顶部),那起点呢?LayoutManager
只加载所有可见表项,在删除表项 2 之前,表项 3 处于不可见状态,它并不会被 layout。
对于这种情况RecyclerView
的策略是“执行两次 layout”:为动画前的表项先执行一次pre-layout
,将不可见的表项 3 也加载到布局中,形成一张布局快照(1、2、3)。再为动画后的表项执行一次post-layout
,同样形成一张布局快照(1、3)。比对两张快照中表项 3 的位置,就知道它该如何做动画了。
具体是怎么实现的?去源码里搜搜答案~
预布局生命周期
一开始,也不知道该从哪看起。但既然是预布局,肯定和布局有关系,就从RecyclerView.onLayout()
开始把:
看原码就是这样,有时候它更像是漫无目的的逛街,而不是明确指向的淘宝搜索。
public class RecyclerView { @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; } }
RecyclerView.onLayout()
很短,一眼就可以找到其中的关键dispatchLayout()
:
public class RecyclerView { void dispatchLayout() { mState.mIsMeasuring = false; if (mState.mLayoutStep == State.STEP_START) { // 分发布局1 dispatchLayoutStep1(); mLayout.setExactMeasureSpecsFrom(this); // 分发布局2 dispatchLayoutStep2(); } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() || mLayout.getHeight() != getHeight()) { mLayout.setExactMeasureSpecsFrom(this); dispatchLayoutStep2(); } else { mLayout.setExactMeasureSpecsFrom(this); } // 分发布局3 dispatchLayoutStep3(); } }
布局分了三个步骤,从第一步骤开始看:
public class RecyclerView { private void dispatchLayoutStep1() { ... mState.mInPreLayout = mState.mRunPredictiveAnimations; ... } public static class State { boolean mInPreLayout = false; ... } }
在分发布局第一步中发现了一个布尔变量mInPreLayout
,字面意思是“是否在 pre-layout 过程中”。
找到一点和pre-layout
沾边的信息,映入脑壳的问题是 “ mInPreLayout 什么时候被置为 true,什么时候又被置为 false?”,回答这个问题就能知道pre-layout
的生命周期了。
全局搜索mInPreLayout
被赋值的地方,除了mState.mInPreLayout = mState.mRunPredictiveAnimations;
其余都被置为 false。想必mState.mRunPredictiveAnimations
一定为 true!怎么验证?看看它在哪里被赋值:
public class RecyclerView { private void processAdapterUpdatesAndSetAnimationFlags() { ... mState.mRunSimpleAnimations = mFirstLayoutComplete && mItemAnimator != null && (mDataSetHasChangedAfterLayout || animationTypeSupported || mLayout.mRequestedSimpleAnimations) && (!mDataSetHasChangedAfterLayout || mAdapter.hasStableIds()); mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations && animationTypeSupported && !mDataSetHasChangedAfterLayout && predictiveItemAnimationsEnabled(); } }
mRunPredictiveAnimations
的值由另外 N 个布尔变量共同决定,难道我得挨个搜索其他变量才能确定它的值吗?(其实有一个更简单的方法可以验证,下面会提到)
就此打住,mRunPredictiveAnimations
的值一定为 true,否则mInPreLayout
就永远为 false 了。
看原码就是这样,源码无涯,回头是岸,点到为止就好。
继续走查dispatchLayoutStep1()
剩余的代码:
public class RecyclerView { private void dispatchLayoutStep1() { ... mState.mInPreLayout = mState.mRunPredictiveAnimations; ... if (mState.mRunPredictiveAnimations) { ... mLayout.onLayoutChildren(mRecycler, mState); ... } ... } }
发现了一个很关键的方法LayoutManager.onLayoutChildren()
,它有很长的注释,大意是“该方法用于布局 Adapter 中所有的表项。若支持表项动画,则 onLayoutChildren() 会被调用 2 次,第一次称为 pre-layout,它是真正布局表项之前的一次预布局。”
搜索LayoutManager.onLayoutChildren()
被调用的地方,只有两处,一次在RecyclerView.dispatchLayoutStep1()
中,另一次在RecyclerView.dispatchLayoutStep2()
:
public class RecyclerView { private void dispatchLayoutStep2() { ... mState.mInPreLayout = false;// pre-layout 结束 mLayout.onLayoutChildren(mRecycler, mState); // 开始正真的布局 ... } }
布局的第二步中,调用onLayoutChildren()
前,把mInPreLayout
置为了 false,pre-layout
就此结束。
而且mState
作为参数被传入onLayoutChildren()
,在onLayoutChildren()
中一定会读取mInPreLayout
。
看到这里,结合注释和代码走查,可以下一些结论:
RecyclerView
为了实现表项动画,进行了 2 次布局,第一次预布局,第二次正真的布局,在源码上表现为LayoutManager.onLayoutChildren()
被调用 2 次
mState.mInPreLayout
的值标记了预布局的生命周期。预布局的过程始于RecyclerView.dispatchLayoutStep1()
,终于RecyclerView.dispatchLayoutStep2()
。两次调用LayoutManager.onLayoutChildren()
会因为这个标记位的不同而执行不同的逻辑分支。
预布局填充额外表项
知道了预布局的起点和终点,就为走查代码缩小了范围。只需要定位在LinearLayoutManager.onLayoutChildren()
中,就可以了解预布局做了些什么。
预布局一定做了很多事情,但现在最关心的是“预布局过程中,如何将额外的不可见表项填充进来?”
在RecyclerView缓存机制(咋复用?)中讲述了怎么在源码中一步步找到 “填充表项” 的逻辑,这段逻辑正好就在onLayoutChildren()
中,引用如下:
public class LinearLayoutManager { // 布局表项 public void onLayoutChildren() { // 填充表项 fill() { while(列表有剩余空间){ // 填充单个表项 layoutChunk(){ // 让表项成为子视图 addView(view) } } } } }
RecyclerView
将布局表项的任务委托给LinearLayoutManager
。
LinearLayoutManager
布局表项时,在fill()
方法中循环不断地调用layoutChunk()
逐个将表项填入,直到列表没有空间。
对于填充表项,fill()
和layoutChunk()
是两个关键方法,添加额外表项的逻辑肯定藏在其中:
public class LinearLayoutManager { // 根据剩余空间填充表项 int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) { ... // 计算剩余空间 = 可用空间 + 额外空间 int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace; // 当剩余空间 > 0 时,继续填充更多表项 while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { ... layoutChunk() ... } } }
LinearLayoutManager
在循环填充表项前会计算剩余空间,计算公式中的mExtraFillSpace
引起了我的注意,它和我关心的问题“额外表项”很匹配,心想 “在 pre-layout 过程中可能是mExtraFillSpace
增大,放宽了循环条件,使得额外表项被填充。” 于是乎,我开始搜索它被赋值的地方,结果显示有 11 处(有点多,好慌):
仔细一瞅,大部分的赋值都发生在onLayoutChildren()
中:
if (mAnchorInfo.mLayoutFromEnd) {// 从尾部开始布局 mLayoutState.mExtraFillSpace = extraForStart; } else {// 从头部开始布局 mLayoutState.mExtraFillSpace = extraForEnd; }
而且它们分别处于不同的方向分支中,即对于一种方向的列表只有一个赋值语句被执行,随便找了一个mLayoutState.mExtraFillSpace = extraForEnd;
,继续搜索extraForEnd
被赋值的地方:
看源码就是这样,道路千万条,抽丝剥茧选一条。
public class LinearLayoutManager { private int[] mReusableIntPair = new int[2]; public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { ... mReusableIntPair[0] = 0; mReusableIntPair[1] = 0; calculateExtraLayoutSpace(state, mReusableIntPair); // 计算值 int extraForEnd = Math.max(0, mReusableIntPair[1]) // 赋值 } }
extraForEnd
的值和mReusableIntPair[1]
有关,而它在calculateExtraLayoutSpace()
中被计算,继续跳转:
看源码就是这样,想知道一个变量的值,可能得先知道其他 N 个变量的值。
public class LinearLayoutManager { // 计算额外空间 protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state,@NonNull int[] extraLayoutSpace) { int extraLayoutSpaceStart = 0; int extraLayoutSpaceEnd = 0; int extraScrollSpace = getExtraLayoutSpace(state);// 计算值 if (mLayoutState.mLayoutDirection == LayoutState.LAYOUT_START) { extraLayoutSpaceStart = extraScrollSpace; } else { extraLayoutSpaceEnd = extraScrollSpace; } extraLayoutSpace[0] = extraLayoutSpaceStart; extraLayoutSpace[1] = extraLayoutSpaceEnd;// 赋值 } }
calculateExtraLayoutSpace()
这个方法名让我更加坚信这条路没错(额外表项对应着额外空间)。
在这个方法中又调用了getExtraLayoutSpace()
并将结果赋值给extraLayoutSpace[1]
,继续跳:
看源码就是这样,不停地跳来跳去,有时候跳远了都忘了为啥而跳。
public class LinearLayoutManager { protected int getExtraLayoutSpace(RecyclerView.State state) { if (state.hasTargetScrollPosition()) { return mOrientationHelper.getTotalSpace(); } else { return 0; } } }
方法要么返回 0 要么返回mOrientationHelper.getTotalSpace()
,我更愿意相信后者,因为只有返回非0值才能证实猜想。为了验证,我还得跳一次:
public class RecyclerView { public static class State { public boolean hasTargetScrollPosition() { return mTargetPosition != RecyclerView.NO_POSITION; } } }
看到这,我陷入了迷茫,因为删除表项操作并不会发生列表滚动,即hasTargetScrollPosition()
应该返回 false,也就说返回额外空间的方法getExtraLayoutSpace()
应该返回0。我无法接受这个事实。。。
看源码就是这样,千辛万苦在一条道上走了很久,到头来却发现是个死胡同。
难道列表发生滚动了?
怎么证明滚动了?
继续搜索 mTargetPosition 被赋值的地方?
不。。。我已经跳不动了。。
硬生生地看了一下午源码,也没有看到想要的结果,更致命的是硬看很容易钻牛角尖,有限的生命就耗费在这无穷的细节中。
想知道某个变量的值,最快的办法是断点调试,它也可以用到阅读源码上。写了一个简单的 Demo 模拟删除表项的场景,将断点打在计算剩余空间那一行:
public class LinearLayoutManager { int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) { ... // 计算剩余空间 = 现有空间 + 额外空间(断点) int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace; while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { ... layoutChunk() ... } } }
断点告诉我layoutState.mExtraFillSpace
的确为0!
那layoutState.mAvailable
的值是否在pre-layout
过程中变大?断点告诉我没有!
循环条件没有放宽!那额外的表项是如何被填充的?
我将断点打在了循环条件while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state))
上,惊喜地发现了一个新的线索:在正常布局表项时,当第二个表项被填充后remainingSpace
就等于0了,但同样的情况在 pre-layout 阶段,remainingSpace
就不为0,这导致循环可以多走一次,即可以将表项 3 填充进来。
每次循环填充表项后remainingSpace
的值应该变小,难道填充被删除的表项时跳过了这个步骤?
又到了硬看源码发挥作用的时刻:
public class LinearLayoutManager { 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(); // 填充单个表项(将layoutChunkResult传入) layoutChunk(recycler, state, layoutState, layoutChunkResult); ... if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null|| !state.isPreLayout()) { // 在剩余空间中扣除刚填充表项消耗的空间 remainingSpace -= layoutChunkResult.mConsumed; } } } }
循环中唯一一处扣除剩余空间的代码被一个条件表达式包裹着,表达式中有三个条件做或运算,其中一个条件!state.isPreLayout()
对于非pre-layout
阶段来说肯定为 true,即无论其他条件如何,非pre-layout
阶段一定会扣除所有表项消耗的空间,而对于pre-layout
来说,填充某些表项时,可能会跳过扣除。哪些表项会跳过?
条件表达式中有一个变量 layoutChunkResult.mIgnoreConsumed,字面意思是忽略这次消耗,而且layoutChunkResult被作为参数传入layoutChunk()
:
public class LinearLayoutManager { void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) { // 获取下一个该被填充的表项视图 View view = layoutState.next(recycler); ... // 获取表项布局参数 RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); // 如果表项被移除 则 mIgnoreConsumed 置为 true if (params.isItemRemoved() || params.isItemChanged()) { result.mIgnoreConsumed = true; } ... } }
看到这里感觉八九不离十了,用断点调试验证了,的确和猜想的一样:
在预布局阶段,循环填充表项时,若遇到被移除的表项,则会忽略它占用的空间,多余空间被用来加载额外的表项,这些表项在屏幕之外,本来不会被加载。
走查到这里,虽然只回答了两个问题,一是预布局的生命周期,二是预布局如何填充额外表项,但篇幅已经有点长了,关于“RecyclerView预布局的其他分析”及“RecyclerView如何实现表项动画”下回在讲。
推荐阅读
RecyclerView 系列文章目录如下: