RecyclerView源码浅析之数据更改与动画
概述
上一篇我们针对RecyclerView的绘制流程做了简单的分析,重点放在了dispatchLayoutStep2()这个真正对子View操作的函数上,它完成了:子View的添加(LinearLayoutManager通过ChildHelper添加)、测量、布局。
绘制流程虽然有了大概的了解,但却引出了很多问题:dispatchLayoutStep1()和dispatchLayoutStep3()有什么作用?ChildHelper如何管理子View?动画怎么产生?Recycler如何回收和提供ViewHolder?等等等等。
Recycler的问题这一篇依旧不讲(实在解耦得太好了以至于可以随时单独拿出来讲),我们下面着重来看一下数据变动下View的动画到底是如何产生的,顺带对上面的一些问题作出分析。
在网上看了好几篇博客都看得云里雾里,后来索性找到了参与编写RecyclerView的工程师写的解释RV动画的博客才梳理通了整个流程,大家可以直接去看原博客,我在跟源码的过程中会把对博客的理解写出来。
首先说明几个类。
AdapterHelper
这个类的介绍是Helper class that can enqueue and process adapter update operations,可以入列和执行adapter的更新操作,个人理解是AdapterHelper托管了Adapter的数据更新操作,即Adapter中Datas改变后调用的notifyItemXXX()都是由AdapterHelper来执行的,而且这个数据的变化到界面上的变化这之间是经历一个onLayout的,AdapterHelper将Adapter的操作记录为一个对象(UpdateOp),并且操作的结果映射在VH的position上,并通过onLayout的过程显示出来。重要的地方注释了, 还有很多方法没有列出来,到具体用到的时候再说。
class AdapterHelper implements OpReorderer.Callback {//一些数据结构保存UpdateOpprivate Pools.Pool<UpdateOp> mUpdateOpPool = new Pools.SimplePool<UpdateOp>(UpdateOp.POOL_SIZE);final ArrayList<UpdateOp> mPendingUpdates = new ArrayList<UpdateOp>();final ArrayList<UpdateOp> mPostponedList = new ArrayList<UpdateOp>();final Callback mCallback;Runnable mOnItemProcessedCallback;final boolean mDisableRecycler;//一个可以实现UpdateOp排序的类,似乎是因为对数据集的操作有优先级final OpReorderer mOpReorderer;private int mExistingUpdateTypes = 0;AdapterHelper(Callback callback) {this(callback, false);} //在RecyclerView初始化的时候我们新建了一个AdapterHelper,并且传入了一个实现了这个Callback的匿名内部类作为AdapterHelper和RV通信手段AdapterHelper(Callback callback, boolean disableRecycler) {mCallback = callback;mDisableRecycler = disableRecycler;mOpReorderer = new OpReorderer(this);}AdapterHelper addUpdateOp(UpdateOp... ops) {Collections.addAll(mPendingUpdates, ops);return this;}//封装各种操作static class UpdateOp {static final int ADD = 1;static final int REMOVE = 1 << 1;static final int UPDATE = 1 << 2;static final int MOVE = 1 << 3;static final int POOL_SIZE = 30;int cmd;int positionStart;Object payload;// holds the target position if this is a MOVEint itemCount;UpdateOp(int cmd, int positionStart, int itemCount, Object payload) {this.cmd = cmd;this.positionStart = positionStart;this.itemCount = itemCount;this.payload = payload;}...}....//和RecyclerView通信的接口static interface Callback {...} }
ChildHelper
类的介绍是它是一个帮助RV管理child的类,一切和View的操作都被它承包了,就像上面AdapterHelper“拦截”了所有Adapter的操作一样。对于理解这个类我们需要知道一个非常重要的概念,就是它可以”hide”一些View,具体一点来说,它提供了两套API,分别用于操作所有的View和可见的View(指没有被hide)。像getChildAt, getChildCount这类正常的接口返回的是可见的View,而如果RV想对VG(ViewGroup)中所有的View进行访问,需要用“unfiltered”这样前缀的方法,比如getUnfilteredChildCount。这两套方法是针对LM(LayoutManager)的,并帮助动画的产生,这个后面再说。那么落地到这种功能如何实现的,就和其内部的成员有关。其中一个成员是mBucket,Bucket是一个Map,key是View,value是true/false(表示View是否特殊),另一个是mHiddenViews,这是一个保存了所有不可见的View的List。class ChildHelper { private static final boolean DEBUG = false; private static final String TAG = "ChildrenHelper"; final Callback mCallback; final Bucket mBucket; final List<View> mHiddenViews;ChildHelper(Callback callback) {mCallback = callback;mBucket = new Bucket();mHiddenViews = new ArrayList<View>();/*** Returns the number of children that are not hidden.** @return Number of children that are not hidden.* @see #getChildAt(int)*/ int getChildCount() {return mCallback.getChildCount() - mHiddenViews.size(); }/*** Returns the total number of children.** @return The total number of children including the hidden views.* @see #getUnfilteredChildAt(int)*/ int getUnfilteredChildCount() {return mCallback.getChildCount(); }boolean isHidden(View view) {return mHiddenViews.contains(view); } .../*** Bitset implementation that provides methods to offset indices.*/ static class Bucket {final static int BITS_PER_WORD = Long.SIZE;final static long LAST_BIT = 1L << (Long.SIZE - 1);long mData = 0;Bucket next;void set(int index) {if (index >= BITS_PER_WORD) {ensureNext();next.set(index - BITS_PER_WORD);} else {mData |= 1L << index;}}boolean get(int index) {if (index >= BITS_PER_WORD) {ensureNext();return next.get(index - BITS_PER_WORD);} else {return (mData & (1L << index)) != 0;}}... }//和RV通信的接口static interface Callback {...} }
dispatchLayoutStep1()
我们从Adapter#notifyItemRemoved()说起,如果我们添加了动画,这个方法会使动画效果显现出来,动画和dispatchLayoutStep1()还有dispatchLayoutStep3()相关,所以我们跟进一下这个方法。Adapter作为Observable,最终调用到了Observer的onItemRangeRemoved()。
//RecyclerViewDataObserver.java
//是RV的一个内部类@Overridepublic void onItemRangeRemoved(int positionStart, int itemCount) {assertNotInLayoutOrScroll(null);//添加UpdateOpif (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {//申请重绘triggerUpdateProcessor();}}
把REMOVE的UpdateOp添加到mPendingUpdates。
//AdapterHelper.javaboolean onItemRangeRemoved(int positionStart, int itemCount) {mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount, null));mExistingUpdateTypes |= UpdateOp.REMOVE;return mPendingUpdates.size() == 1;}
这里分享一个小技巧,我们在debug的时候可以使用条件断点,我设置的是点击按钮删除一个RV上的条目后条件成立停在onLayout(),这样方便于观察requestLayout()之后的layout发生了什么。
之后便会进入onLayout()
void dispatchLayout() {...if (mState.mLayoutStep == State.STEP_START) {dispatchLayoutStep1();...dispatchLayoutStep2();} ...dispatchLayoutStep3();...}
这个方法一开始会调用mViewInfoStore.clear(),我们同样还是先来看一下ViewInfoStore这个模块。
ViewInfoStore
对这个类的注释是Keeps data about views to be used for animations,记录了view的一些数据以用来做动画。我们还是以注释的方式对重要的部分说明。总的来说,这个类会保存VH的InfoRecord,而InfoRecord中比较重要的成员是VH的prelayout后和postlayout后的itemView边界信息ItemHolderInfo。class ViewInfoStore {//可以看到这里用了两个数据结构存储了VH的信息,key是VH,value是InfoRecord@VisibleForTestingfinal ArrayMap<ViewHolder, InfoRecord> mLayoutHolderMap = new ArrayMap<>();@VisibleForTestingfinal LongSparseArray<ViewHolder> mOldChangedHolders = new LongSparseArray<>();...//ViewInfoStore的静态内部类static class InfoRecord {// disappearing list 正在消失static final int FLAG_DISAPPEARED = 1;// appear in pre layout list 在prelayout出现static final int FLAG_APPEAR = 1 << 1;// pre layout, this is necessary to distinguish null item infostatic final int FLAG_PRE = 1 << 2;// post layout, this is necessary to distinguish null item infostatic final int FLAG_POST = 1 << 3;static final int FLAG_APPEAR_AND_DISAPPEAR = FLAG_APPEAR | FLAG_DISAPPEARED;static final int FLAG_PRE_AND_POST = FLAG_PRE | FLAG_POST;static final int FLAG_APPEAR_PRE_AND_POST = FLAG_APPEAR | FLAG_PRE | FLAG_POST;int flags;//VH的itemview的边界信息@Nullable ItemHolderInfo preInfo;@Nullable ItemHolderInfo postInfo;static Pools.Pool<InfoRecord> sPool = new Pools.SimplePool<>(20);...}... }//ItemAnimator.javapublic static class ItemHolderInfo {public int left;public int top;public int right;public int bottom;@AdapterChangespublic int changeFlags;...public ItemHolderInfo setFrom(RecyclerView.ViewHolder holder,@AdapterChanges int flags) {final View view = holder.itemView;this.left = view.getLeft();this.top = view.getTop();this.right = view.getRight();this.bottom = view.getBottom();return this;}...}
接下来我们继续看,以remove为例,dispatchLayoutStep1()也就是prelayout总体上做了这么几件事:
1.重排序所有UpdateOp。
2.依次执行所有UpdateOp事件,更新VH的position,如果VH被remove了标记它。
3.mViewInfoStore记录所有“可见的”的View的位置信息
4.用旧信息执行prelayout。
更具体的注释标注在了对应的方法(块)上面,大家可以进入方法对应查看,我就不把方法再具体拿出来了。总而言之,prelayout阶段RV把应该告诉LM的更新操作映射到了VH的mPreLayoutPosition上,LM根据mPreLayoutPosition进行布局,View的信息也被保存了一次。我们要记住既布局了旧信息,也布局了变化量(指的是因为更新而造成的新添加的View)
private void dispatchLayoutStep1() {...//1.重排序所有UpdateOp,比如move操作会排到末尾//2.依次执行所有UpdateOp事件,更新VH的position(这里是前移mPosition,mPreLayoutPosition不变),如果VH被remove了标记它。/*在2中,“会决定是否在prelayout之前把更新告诉LM”,这里把更新告诉LM指的是把更新反应在VH的mPreposition上(VH中有mPosition、mPreLayoutPosition等成员,注意,prelayout中是使用的mPreLayoutPosition)mPosition是一定会更新的,mPreLayoutPosition则不一定。如果RV决定不把更新再prelayout之前告诉LM,则会对VH更新时的参数applyToPreLayout传入false,mPosition更新了而mPreLayoutPosition则是旧值,反之mPreLayoutPosition则和mPosition同步。当然,如何“决定”我们就不说了,有兴趣可以看下原文和源码。*/processAdapterUpdatesAndSetAnimationFlags();...//这里最大最小position就是用的mPrelayoutPositionfindMinMaxChildLayoutPositions(mMinMaxLayoutPositions);if (mState.mRunSimpleAnimations) {//注意这里获取的是可见的Viewint count = mChildHelper.getChildCount();for (int i = 0; i < count; ++i) {final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) {continue;}//3.(Adapter的更新操作有选择地反应到mPreLayoutPosition之后),mViewInfoStore记录所有“可见的”的View的位置信息final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(mState, holder,ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),holder.getUnmodifiedPayloads());mViewInfoStore.addToPreLayout(holder, animationInfo);...}}if (mState.mRunPredictiveAnimations) {//4.用旧信息执行prelayout(即使被removed也布局),并且布局因为remove而新出现的item。(这里的旧信息应该就是mPreLayoutPosition,而具体的实现是在确定最大最小position的时候以及确定Anchor的时候都是用的mPreLayoutPosition,大家可以单步进去来看)。mLayout.onLayoutChildren(mRecycler, mState);...}
dispatchLayoutStep2()
接下来我们对比看一下dispatchLayoutStep2()也就是postlayout过程,主要做了下面几件事:
5.RV把剩余的更新操作映射到VH上
6.执行postlayout操作
private void dispatchLayoutStep2() {//5.RV把剩余的更新操作映射给VHmAdapterHelper.consumeUpdatesInOnePass();//6.执行postlayout(这里 似乎 是用了VH的mPosition,因为Anchor的布局位置被更新到0了),执行完毕后,RV中的内容和Adapter中的内容同步。//还要说一句的是这个方法的最后部分有一个layoutForPredictiveAnimations(),对于add更新是有用的,因为我们在布局前会先detach所有View,如果是add产生的变化,在postlayout中会有view超出了layout的position范围而没有被layout,只存在于scrap列表中,我们这时候也要在显示区外layout出来,以产生正确的动画。mLayout.onLayoutChildren(mRecycler, mState);....}
dispatchLayoutStep3()
第一次布局(prelayout)和第二次布局(postlayout)过后,就可以开始执行动画相关的操作了,dispatchLayoutStep3()主要做了这么几件事:
7.mItemAnimator记录postlayout过后所有“可见的”View的信息。
8.确定VH的类型。
9.执行相应动画。
10.执行完动画后做一些回收工作。
private void dispatchLayoutStep3() {if (mState.mRunSimpleAnimations) {int count = mChildHelper.getChildCount();for (int i = 0; i < count; ++i) {long key = getChangedHolderKey(holder);//7. mItemAnimator记录postlayout过后所有“可见的”View的信息。final ItemHolderInfo animationInfo = mItemAnimator.recordPostLayoutInformation(mState, holder);ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);} //8.确定VH的类型。//9.执行相应动画。mViewInfoStore.process(mViewInfoProcessCallback);}//10.执行完动画后做一些回收工作。mLayout.removeAndRecycleScrapInt(mRecycler);....}
总结
整个数据变动到动画产生的过程算是梳理清楚了,当然我现在水平还不够深入每一个细节去解释,动画的执行我也无意去深究。
总的来说(对于remove),在进入prelayout阶段之前,RV会决定是否将更新(UpdateOp)映射给VH,具体来说是VH的mPreLayoutPosition。prelayout阶段根据VH的mPreLayoutPosition确定锚点和最大最小位置后进行布局并多布局一个View,保存View的信息用作动画。postlayout阶段根据VH的mPosition布局,布局完之后数据和Adapter中同步。最后记录postlayout后的View信息并进行动画。
其实最大的收获是清楚了LayoutTransition和这里RV的Animation的区别,RV对remove和add都会进行“预处理”,以防item产生错误的动画,比如remove的时候view仅仅是“保持和移动”而并不是fade in。
下一篇我们来看一下VH从哪来以及到哪去——Recycler的作用,顺带把滑动也过一遍。
RecyclerView源码浅析之数据更改与动画相关推荐
- 【flink】Flink 1.12.2 源码浅析 : Task数据输入
1.概述 转载:Flink 1.12.2 源码浅析 : Task数据输入 在 Task 中,InputGate 是对输入的封装,InputGate 是和 JobGraph 中 JobEdge 一一对应 ...
- 【flink】Flink 1.12.2 源码浅析 :Task数据输出
1.概述 转载:Flink 1.12.2 源码浅析 :Task数据输出 Stream的计算模型采用的是PUSH模式, 上游主动向下游推送数据, 上下游之间采用生产者-消费者模式, 下游收到数据触发计算 ...
- 【MATLAB第11期】#源码分享 |时间序列数据绘图,横坐标更改为时间轴 横坐标轴参数更改 日期间隔设置 日期标签或格式更改
[MATLAB第11期]#源码分享 |时间序列数据绘图,横坐标更改为时间轴 横坐标轴参数更改 绘图问题解决方案 1.导入数据方式(识别时间数据) 2.案例展示 (1)打开数据 (2)定义时间和数据 ( ...
- 13个Vue3中的全局API的源码浅析汇总整理
前言 不知不觉vue-next的版本已经来到了3.1.2,最近对照着源码学习vue3的全局Api,边学习边整理了下来,希望可以和大家一起进步. 我们以官方定义.用法.源码浅析三个维度来一起看看它们.下 ...
- hashmap允许null键和值吗_hashMap底层源码浅析
来源:https://blog.csdn.net/qq_35824590/article/details/111769203 hashmap是我们经常使用的一个工具类.那么知道它的一些原理和特性吗? ...
- Android Loader机制全面详解及源码浅析
原文出处:csdn@工匠若水,http://blog.csdn.net/yanbober/article/details/48861457 一.概述 在Android中任何耗时的操作都不能放在UI主线 ...
- 内核启动流程分析(四)源码浅析
目录 kernel(四)源码浅析 建立工程 启动简析 head.s 入口点 查询处理器 查询机器ID 启动MMU 其他操作 start_kernel 处理命令行 分区 kernel(四)源码浅析 建立 ...
- harbor登录验证_Harbor 源码浅析
Harbor 源码浅析www.qikqiak.com Harbor 是一个CNCF基金会托管的开源的可信的云原生docker registry项目,可以用于存储.签名.扫描镜像内容,Harbor 通 ...
- fetch first mysql_MySQL多版本并发控制机制(MVCC)源码浅析
MySQL多版本并发控制机制(MVCC)-源码浅析 前言 作为一个数据库爱好者,自己动手写过简单的SQL解析器以及存储引擎,但感觉还是不够过瘾.<>诚然讲的非常透彻,但只能提纲挈领,不能让 ...
最新文章
- POJ 3320 Jessica's Reading Problem (尺取)
- shell-4.bash的变量:用户自定义变量
- smartforms不输出0
- html3D效果可以在手机打开吗,手机怎么打开HTML
- GitHub 超 3W Star,最受欢迎的 VS Code IDE 是如何炼成的?
- Docker之使用Dockerfile创建定制化镜像(四)--技术流ken
- 应用程序委托协议栈发送消息
- Android 引导页
- Gmaill和MSN 8.0备忘
- opencv的第一个lena图片显示
- 2021年危险化学品经营单位主要负责人考试技巧及危险化学品经营单位主要负责人模拟考试题库
- 【chrome】谷歌chrome浏览器离线安装包的获取及使用技巧
- FreeSwitch笔记
- Windows Internet Information Services(IIS) 与 inetpub 文件夹
- 如何用PS把照片变成红/白/蓝底
- 图解B+树并和B-树特点对比总结
- 分步傅里叶算法_快速分步傅里叶算法,split-step fast Fourier transform,音标,读音,翻译,英文例句,英语词典...
- LM335使用讲解之51单片机
- 69个办公室常用到的电脑快捷键,苦寻整理出来的,建议收藏一份!
- IM消息重试机制Java实现_IM群聊消息的已读回执功能该怎么实现?
热门文章
- 高效写毕设论文之EndNote
- PXE+VMware主机模式+KickStart脚本 自动安装ESXi 并试用WireShark抓包分析其中的协议TFTP,DHCP
- 类型多样的昆虫免抠元素素材,速来收藏
- GitHub Actions 实现 Azure Infra 资源的自动化部署
- 看哭马云张勇王坚的一段视频《唐小菊穿越记》
- python中文分词库_jieba分词-强大的Python 中文分词库
- Python matplotlib 绘制三维图并修改样式
- iOS 答题功能实现 —— HERO博客
- 构造Delaunay三角形网格
- MPC Multiparty Threshold ECDSA (GG18实现原理)