前言

温馨提示:文章有点长,建议关注微信公众号“字节小站”收藏阅读

本文主要通过以下几个方面来讲解RecyclerView的布局和动画原理:

  1. 布局放置:RecyclerView#dispatchLayout()
  2. 预布局阶段:RecyclerView#dispatchLayoutStep1()
  3. 布局阶段:RecyclerView#dispatchLayoutStep2()
  4. 开启动画阶段: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()与其他方法的区别:

  1. 会导致整个列表刷新,其它几个方法则不会;
  2. 不会触发RecyclerView的动画机制,其它几个方法则会触发各种不同类型的动画。

1. 布局放置

1.1 核心方法

RecyclerView#dispatchLayout()

1.2 作用

  1. 将View放置到合适的位置
  2. 记录布局阶段View的信息
  3. 处理动画

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()

  1. dispatchLayoutStep1()执行预布局,记录ViewHolder位置信息;
  2. dispatchLayoutStep2()执行布局,用户最终看到的效果;
  3. dispatchLayoutStep3()执行动画操作。

2. 预布局阶段

2.1 核心方法

  1. RecyclerView#dispatchLayoutStep1()

  2. RecyclerView#processAdapterUpdatesAndSetAnimationFlags()

  3. LinearLayoutManager#onLayoutChildren()

  4. LinearLayoutManager#updateAnchorInfoForLayout()

2.2 作用

  1. 处理Adapter变化
  2. 决定该执行哪种类型动画
  3. 保存当前RecyclerView上的子View的信息
  4. 如果需要执行动画,进行预布局

2.3 源码解析

2.3.1 RecyclerView#dispatchLayoutStep1()

  1. 判断是否需要开启动画功能
  2. 如果开启动画,将当前屏幕上的Item相关信息保存起来供后续动画使用
  3. 如果开启动画,调用mLayout.onLayoutChildren方法预布局
  4. 预布局后,与第二步保存的信息对比,将新出现的Item信息保存到Appeared中

2.3.2 RecyclerView#processAdapterUpdatesAndSetAnimationFlags()

作用:判断是否需要开启动画
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yCKSinj1-1613957809968)(https://cdn.jsdelivr.net/gh/lizijin/bytestation@master/rv/animationpic3.png)]

2.3.3 LinearLayoutManager#onLayoutChildren()

以垂直方向的RecyclerView为例子,我们填充RecyclerView的方向有两种,从上往下填充和从下往上填充。开始填充的位置不是固定的,可以从RecyclerView的任意位置处开始填充。

  1. 寻找填充的锚点(最终调用findReferenceChild方法);
  2. 移除屏幕上的Views(最终调用detachAndScrapAttachedViews方法);
  3. 从锚点处从上往下填充(调用fill和layoutChunk方法);
  4. 从锚点处从下往上填充(调用fill和layoutChunk方法);
  5. 如果还有多余的空间,继续填充(调用fill和layoutChunk方法);
  6. 布局完成后有可能产生GAP,需要修复GAP;
  7. dispatchLayoutStep2阶段调用layoutForPredictiveAnimation将scrapList中多余的ViewHolder填充(调用fill和layoutChunk方法)。


2.3.3.1 寻找填充的锚点

  1. 优先返回全部在屏幕内,未标记removed的View;
  2. 次优先级返回不可见的View;
  3. 最低优先级返回删掉的view。


2.3.3.2 移除屏幕上的Views

  1. 调用notifyItemChanged(position),position对应的ViewHolder会放入到mChangedScrap缓存中;
  2. 否则会放入到mAttachedScrap缓存中
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8z87DbQG-1613957809974)(https://cdn.jsdelivr.net/gh/lizijin/bytestation@master/rv/animationpic6.png)]

2.3.3.3 ~ 2.3.3.5 填充

调用LinearLayoutManager#fill()和LinearLayoutManager#layoutChunk()

  1. 从缓存中获取View或者创建View
  2. 如果是step1预布局阶段,调用addView(),将标记为removed的view放入到DISAPPEARED动画列表中
  3. 如果是step2布局阶段,调用addDisappearingView(),将被挤出屏幕的view放入到DISAPPEARED动画列表中
  4. 如果是removed的或者changed,不会记录消耗的填充量


2.3.3.6 修复GAP

通过mOrientationHelper.offsetChildren(gap)直接填补GAP


2.3.3.7 layoutForPredictiveAnimation

为了做动画,增加额外的Item

  1. 不需要做动画,或者是预布局直接返回
  2. 从mAttachedScrap中遍历到非removed的ViewHolder,但是返回的结果可能包含removed ViewHolder
  3. 如果遍历找到了非Removed ViewHolder,填充View
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-376eFIsJ-1613957809976)(https://cdn.jsdelivr.net/gh/lizijin/bytestation@master/rv/animationpic8.png)]

3. 布局阶段

3.1 核心方法

  1. RecyclerView#dispatchLayoutStep2()
  2. LinearLayoutManager#layoutChunk()
  3. LinearLayoutManager#addDisappearingView()
  4. ViewInfoStore#addToDisappearedInLayout()

3.2 作用

  1. 根据数据源中的数据进行布局,真正展示给用户看的最终界面
  2. 如果开启动画,将被挤出屏幕的View的保存到消失动画列表中

3.3 源码解析

3.3.1 RecyclerView#dispatchLayoutStep2()

  1. 将预布局模式改为false
  2. 布局填充View

3.3.2 LinearLayoutManager#layoutChunk()

布局阶段将被挤出屏幕的View放入到DISAPPEARED动画列表中

3.3.3 LinearLayoutManager#addDisappearingView()

把Removed的View或被挤出屏幕的View添加到Disappearing动画列表

3.3.4 ViewInfoStore#addToDisappearedInLayout()

加入到Disappeared动画列表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rBJnErUZ-1613957809979)(https://cdn.jsdelivr.net/gh/lizijin/bytestation@master/rv/animationpic26.png)]

4. 触发动画阶段

4.1 核心方法

  1. RecyclerView#dispatchLayoutStep3()
  2. ViewInfoStore#addToPostLayout()
  3. ViewInfoStore#process()
  4. ItemAnimator#animateAppearance()

4.2 作用

  1. 清理工作
  2. 保存布局后的view的信息
  3. 触发动画
  4. 动画执行完回收工作

4.3 源码解析

4.3.1 RecyclerView#dispatchLayoutStep3()

  1. 将当前屏幕上的View信息记录到postLayout动画列表中
  2. 执行动画
  3. 清理操作
  4. 布局完成回调
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TfroM0ow-1613957809980)(https://cdn.jsdelivr.net/gh/lizijin/bytestation@master/rv/animationpic28.png)]

4.3.2 ViewInfoStore#addToPostLayout()

View信息记录到postLayout动画列表中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eCi62CMf-1613957809981)(https://cdn.jsdelivr.net/gh/lizijin/bytestation@master/rv/animationpic29.png)]

4.3.3 ViewInfoStore#process()

作用:执行动画

工作流程,按优先级执行

  1. 调用unuse() 将view回收掉
  2. 执行消失动画
  • 2.1 预布局中不可见调用unuse()
  • 2.2 调用processDisappeared()
  1. 调用processPersistent()执行move或者change动画
  2. 执行remove动画
  3. 执行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 接口实现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WO3iC6FU-1613957809984)(https://cdn.jsdelivr.net/gh/lizijin/bytestation@master/rv/animationpic15.png)]

4.3.7 ProccessCallback#processAppeared

兵分两路

  1. 调用ItemAnimator#animateAppearance()
  2. 调用RecyclerView#postAnimationRunner()

4.3.8 一路兵:ItemAnimator#animateAppearance()

4.3.8.1 SimpleItemAnimator#animateAppearance
  1. 该方法返回true表示需要做动画
  2. 否则不需要做动画
  3. 如果预布局前View已经存在而且位置发生改变,处理MOVE动画
  4. 否则,处理ADD动画
4.3.8.2 DefaultItemAnimator.animateMove
  1. 该方法并没有真正执行动画
  2. 将MoveInfo保存到mPendingMoves中,以便RecyclerView#postAnimationRunner()使用
  3. 判断是否有必要执行MOVE动画
  4. 回到preLayout的位置

4.3.8.3 DefaultItemAnimator.animateAdd

先调用setAlpha(0),以便做淡入动画
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y9ff8cjg-1613957809987)(https://cdn.jsdelivr.net/gh/lizijin/bytestation@master/rv/animationpic22.png)]


4.3.9 二路兵:RecyclerView#postAnimationRunner()

4.3.9.1 RecyclerView#postAnimationRunner

最终调用到ItemAnimator.runPendingAnimations

4.3.9.2 DefaultItemAnimator.runPendingAnimations
  1. 首先执行Remove动画
  2. 然后同时执行Move和Change动画
  3. 最后执行Add动画

动画的总时长为removeDuration + Math.max(moveDuration, changeDuration) + addDuration

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G0Ae7Iap-1613957809989)(https://cdn.jsdelivr.net/gh/lizijin/bytestation@master/rv/animationpic32.png)]

4.3.10 RecyclerView$ItemAnimatorRestoreListener

作用:动画结束后执行回收操作

  1. 动画执行完毕,removeAnimatingView
  2. 调用Recycler.recycleViewHolderInternal执行回收操作

5. 场景篇

5.1 notifyItemRemoved场景

5.1.1 场景描述

  1. 调用notifyItemRemoved()
  2. Adapter数据有100条,屏幕上有Item1~Item6 6个View,删除Item1和Item2

5.1.2 布局过程

  1. 将Item1 Item2对应的ViewHolder设置为REMOVE状态
  2. 将所有的Item对应的ViewHolder的mPreLayoutPosition字段赋值为当前的position

5.1.2.1 dispatchLayoutStep1阶段

  1. 寻找填充的锚点,寻找锚点的逻辑是,从上往下,找到第一个非remove状态的Item。在本Case中,找到Item3

  2. 移除屏幕上的Views,将它们的ViewHolder放入到Recycler的mAttachedScrap缓存中,这个缓存的好处是如果position对应上了,无需重新绑定,直接拿来用。

  3. 从锚点Item3处往下填充,mAttachedScrap只剩下ViewHolder2和ViewHolder1

  4. 从锚点Item3处往上填充Item2 Item1,因为Item2,Imte1已经被remove掉了,它消耗的空间不会被记录,那么到步骤5的时候还可以填充
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-my2RE43T-1613957809994)(https://cdn.jsdelivr.net/gh/lizijin/bytestation@master/dmall/recyclerviewpic7.png)]

  5. 还有多余的空间,继续填充,把Item7、Item8填充到屏幕中

  6. 因为当前是预布局,直接返回


5.1.2.2 dispatchLayoutStep2阶段

  1. 寻找填充的锚点,寻找锚点的逻辑是,从上往下,找到第一个非remove状态的Item,找到Item3
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HNVRZI4G-1613957809995)(https://cdn.jsdelivr.net/gh/lizijin/bytestation@master/dmall/recyclerviewpic13.png)]

  2. 移除屏幕上的Views,将它们的ViewHolder放入到Recycler的mAttachedScrap缓存中
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GYiZq3f1-1613957809996)(https://cdn.jsdelivr.net/gh/lizijin/bytestation@master/dmall/recyclerviewpic14.png)]

  3. 从锚点Item3处往下填充,填充到Item6为止,就没有足够的距离了,mAttachedScrap只剩下ViewHolder8,ViewHolder7,ViewHolder2,ViewHolder1

  4. 往上填充,虽然此时还有两个View的高度,但是此时,上边没有数据了,此处不填充

  5. 此时还有两个View的高度,继续往下填充

  6. 修复GAP

  1. 当前是布局阶段,但是因为ViewHolder1和ViewHolder2都是被Remove掉的,所以跳过

5.1.2.3 dispatchLayoutStep3阶段

  1. Item1、Item2做消失动画
  2. Item3、Item4~Item8做移动动画
  3. 动画结束后,Item1、Item2会被回收到mCachedViews缓存池中

5.2 notifyItemInserted场景

5.2.1 场景描述

假设在Item1下面插入两条数据AddItem1,AddItem2

5.2.2 布局过程

5.2.2.1 dispatchLayoutStep1阶段

  1. 寻找锚点,找到Item1
  2. 移除屏幕上的Views,放入到mAttachedScrap中
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p2xYBR44-1613957810007)(https://cdn.jsdelivr.net/gh/lizijin/bytestation@master/dmall/recyclerviewadd2.png)]
  3. 锚点处从上往下填充
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xPIBFOju-1613957810008)(https://cdn.jsdelivr.net/gh/lizijin/bytestation@master/dmall/recyclerviewadd3.png)]
  4. 锚点处从下往上填充,由上图可知,上面没有空间了,不填充
  5. 判断是否还有剩余的空间,如果有在末尾填充,下面没空间了,不填充
  6. 因为当前是预布局阶段,不填充

5.2.2.2 dispatchLayoutStep2阶段

  1. 寻找锚点,找到Item1
  2. 移除屏幕上的Views,放入到mAttachedScrap中
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l088wjTE-1613957810010)(https://cdn.jsdelivr.net/gh/lizijin/bytestation@master/dmall/recyclerviewadd2.png)]
  3. 锚点处从上往下填充,此时将变化后的数据填充到屏幕上,addItem1和addItem2被填充到item1下面
  4. 锚点处从下往上填充,由图可知,没有空间不填充
  5. 判断是否还有剩余的空间,由图可知,没有空间不填充
  6. 当前是layoutStep2阶段,会将mAttachScrap的内容,填充到屏幕末尾,ViewHolder5和ViewHolder6对应的ItemView被填充

5.2.2.3 dispatchLayoutStep3阶段

  1. Item2、Item3~Item6做移动动画
  2. addItem1、addItem2做淡入动画
  3. 动画结束后Item5、Item6被回收到mCachedViews缓存池中

5.3 场景总结

5.3.1 notifyItemRemoved场景

5.3.2 notifyItemInserted场景

欢迎关注我的微信公众号:字节小站


这是一个分享Android技术干货的公众号,如果您喜欢我的文章,帮我点“在看”。如果您觉得文章有干货,帮我分享给您的朋友吧。你们的支持是我持续创作的动力。欢迎投稿,让我们一起成长。一定要记得微信搜索“字节小站”,关注我哟~

面试官:RecyclerView布局动画原理了解吗?相关推荐

  1. Android RecyclerView布局动画

    In this tutorial, we'll be discussing and implementing RecyclerView Layout Animations in our Android ...

  2. 实战系列-被面试官问到Feign原理

    导语   事情是这样的,昨天参加了某公司二面,被面试官问道了Spring Cloud的RESTFul远程调用.项目上用到的技术就是OpenFeign,面试官可能自己不是太了解,给他解释一番发现自己还有 ...

  3. 手写Vuex核心原理,再也不怕面试官问我Vuex原理

    手写Vuex核心原理 文章目录 手写Vuex核心原理 一.核心原理 二.基本准备工作 三.剖析Vuex本质 四.分析Vue.use 五.完善install方法 六.实现Vuex的state 七.实现g ...

  4. babel原理_手写webpack核心原理,再也不怕面试官问我webpack原理

    手写webpack核心原理 一.核心打包原理 1.1 打包的主要流程如下 1.2 具体细节 二.基本准备工作 三.获取模块内容 四.分析模块 五.收集依赖 六.ES6转成ES5(AST) 七.递归获取 ...

  5. 面试官_vue的这些原理你了解吗_

    前言 努力! 奋斗! 爱折腾的程序员, 不会没关系, 最重要的是我们一起学习! 在之前面试的时候我自己也经常会遇到一些vue原理的问题, 我也总结了下自己的经常的用到的,方便自己学习,今天也给大家分享 ...

  6. 【面试题】面试官:vue的这些原理你了解吗?

    前言 在之前面试的时候我自己也经常会遇到一些vue原理的问题, 我也总结了下自己的经常的用到的,方便自己学习,今天也给大家分享出来, 欢迎大家一起学习交流, 有更好的方法欢迎评论区指出, 后序我也将持 ...

  7. 面试官:vue的这些原理你了解吗?

    前言 努力! 奋斗! 爱折腾的程序员, 不会没关系, 最重要的是我们一起学习! 在之前面试的时候我自己也经常会遇到一些vue原理的问题, 我也总结了下自己的经常的用到的,方便自己学习,今天也给大家分享 ...

  8. 定时器和promise_手写Promise核心原理,再也不怕面试官问我Promise原理

    整体流程的介绍 整体流程的介绍 1. 定义整体结构 2. 实现Promise构造函数 3. 实现then方法 3.实现catch方法 4. 实现Promise.resolve 5.实现Promise. ...

  9. 面试官:熔断降级原理是什么?

    仅以两张图来初步形容一下 熔断 适用的场景: 雪崩 股灾   什么是熔断 来自 wiki 的 熔断机制 描述: 熔断机制(英语:Circuit breaker / Trading curb)指的是在股 ...

最新文章

  1. Apache Tomcat7+MySQL5.6配置
  2. 直播预告|灵动MM32 MCU助力全国大学生智能汽车竞赛——基础培训
  3. matlab 图论工具箱
  4. 有了java为什么还需要groovy_Groovy创始人:Java面临终结 Scala将取而代之
  5. 微软称电脑系统识别能力已经超越了人类
  6. fastxml 大于符号不转换_你可能不知道的MATLAB入门技巧#第二话
  7. mysql集群初始化配置_集群Cluster MySQL的安装配置和使用
  8. c#.winform,datagridview,数组,绑定,字符串,字符串数组绑定datagridview显示,长度,显示数组内容...
  9. 深度学习《patchGAN》
  10. 按 字节截取分别以GBK 和 utf-8 编码的 字符串的java程序。
  11. ssm如何支持热部署_IntelliJ IDEA基于SpringBoot如何搭建SSM开发环境
  12. 转:什么是Node.js?
  13. 软件开发模型_为什么越来越多软件开发团队都放弃了瀑布模型?
  14. 零基础想考华为认证,该怎么学?
  15. mysql md5 sha1_PHP md5 vs sha1 性能测试
  16. 福田中心区20个楼盘航拍全景
  17. IO中write函数
  18. 断网怎么装网卡驱动?
  19. Composer的基本使用
  20. Mac常见问题:如何在Mac中查看多张图片

热门文章

  1. AS3多人游戏开发—同步人物移动
  2. 管不住嘴、挪不动腿?
  3. 南加大计算机游戏专业,南加州大学计算机科学理科硕士(游戏发展)入学条件及实习就业...
  4. mounted钩子函数_vue中created钩子函数与mounted钩子函数的使用区别
  5. 威力曲面sw2020_威力曲面powersufacing_沐风网
  6. sping全家桶笔记
  7. leetcode(575)分糖果
  8. docx-templates前端模板引擎生成word
  9. 海康摄像头http抓图
  10. vue-seamless-scroll 从入坑到放弃