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

简介: 面试官: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


相关文章
|
8月前
|
消息中间件 存储 缓存
大厂面试高频:Kafka 工作原理 ( 详细图解 )
本文详细解析了 Kafka 的核心架构和实现原理,消息中间件是亿级互联网架构的基石,大厂面试高频,非常重要,建议收藏。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:Kafka 工作原理 ( 详细图解 )
|
7月前
|
存储 SQL 关系型数据库
MySQL进阶突击系列(03) MySQL架构原理solo九魂17环连问 | 给大厂面试官的一封信
本文介绍了MySQL架构原理、存储引擎和索引的相关知识点,涵盖查询和更新SQL的执行过程、MySQL各组件的作用、存储引擎的类型及特性、索引的建立和使用原则,以及二叉树、平衡二叉树和B树的区别。通过这些内容,帮助读者深入了解MySQL的工作机制,提高数据库管理和优化能力。
|
5月前
|
存储 NoSQL 前端开发
美团面试:手机扫描PC二维码登录,底层原理和完整流程是什么?
45岁老架构师尼恩详细梳理了手机扫码登录的完整流程,帮助大家在面试中脱颖而出。该过程分为三个阶段:待扫描阶段、已扫描待确认阶段和已确认阶段。更多技术圣经系列PDF及详细内容,请关注【技术自由圈】获取。
|
11月前
|
安全 Java 容器
【Java集合类面试二十七】、谈谈CopyOnWriteArrayList的原理
CopyOnWriteArrayList是一种线程安全的ArrayList,通过在写操作时复制新数组来保证线程安全,适用于读多写少的场景,但可能因内存占用和无法保证实时性而有性能问题。
|
7月前
|
Java 数据库连接 Maven
最新版 | 深入剖析SpringBoot3源码——分析自动装配原理(面试常考)
自动装配是现在面试中常考的一道面试题。本文基于最新的 SpringBoot 3.3.3 版本的源码来分析自动装配的原理,并在文未说明了SpringBoot2和SpringBoot3的自动装配源码中区别,以及面试回答的拿分核心话术。
最新版 | 深入剖析SpringBoot3源码——分析自动装配原理(面试常考)
|
6月前
|
Java Linux 调度
硬核揭秘:线程与进程的底层原理,面试高分必备!
嘿,大家好!我是小米,29岁的技术爱好者。今天来聊聊线程和进程的区别。进程是操作系统中运行的程序实例,有独立内存空间;线程是进程内的最小执行单元,共享内存。创建进程开销大但更安全,线程轻量高效但易引发数据竞争。面试时可强调:进程是资源分配单位,线程是CPU调度单位。根据不同场景选择合适的并发模型,如高并发用线程池。希望这篇文章能帮你更好地理解并回答面试中的相关问题,祝你早日拿下心仪的offer!
123 6
|
8月前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
8月前
|
存储 安全 Java
面试高频:Synchronized 原理,建议收藏备用 !
本文详解Synchronized原理,包括其作用、使用方式、底层实现及锁升级机制。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
面试高频:Synchronized 原理,建议收藏备用 !
|
9月前
|
存储 监控 算法
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程 ?
尼恩提示: G1垃圾回收 原理非常重要, 是面试的重点, 大家一定要好好掌握
美团面试:说说 G1垃圾回收 底层原理?说说你 JVM 调优的过程  ?
|
9月前
|
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
    云计算运维工程师面试技巧
    900
  • 2
    【机器学习】面试问答:PCA算法介绍?PCA算法过程?PCA为什么要中心化处理?PCA为什么要做正交变化?PCA与线性判别分析LDA降维的区别?
    317
  • 3
    【机器学习】面试问答:决策树如何进行剪枝?剪枝的方法有哪些?
    234
  • 4
    【机器学习】SVM面试题:简单介绍一下SVM?支持向量机SVM、逻辑回归LR、决策树DT的直观对比和理论对比,该如何选择?SVM为什么采用间隔最大化?为什么要将求解SVM的原始问题转换为其对偶问题?
    211
  • 5
    【深度学习】Pytorch面试题:什么是 PyTorch?PyTorch 的基本要素是什么?Conv1d、Conv2d 和 Conv3d 有什么区别?
    733
  • 6
    【深度学习】TensorFlow面试题:什么是TensorFlow?你对张量了解多少?TensorFlow有什么优势?TensorFlow比PyTorch有什么不同?该如何选择?
    563
  • 7
    【机器学习】面试题:LSTM长短期记忆网络的理解?LSTM是怎么解决梯度消失的问题的?还有哪些其它的解决梯度消失或梯度爆炸的方法?
    503
  • 8
    【数据挖掘】XGBoost面试题:与GBDT的区别?为什么使用泰勒二阶展开?为什么可以并行训练?为什么快?防止过拟合的方法?如何处理缺失值?
    558
  • 9
    【数据挖掘】 GBDT面试题:其中基分类器CART回归树,节点的分裂标准是什么?与RF的区别?与XGB的区别?
    188
  • 10
    【机器学习】过拟合和欠拟合怎么判断,如何解决?(面试回答)
    1007