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 系列文章目录如下:
- RecyclerView 缓存机制 | 如何复用表项?
- RecyclerView 缓存机制 | 回收些什么?
- RecyclerView 缓存机制 | 回收到哪去?
- RecyclerView缓存机制 | scrap view 的生命周期
- 读源码长知识 | 更好的RecyclerView点击监听器
- 代理模式应用 | 每当为 RecyclerView 新增类型时就很抓狂
- 更好的 RecyclerView 表项子控件点击监听器
- 更高效地刷新 RecyclerView | DiffUtil二次封装
- 换一个思路,超简单的RecyclerView预加载
- RecyclerView 动画原理 | 换个姿势看源码(pre-layout)
- RecyclerView 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系
- RecyclerView 动画原理 | 如何存储并应用动画属性值?
- RecyclerView 面试题 | 列表滚动时,表项是如何被填充或回收的?
- RecyclerView 面试题 | 哪些情况下表项会被回收到缓存池?
- RecyclerView 性能优化 | 把加载表项耗时减半 (一)
- RecyclerView 性能优化 | 把加载表项耗时减半 (二)
- RecyclerView 性能优化 | 把加载表项耗时减半 (三)
- RecyclerView 的滚动是怎么实现的?(一)| 解锁阅读源码新姿势
- RecyclerView 的滚动时怎么实现的?(二)| Fling
- RecyclerView 刷新列表数据的 notifyDataSetChanged() 为什么是昂贵的?