您的位置:首页 > 其它

RecyclerView源码浅析之数据更改与动画

2017-07-11 17:54 393 查看

概述

上一篇我们针对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 {
//一些数据结构保存UpdateOp
private 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
f8cf
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 MOVE
int 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的一个内部类
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
assertNotInLayoutOrScroll(null);
//添加UpdateOp
if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {
//申请重绘
triggerUpdateProcessor();
}
}


把REMOVE的UpdateOp添加到mPendingUpdates。

//AdapterHelper.java
boolean 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
@VisibleForTesting
final ArrayMap<ViewHolder, InfoRecord> mLayoutHolderMap = new ArrayMap<>();
@VisibleForTesting
final 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 info
static final int FLAG_PRE = 1 << 2;
// post layout, this is necessary to distinguish null item info
static 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.java
public static class ItemHolderInfo {
public int left;
public int top;
public int right;
public int bottom;

@AdapterChanges
public 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就是用的mPrelayoutPosition
findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);

if (mState.mRunSimpleAnimations) {
//注意这里获取的是可见的View
int 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把剩余的更新操作映射给VH
mAdapterHelper.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的作用,顺带把滑动也过一遍。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: