前言
「温馨提示:文章有点长,建议关注微信公众号“字节小站”收藏阅读」
本文主要通过以下几个方面来讲解RecyclerView的布局和动画原理:
-
布局放置:RecyclerView#dispatchLayout() -
预布局阶段:RecyclerView#dispatchLayoutStep1() -
布局阶段:RecyclerView#dispatchLayoutStep2() -
开启动画阶段:RecyclerView#dispatchLayoutStep3()
背景知识
RecyclerView的Adapter有几个notify相关的方法:
-
notifyDataSetChanged() -
notifyItemChanged(int) -
notifyItemInserted(int) -
notifyItemRemoved(int) -
notifyItemRangeChanged(int, int) -
notifyItemRangeInserted(int, int) -
notifyItemRangeRemoved(int, int) -
notifyItemMoved(int, int)
notifyDataSetChanged()与其他方法的区别:
-
会导致整个列表刷新,其它几个方法则不会; -
不会触发RecyclerView的动画机制,其它几个方法则会触发各种不同类型的动画。
1. 布局放置
1.1 核心方法
RecyclerView#dispatchLayout()
1.2 作用
-
将View放置到合适的位置 -
记录布局阶段View的信息 -
处理动画
RecyclerView的布局我们可以分成三个阶段,也可以精细分成五个阶段。
1.2.1 三个阶段
1.2.1.1 预布局阶段
当需要做动画时,预布局阶段才会工作,否则没有实际意义,它对应dispatchLayoutStep1方法。动画有开始状态和结束状态,预布局完成后的RecyclerView是动画的开始状态。
1.2.1.2 布局阶段
无论是否需要做动画,布局阶段都会工作,它对应dispatchLayoutStep2方法。布局完成后的状态是用户最终看到的状态,也是动画的结束状态。
1.2.1.3 布局后阶段
布局完成后,需要执行动画操作,它对应的是dispatchLayoutStep3方法。当动画完成后,还会进行View回收操作。
1.2.2 五个阶段
1.2.2.1 预布局前
在dispatchLayoutStep1方法调用onLayoutChildren方法之前。它会保存当前RecyclerView上所有子View的信息到ViewInfoStore中,FLAG增加FLAG_PRE。表示View在预布局前就显示在RecyclerView上。
1.2.2.2 预布局中
在dispatchLayoutStep1方法调用onLayoutChildren方法时。它会根据算法,重新布置RecyclerView的子View,该阶段可能会添加新的子View。该阶段能够确定哪些View最终是不会展示给用户看的,FLAG增加FLAG_DISAPPEARED(例如:removed的View)。
1.2.2.3 预布局后
在dispatchLayoutStep1方法调用onLayoutChildren方法之后,将预布局完成后的子View与预布局前的子View对比,将新增的View的FLAG增加FLAG_APPEAR(调用notifyItemRemoved后,新填充的View)。
1.2.2.4 布局中
在dispatchLayoutStep2方法调用onLayoutChildren方法时。该阶段会把被挤出屏幕的View的FLAG增加FLAG_DISAPPEARED。
1.2.2.5 布局后
在dispatchLayoutStep3方法中。会将最终的子View的FLAG增加FLAG_POST。
1.2.3 动画类型
1.2.3.1 PERSISTENT
预布局前和布局后都存在的View所做的动画,位置有可能发生变化了,也有可能没有发生变化。
1.2.3.2 REMOVED
在布局前对用户可见,布局后不可见,而且数据已经从数据源中删除掉了。
1.2.3.3 ADDED
新增数据到数据源中,并且在布局后对用户可见。
1.2.3.4 DISAPPEARING
数据一直都存在于数据源中,但是布局后从可见变成不可见状态(例如因为其它View插入操作,导致被挤出屏幕外了)。
1.2.3.5 APPEARING
数据一直都存在于数据源中,但是布局后从不可见变成可见状态(例如因为其它View被删除,导致补位到屏幕内了)。
1.3 源码解析
1.3.1 RecyclerView#dispatchLayout()
-
dispatchLayoutStep1()执行预布局,记录ViewHolder位置信息; -
dispatchLayoutStep2()执行布局,用户最终看到的效果; -
dispatchLayoutStep3()执行动画操作。
2. 预布局阶段
2.1 核心方法
-
RecyclerView#dispatchLayoutStep1()
-
RecyclerView#processAdapterUpdatesAndSetAnimationFlags()
-
LinearLayoutManager#onLayoutChildren()
-
LinearLayoutManager#updateAnchorInfoForLayout()
2.2 作用
-
处理Adapter变化 -
决定该执行哪种类型动画 -
保存当前RecyclerView上的子View的信息 -
如果需要执行动画,进行预布局
2.3 源码解析
2.3.1 RecyclerView#dispatchLayoutStep1()
-
判断是否需要开启动画功能 -
如果开启动画,将当前屏幕上的Item相关信息保存起来供后续动画使用 -
如果开启动画,调用mLayout.onLayoutChildren方法预布局 -
预布局后,与第二步保存的信息对比,将新出现的Item信息保存到Appeared中
2.3.2 RecyclerView#processAdapterUpdatesAndSetAnimationFlags()
作用:判断是否需要开启动画
2.3.3 LinearLayoutManager#onLayoutChildren()
以垂直方向的RecyclerView为例子,我们填充RecyclerView的方向有两种,从上往下填充和从下往上填充。开始填充的位置不是固定的,可以从RecyclerView的任意位置处开始填充。
-
寻找填充的锚点(最终调用findReferenceChild方法); -
移除屏幕上的Views(最终调用detachAndScrapAttachedViews方法); -
从锚点处从上往下填充(调用fill和layoutChunk方法); -
从锚点处从下往上填充(调用fill和layoutChunk方法); -
如果还有多余的空间,继续填充(调用fill和layoutChunk方法); -
布局完成后有可能产生GAP,需要修复GAP; -
dispatchLayoutStep2阶段调用layoutForPredictiveAnimation将scrapList中多余的ViewHolder填充(调用fill和layoutChunk方法)。
2.3.3.1 寻找填充的锚点
-
优先返回全部在屏幕内,未标记removed的View; -
次优先级返回不可见的View; -
最低优先级返回删掉的view。
2.3.3.2 移除屏幕上的Views
-
调用notifyItemChanged(position),position对应的ViewHolder会放入到mChangedScrap缓存中; -
否则会放入到mAttachedScrap缓存中
2.3.3.3 ~ 2.3.3.5 填充
调用LinearLayoutManager#fill()和LinearLayoutManager#layoutChunk()
-
从缓存中获取View或者创建View -
如果是step1预布局阶段,调用addView(),将标记为removed的view放入到DISAPPEARED动画列表中 -
如果是step2布局阶段,调用addDisappearingView(),将被挤出屏幕的view放入到DISAPPEARED动画列表中 -
如果是removed的或者changed,不会记录消耗的填充量
2.3.3.6 修复GAP
通过mOrientationHelper.offsetChildren(gap)直接填补GAP
2.3.3.7 layoutForPredictiveAnimation
为了做动画,增加额外的Item
-
不需要做动画,或者是预布局直接返回 -
从mAttachedScrap中遍历到非removed的ViewHolder,但是返回的结果可能包含removed ViewHolder -
如果遍历找到了非Removed ViewHolder,填充View
3. 布局阶段
3.1 核心方法
-
RecyclerView#dispatchLayoutStep2() -
LinearLayoutManager#layoutChunk() -
LinearLayoutManager#addDisappearingView() -
ViewInfoStore#addToDisappearedInLayout()
3.2 作用
-
根据数据源中的数据进行布局,真正展示给用户看的最终界面 -
如果开启动画,将被挤出屏幕的View的保存到消失动画列表中
3.3 源码解析
3.3.1 RecyclerView#dispatchLayoutStep2()
-
将预布局模式改为false -
布局填充View
3.3.2 LinearLayoutManager#layoutChunk()
布局阶段将被挤出屏幕的View放入到DISAPPEARED动画列表中
3.3.3 LinearLayoutManager#addDisappearingView()
把Removed的View或被挤出屏幕的View添加到Disappearing动画列表
3.3.4 ViewInfoStore#addToDisappearedInLayout()
加入到Disappeared动画列表
4. 触发动画阶段
4.1 核心方法
-
RecyclerView#dispatchLayoutStep3() -
ViewInfoStore#addToPostLayout() -
ViewInfoStore#process() -
ItemAnimator#animateAppearance()
4.2 作用
-
清理工作 -
保存布局后的view的信息 -
触发动画 -
动画执行完回收工作
4.3 源码解析
4.3.1 RecyclerView#dispatchLayoutStep3()
-
将当前屏幕上的View信息记录到postLayout动画列表中 -
执行动画 -
清理操作 -
布局完成回调
4.3.2 ViewInfoStore#addToPostLayout()
View信息记录到postLayout动画列表中
4.3.3 ViewInfoStore#process()
作用:执行动画
工作流程,按优先级执行
-
调用unuse() 将view回收掉 -
执行消失动画
-
2.1 预布局中不可见调用unuse() -
2.2 调用processDisappeared()
-
调用processPersistent()执行move或者change动画 -
执行remove动画 -
执行insert动画
4.3.4 ViewInfoStore$InfoRecord
作用:定义动画类型
-
FLAG_DISAPPEARED:消失动画,包含move和remove动画 -
FLAG_APPEAR:出现动画,包含move和insert动画 -
FLAG_PRE:预布局前已经显示在RecyclerView上 -
FLAG_POST:布局后显示在RecyclerView上 -
FLAG_APPEAR_AND_DISAPPEAR:先做出现动画,再做消失动画,无意义 -
FLAG_PRE_AND_POST:预布局前和布局后一直显示在RecyclerView上 -
FLAG_APPEAR_PRE_AND_POST:在FLAG_PRE_AND_POST基础上做出现动画
4.3.5 ViewInfoStore$ProccessCallback
作用:定义四种处理动画的接口
-
processDisappeared 处理消失动画 -
processAppeared 处理出现动画 -
processPersistent 处理一直存在动画,包含move和change动画 -
unused 不需要处理动画,执行回收
4.3.6 接口实现
4.3.7 ProccessCallback#processAppeared
兵分两路
-
调用ItemAnimator#animateAppearance() -
调用RecyclerView#postAnimationRunner()
4.3.8 一路兵:ItemAnimator#animateAppearance()
4.3.8.1 SimpleItemAnimator#animateAppearance
-
该方法返回true表示需要做动画 -
否则不需要做动画 -
如果预布局前View已经存在而且位置发生改变,处理MOVE动画 -
否则,处理ADD动画
4.3.8.2 DefaultItemAnimator.animateMove
-
该方法并没有真正执行动画 -
将MoveInfo保存到mPendingMoves中,以便RecyclerView#postAnimationRunner()使用 -
判断是否有必要执行MOVE动画 -
回到preLayout的位置
4.3.8.3 DefaultItemAnimator.animateAdd
先调用setAlpha(0),以便做淡入动画
4.3.9 二路兵:RecyclerView#postAnimationRunner()
4.3.9.1 RecyclerView#postAnimationRunner
最终调用到ItemAnimator.runPendingAnimations
4.3.9.2 DefaultItemAnimator.runPendingAnimations
-
首先执行Remove动画 -
然后同时执行Move和Change动画 -
最后执行Add动画
动画的总时长为removeDuration + Math.max(moveDuration, changeDuration) + addDuration
4.3.10 RecyclerView$ItemAnimatorRestoreListener
作用:动画结束后执行回收操作
-
动画执行完毕,removeAnimatingView -
调用Recycler.recycleViewHolderInternal执行回收操作
5. 场景篇
5.1 notifyItemRemoved场景
5.1.1 场景描述
-
调用notifyItemRemoved() -
Adapter数据有100条,屏幕上有Item1~Item6 6个View,删除Item1和Item2
5.1.2 布局过程
-
将Item1 Item2对应的ViewHolder设置为REMOVE状态 -
将所有的Item对应的ViewHolder的mPreLayoutPosition字段赋值为当前的position
5.1.2.1 dispatchLayoutStep1阶段
-
寻找填充的锚点,寻找锚点的逻辑是,从上往下,找到第一个非remove状态的Item。在本Case中,找到Item3
-
移除屏幕上的Views,将它们的ViewHolder放入到Recycler的mAttachedScrap缓存中,这个缓存的好处是如果position对应上了,无需重新绑定,直接拿来用。
-
从锚点Item3处往下填充,mAttachedScrap只剩下ViewHolder2和ViewHolder1
-
从锚点Item3处往上填充Item2 Item1,因为Item2,Imte1已经被remove掉了,它消耗的空间不会被记录,那么到步骤5的时候还可以填充
-
还有多余的空间,继续填充,把Item7、Item8填充到屏幕中
-
因为当前是预布局,直接返回
5.1.2.2 dispatchLayoutStep2阶段
-
寻找填充的锚点,寻找锚点的逻辑是,从上往下,找到第一个非remove状态的Item,找到Item3
-
移除屏幕上的Views,将它们的ViewHolder放入到Recycler的mAttachedScrap缓存中
-
从锚点Item3处往下填充,填充到Item6为止,就没有足够的距离了,mAttachedScrap只剩下ViewHolder8,ViewHolder7,ViewHolder2,ViewHolder1
-
往上填充,虽然此时还有两个View的高度,但是此时,上边没有数据了,此处不填充
-
此时还有两个View的高度,继续往下填充
-
修复GAP
-
当前是布局阶段,但是因为ViewHolder1和ViewHolder2都是被Remove掉的,所以跳过
5.1.2.3 dispatchLayoutStep3阶段
-
Item1、Item2做消失动画 -
Item3、Item4~Item8做移动动画 -
动画结束后,Item1、Item2会被回收到mCachedViews缓存池中
5.2 notifyItemInserted场景
5.2.1 场景描述
假设在Item1下面插入两条数据AddItem1,AddItem2
5.2.2 布局过程
5.2.2.1 dispatchLayoutStep1阶段
-
寻找锚点,找到Item1 -
移除屏幕上的Views,放入到mAttachedScrap中 -
锚点处从上往下填充 -
锚点处从下往上填充,由上图可知,上面没有空间了,不填充 -
判断是否还有剩余的空间,如果有在末尾填充,下面没空间了,不填充 -
因为当前是预布局阶段,不填充
5.2.2.2 dispatchLayoutStep2阶段
-
寻找锚点,找到Item1 -
移除屏幕上的Views,放入到mAttachedScrap中 -
锚点处从上往下填充,此时将变化后的数据填充到屏幕上,addItem1和addItem2被填充到item1下面 -
锚点处从下往上填充,由图可知,没有空间不填充 -
判断是否还有剩余的空间,由图可知,没有空间不填充 -
当前是layoutStep2阶段,会将mAttachScrap的内容,填充到屏幕末尾,ViewHolder5和ViewHolder6对应的ItemView被填充
5.2.2.3 dispatchLayoutStep3阶段
-
Item2、Item3~Item6做移动动画 -
addItem1、addItem2做淡入动画 -
动画结束后Item5、Item6被回收到mCachedViews缓存池中
5.3 场景总结
5.3.1 notifyItemRemoved场景
5.3.2 notifyItemInserted场景
本文分享自微信公众号 - 音视频开发进阶(glumes_blog)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。