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 {
      //一些数据结构保存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 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的作用,顺带把滑动也过一遍。

    原文作者:chengkun_123
    原文地址: https://blog.csdn.net/chengkun_123/article/details/74990374
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞