面试官:RecyclerView布局动画原理了解吗?

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 面试官:RecyclerView布局动画原理了解吗?

前言


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

本文主要通过以下几个方面来讲解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()执行动画操作。

640.png

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中

640.png


2.3.2 RecyclerView#processAdapterUpdatesAndSetAnimationFlags()

作用:判断是否需要开启动画

640.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方法)。


image.png

image.png

image.png

image.png


2.3.3.1 寻找填充的锚点


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

640.png


2.3.3.2 移除屏幕上的Views


  1. 调用notifyItemChanged(position),position对应的ViewHolder会放入到mChangedScrap缓存中;
  2. 否则会放入到mAttachedScrap缓存中

640.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,不会记录消耗的填充量

640.png


2.3.3.6 修复GAP


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

640.png


2.3.3.7 layoutForPredictiveAnimation


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

  1. 不需要做动画,或者是预布局直接返回
  2. 从mAttachedScrap中遍历到非removed的ViewHolder,但是返回的结果可能包含removed ViewHolder
  3. 如果遍历找到了非Removed ViewHolder,填充View

640.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

640.png

3.3.2 LinearLayoutManager#layoutChunk()

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

640.png

3.3.3 LinearLayoutManager#addDisappearingView()

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

640.png


3.3.4 ViewInfoStore#addToDisappearedInLayout()

加入到Disappeared动画列表

640.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. 布局完成回调

640.png


4.3.2 ViewInfoStore#addToPostLayout()

View信息记录到postLayout动画列表中

640.png



4.3.3 ViewInfoStore#process()

作用:执行动画


工作流程,按优先级执行


  1. 调用unuse() 将view回收掉
  2. 执行消失动画
  • 2.1 预布局中不可见调用unuse()
  • 2.2 调用processDisappeared()
  1. 调用processPersistent()执行move或者change动画
  2. 执行remove动画
  3. 执行insert动画

640.png

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基础上做出现动画

640.png

4.3.5 ViewInfoStore$ProccessCallback

作用:定义四种处理动画的接口

  • processDisappeared 处理消失动画
  • processAppeared 处理出现动画
  • processPersistent 处理一直存在动画,包含move和change动画
  • unused 不需要处理动画,执行回收

640.png


4.3.6 接口实现


640.png



4.3.7 ProccessCallback#processAppeared

兵分两路

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

640.png


4.3.8 一路兵:ItemAnimator#animateAppearance()

4.3.8.1 SimpleItemAnimator#animateAppearance
  1. 该方法返回true表示需要做动画
  2. 否则不需要做动画
  3. 如果预布局前View已经存在而且位置发生改变,处理MOVE动画
  4. 否则,处理ADD动画

640.png


4.3.8.2 DefaultItemAnimator.animateMove
  1. 该方法并没有真正执行动画
  2. 将MoveInfo保存到mPendingMoves中,以便RecyclerView#postAnimationRunner()使用
  3. 判断是否有必要执行MOVE动画
  4. 回到preLayout的位置

640.png

4.3.8.3 DefaultItemAnimator.animateAdd

先调用setAlpha(0),以便做淡入动画

640.png


4.3.9 二路兵:RecyclerView#postAnimationRunner()

4.3.9.1 RecyclerView#postAnimationRunner

最终调用到ItemAnimator.runPendingAnimations

640.png


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

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

640.png



4.3.10 RecyclerView$ItemAnimatorRestoreListener

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

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

640.png


5. 场景篇


5.1 notifyItemRemoved场景

5.1.1 场景描述

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

640.png

5.1.2 布局过程

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

5.1.2.1 dispatchLayoutStep1阶段

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

640.png

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

640.png

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

640.png


从锚点Item3处往上填充Item2 Item1,因为Item2,Imte1已经被remove掉了,它消耗的空间不会被记录,那么到步骤5的时候还可以填充

640.png

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

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


5.1.2.2 dispatchLayoutStep2阶段


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

640.png

移除屏幕上的Views,将它们的ViewHolder放入到Recycler的mAttachedScrap缓存中

640.png


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

640.png

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

640.png

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

640.png

修复GAP

640.png

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

640.png

5.1.2.3 dispatchLayoutStep3阶段


Item1、Item2做消失动画

Item3、Item4~Item8做移动动画

动画结束后,Item1、Item2会被回收到mCachedViews缓存池中

640.png



5.2 notifyItemInserted场景

5.2.1 场景描述

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

640.png

5.2.2 布局过程

5.2.2.1 dispatchLayoutStep1阶段

  1. 寻找锚点,找到Item1


640.png

移除屏幕上的Views,放入到mAttachedScrap中

640.png

锚点处从上往下填充

640.png

  1. 锚点处从下往上填充,由上图可知,上面没有空间了,不填充
  2. 判断是否还有剩余的空间,如果有在末尾填充,下面没空间了,不填充
  3. 因为当前是预布局阶段,不填充


5.2.2.2 dispatchLayoutStep2阶段


  1. 寻找锚点,找到Item1

640.png

移除屏幕上的Views,放入到mAttachedScrap中

640.png

锚点处从上往下填充,此时将变化后的数据填充到屏幕上,addItem1和addItem2被填充到item1下面

640.png

  1. 锚点处从下往上填充,由图可知,没有空间不填充
  2. 判断是否还有剩余的空间,由图可知,没有空间不填充
  3. 当前是layoutStep2阶段,会将mAttachScrap的内容,填充到屏幕末尾,ViewHolder5和ViewHolder6对应的ItemView被填充

640.png


5.2.2.3 dispatchLayoutStep3阶段


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

640.png

5.3 场景总结


5.3.1 notifyItemRemoved场景

640.png

5.3.2 notifyItemInserted场景

640.png


相关文章
|
7天前
|
消息中间件 存储 缓存
大厂面试高频:Kafka 工作原理 ( 详细图解 )
本文详细解析了 Kafka 的核心架构和实现原理,消息中间件是亿级互联网架构的基石,大厂面试高频,非常重要,建议收藏。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:Kafka 工作原理 ( 详细图解 )
|
3月前
|
JavaScript 前端开发
【Vue面试题二十五】、你了解axios的原理吗?有看过它的源码吗?
这篇文章主要讨论了axios的使用、原理以及源码分析。 文章中首先回顾了axios的基本用法,包括发送请求、请求拦截器和响应拦截器的使用,以及如何取消请求。接着,作者实现了一个简易版的axios,包括构造函数、请求方法、拦截器的实现等。最后,文章对axios的源码进行了分析,包括目录结构、核心文件axios.js的内容,以及axios实例化过程中的配置合并、拦截器的使用等。
【Vue面试题二十五】、你了解axios的原理吗?有看过它的源码吗?
|
3月前
|
安全 Java 容器
【Java集合类面试二十七】、谈谈CopyOnWriteArrayList的原理
CopyOnWriteArrayList是一种线程安全的ArrayList,通过在写操作时复制新数组来保证线程安全,适用于读多写少的场景,但可能因内存占用和无法保证实时性而有性能问题。
|
3月前
|
安全 Java 数据库
一天十道Java面试题----第四天(线程池复用的原理------>spring事务的实现方式原理以及隔离级别)
这篇文章是关于Java面试题的笔记,涵盖了线程池复用原理、Spring框架基础、AOP和IOC概念、Bean生命周期和作用域、单例Bean的线程安全性、Spring中使用的设计模式、以及Spring事务的实现方式和隔离级别等知识点。
|
9天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
9天前
|
存储 安全 Java
面试高频:Synchronized 原理,建议收藏备用 !
本文详解Synchronized原理,包括其作用、使用方式、底层实现及锁升级机制。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
面试高频:Synchronized 原理,建议收藏备用 !
|
3月前
|
JavaScript 前端开发
【Vue面试题二十七】、你了解axios的原理吗?有看过它的源码吗?
文章讨论了Vue项目目录结构的设计原则和实践,强调了项目结构清晰的重要性,提出了包括语义一致性、单一入口/出口、就近原则、公共文件的绝对路径引用等原则,并展示了单页面和多页面Vue项目的目录结构示例。
|
1月前
|
存储 监控 算法
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程 ?
尼恩提示: G1垃圾回收 原理非常重要, 是面试的重点, 大家一定要好好掌握
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程  ?
|
1月前
|
SQL 存储 关系型数据库
美团面试:binlog、redo log、undo log的底层原理是什么?它们分别实现ACID的哪个特性?
老架构师尼恩在其读者交流群中分享了关于 MySQL 中 redo log、undo log 和 binlog 的面试题及其答案。这些问题涵盖了事务的 ACID 特性、日志的一致性问题、SQL 语句的执行流程等。尼恩详细解释了这些日志的作用、所在架构层级、日志形式、缓存机制以及写文件方式等内容。他还提供了多个面试题的详细解答,帮助读者系统化地掌握这些知识点,提升面试表现。此外,尼恩还推荐了《尼恩Java面试宝典PDF》和其他技术圣经系列PDF,帮助读者进一步巩固知识,实现“offer自由”。
美团面试:binlog、redo log、undo log的底层原理是什么?它们分别实现ACID的哪个特性?
|
1月前
|
负载均衡 算法 Java
蚂蚁面试:Nacos、Sentinel了解吗?Springcloud 核心底层原理,你知道多少?
40岁老架构师尼恩分享了关于SpringCloud核心组件的底层原理,特别是针对蚂蚁集团面试中常见的面试题进行了详细解析。内容涵盖了Nacos注册中心的AP/CP模式、Distro和Raft分布式协议、Sentinel的高可用组件、负载均衡组件的实现原理等。尼恩强调了系统化学习的重要性,推荐了《尼恩Java面试宝典PDF》等资料,帮助读者更好地准备面试,提高技术实力,最终实现“offer自由”。更多技术资料和指导,可关注公众号【技术自由圈】获取。
蚂蚁面试:Nacos、Sentinel了解吗?Springcloud 核心底层原理,你知道多少?