RecyclerView 是如何实现炫酷的Item动画的?
本文作者
作者:唐子玄
链接:
https://juejin.im/post/6890288761783975950
本文由作者授权发布。
RecyclerView 表项动画是怎么实现的?RecyclerView 在做表项动画时会布局几次?pre-layout 是什么意思?我带着这么多疑问在茫茫源码中苦苦搜寻,本想直接给出答案,却发现过程也很值得回味,且听我慢慢道来。
最初听到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缓存机制(咋复用? https://juejin.im/post/6844903778303344647 )中讲述了怎么在源码中一步步找到 “填充表项” 的逻辑,这段逻辑正好就在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。我无法接受这个事实。。。
看源码就是这样,千辛万苦在一条道上走了很久,到头来却发现是个死胡同。
硬生生地看了一下午源码,也没有看到想要的结果,更致命的是硬看很容易钻牛角尖,有限的生命就耗费在这无穷的细节中。
想知道某个变量的值,最快的办法是断点调试,它也可以用到阅读源码上。写了一个简单的 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如何实现表项动画”下回在讲。
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!