RecyclerView 源码分析 --- remove动画源码分析

1,008 阅读6分钟

先来思考两个问题~

  • 为什么改变数据后调用notifyDataSetChanged不发生删除动画?调用notifyItemRemoved等notify这样的操作会发生动画?

  • 两次layout是为了什么?



删除动画分析

我们知道RecyclerView的动画执行和它的layout过程息息相关,研究动画的执行实际上也是在研究RecyclerView的布局layout过程~

先找到删除动画的触发入口(我们以DefaultAnimator为例来看动画的执行)

1.1 notifyItemRemoved开始

最后会调用到 triggerUpdateProcessor

        void triggerUpdateProcessor() {
            if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
                ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
            } else {
                mAdapterUpdateDuringMeasure = true;
                //布局触发动画,动画过程都发生在布局计算的时候
                requestLayout();
            }
        }

1.2 触发了绘制流程,去看onLayout过程

RecyclerView的布局分成了三个步骤。dispatchLayoutStep1 , dispatchLayoutStep2 , diapatchLayoutStep3

我们依次来分析这三个阶段都干了些什么不可告人的秘密~

1.3 dispatchLayoutStep1 这个方法只保留重点的地方来理解

    private void dispatchLayoutStep1() {
        
        // 1. 第一步 给ViewHolder设置flag
        processAdapterUpdatesAndSetAnimationFlags();
        ... ... 
        
        if (mState.mRunSimpleAnimations) {
            int count = mChildHelper.getChildCount();
            for (int i = 0; i < count; ++i) {
                final ItemHolderInfo animationInfo = mItemAnimator
                        .recordPreLayoutInformation(mState, holder,
                                ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                                holder.getUnmodifiedPayloads());
                                
                //2. 第二步 记录每个ViewHolder对象,它们即将进行preLayout
                mViewInfoStore.addToPreLayout(holder, animationInfo);
        ... ... ...
        if (mState.mRunPredictiveAnimations) {
            ... ... ...
            //3. 第三步 进行 preLayout 
            mLayout.onLayoutChildren(mRecycler, mState);
            ... ... ...

1.3.1 第一步 processAdapterUpdatesAndSetAnimationFlags()

这个方法流程稍为简化一下,关键看remove的动画过程:

private void processAdapterUpdatesAndSetAnimationFlags() {
    ... ... ...
    if (predictiveItemAnimationsEnabled()) {
        //核心步骤
        mAdapterHelper.preProcess();
    }
    ... ... ...

mAdapterHelper.preProcess

   void preProcess() {
        mOpReorderer.reorderOps(mPendingUpdates);
        final int count = mPendingUpdates.size();
        for (int i = 0; i < count; i++) {
            UpdateOp op = mPendingUpdates.get(i);
            switch (op.cmd) {
                case UpdateOp.ADD:
                    applyAdd(op);
                    break;
                case UpdateOp.REMOVE:
                    applyRemove(op);
                    break;
                case UpdateOp.UPDATE:
                    applyUpdate(op);
                    break;
                case UpdateOp.MOVE:
                    applyMove(op);
                    break;
            }
            if (mOnItemProcessedCallback != null) {
                mOnItemProcessedCallback.run();
            }
        }
        mPendingUpdates.clear();
    }

这里可以留一个小结论:

这个方法会消耗完所有的mPengindUpdates,这样就可以方便后面计算出所有ViewHolder的mPreLayoutPosition mPosition mOldPosition

继续跟到RecyclerView offsetPositionRecordsForRemove证实上面的所说

    //positionStart表示删除的起始地方 itemCount表示个数
    void offsetPositionRecordsForRemove(int positionStart, int itemCount,
            boolean applyToPreLayout) {
        final int positionEnd = positionStart + itemCount;
        final int childCount = mChildHelper.getUnfilteredChildCount();
        for (int i = 0; i < childCount; i++) {
            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
            if (holder != null && !holder.shouldIgnore()) {
            
                if (holder.mPosition >= positionEnd) {
                    //mPosition mPreLayoutPosition mOldPosition
                    //  位置改变的holder 重新计算位置
                    holder.offsetPosition(-itemCount, applyToPreLayout);
                    mState.mStructureChanged = true;
                } else if (holder.mPosition >= positionStart) {
                    // 给要删除的ViewHolder 添加一个Flag FLAG_REMOVE 
                    holder.flagRemovedAndOffsetPosition(positionStart - 1, -itemCount,
                            applyToPreLayout);
                    mState.mStructureChanged = true;
                }
            }
        }
        
        mRecycler.offsetPositionRecordsForRemove(positionStart, itemCount, applyToPreLayout);
        requestLayout();
    }

对于位置需要改变的ViewHolder们, offsetPosition方法重新计算了 mOldPosition mPreLayoutPosition mPosition

        void offsetPosition(int offset, boolean applyToPreLayout) {
            if (mOldPosition == NO_POSITION) {
                mOldPosition = mPosition;
            }
            if (mPreLayoutPosition == NO_POSITION) {
                mPreLayoutPosition = mPosition;
            }
            if (applyToPreLayout) {
                mPreLayoutPosition += offset;
            }
            mPosition += offset;
            ... ...
            

小结论

针对每次update(指的是mPendingUpdates里的元素),都会遍历所有的可能会被影响到的ViewHolder,重新计算preLayoutPostion mPostion(可以理解为postLayoutPosition)。 对于某些要被删除的ViewHolder,走flagRemovedAndOffsetPosition方法。所以,最后消耗完mPendingUpdates之后,我们就安排明白了所有ViewHolder的preLayoutPositionpostLayoutPosition

1.3.2 第二步 mViewInfoStore.addToPreLayout 所有的ViewHolder挨个记录

void addToPreLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
    InfoRecord record = mLayoutHolderMap.get(holder);
    if (record == null) {
        record = InfoRecord.obtain();
        mLayoutHolderMap.put(holder, record);
    }
    record.preInfo = info;   //record.preInfo
    record.flags |= FLAG_PRE;   // 设置flag
}

1.3.3 第三步 dispatchLayoutStep1 -> mLayout.onLayoutChildren

这是一次布局过程,我们称为preLayout,我理解它的目的是记录每个ViewHolder在未变化之前的位置信息,方便执行比如move动画时来确定动画的起始位置坐标,onLayoutChildren布局逻辑涉及到比如LinearLayoutManager的布局逻辑,ViewHolder复用逻辑等(不详细展开)

1.5 继续往下看dispatchLayoutStep2

可以看到disptchLayout2的代码明显要简洁了很多,这里也执行了一次布局,这次布局的不同之处在于,使用的是ViewHoldergetLayoutPosition返回的是mPosition。而preLayoutgetLayoutPosition返回的是mPreLayoutPosition,所以这次layout是根据实际数据进行的一次layout

    private void dispatchLayoutStep2() {
        startInterceptRequestLayout();
        ... ...
        //postLayout
        mLayout.onLayoutChildren(mRecycler, mState);
        ... ...
        stopInterceptRequestLayout(false);
    }

1.6 dispatchLayoutStep3 分析

需要注意两个地方,一个是会通过方法addToPostLayout把当前的ViewHolder当前的flags添加上 FLAG_POST

第二步是process真正触发动画的执行!

    private void dispatchLayoutStep3() {
        ... ... ...
        if (mState.mRunSimpleAnimations) {
            for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
                if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
                    //更新动画走这里
                    ... ... ...
                } else {
                    //1. 删除动画 走这里
                    mViewInfoStore.addToPostLayout(holder, animationInfo);
                }
            }
            //2.触发动画的执行
            mViewInfoStore.process(mViewInfoProcessCallback);
        }
    }
    

1.6.1 addToPostLayout

    void addToPostLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
        InfoRecord record = mLayoutHolderMap.get(holder);
        if (record == null) {
            record = InfoRecord.obtain();
            mLayoutHolderMap.put(holder, record);
        }
        record.postInfo = info;
        record.flags |= FLAG_POST;
    }

这里会有一个有意思的事情,我们知道在preLayout我们已经在mLayoutHolderMap放入了所有参与preLayout的ViewHolder信息,并且添加了FLAG_PRE。在这里我们又在mLayoutHolderMap添加了动画完毕之后的ViewHolder,并加上了FLAG_POST,这里就有几种情况:

  1. 只带有FLAG_PRE
  2. 只带有FLAG_POST
  3. 既带有FLAG_PRE,也有FLAG_POST

作用是什么呢? 带有FLAG_PRE说明参与了PRE_LAYOUT,如果没有带FLAG_POST,说明这个ViewHolder被删除了,要做删除动画。如果只带有FLAG_POST,但是没有带FLAG_PRE,说明是新增的,要做新增动画,如果两个FLAG都带,说明可能发生了移动动画,或者就是没动

1.6.2 删除动画执行过程

    void process(ProcessCallback callback) {
        for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
        
            ... ...

            } else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {  //move动画走这里
                // Persistent in both passes. Animate persistence
                callback.processPersistent(viewHolder, record.preInfo, record.postInfo);

            } else if ((record.flags & FLAG_PRE) != 0) {   // 删除动画
                // Was in pre-layout, never been added to post layout
                callback.processDisappeared(viewHolder, record.preInfo, null);

            } else if ((record.flags & FLAG_POST) != 0) {   //add动画
                // Was not in pre-layout, been added to post layout
                callback.processAppeared(viewHolder, record.preInfo, record.postInfo);

            ... ...
        }
    }

继续跟删除动画 mViewInfoProcessCallback processDisappeared

public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info,
        @Nullable ItemHolderInfo postInfo) {
        mRecycler.unscrapView(viewHolder);
        animateDisappearance(viewHolder, info, postInfo);
}

animateDisappearance

void animateDisappearance(@NonNull ViewHolder holder,
    @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
    ... ...
    // animateDisappearance 
    if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
        postAnimationRunner();
    }
}

SimpleItemAnimator animateDisappearance

    @Override
    public boolean animateDisappearance(@NonNull RecyclerView.ViewHolder viewHolder,
            @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {、
        ... ...
        if (!viewHolder.isRemoved() && (oldLeft != newLeft || oldTop != newTop)) {
            ... ...
        } else {
            
            return animateRemove(viewHolder);
        }
    }

DefaultItemAnimator animateRemove

public boolean animateRemove(final RecyclerView.ViewHolder holder) {
    resetAnimation(holder);
    //添加到 mPendingRemovals 等待触发
    mPendingRemovals.add(holder);
    return true;
}

DefaultItemAnimator runPendingAnimations


public void runPendingAnimations() {
        //remove 动画的ArrayList
        boolean removalsPending = !mPendingRemovals.isEmpty();
        //move 动画的ArrayList
        boolean movesPending = !mPendingMoves.isEmpty();
        //change 动画的ArrayList
        boolean changesPending = !mPendingChanges.isEmpty();
        //add 动画ArrayList
        boolean additionsPending = !mPendingAdditions.isEmpty();
        
        ... ...

        // First, remove stuff  优先删除动画
        for (RecyclerView.ViewHolder holder : mPendingRemovals) {
            animateRemoveImpl(holder);
        }
        mPendingRemovals.clear();
        // Next, move stuff   move动画
                ... ... 
                    for (MoveInfo moveInfo : moves) {
                        animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
                                moveInfo.toX, moveInfo.toY);
                    }
            ... ...
        }
        // Next, change stuff, to run in parallel with move animations 更新动画
                    ... ... 
                    for (ChangeInfo change : changes) {
                        animateChangeImpl(change);
                    }
            ... ...
        }
        // Next, add stuff 添加动画
                    ... ... 
                    for (RecyclerView.ViewHolder holder : additions) {
                        animateAddImpl(holder);
                    }
            ...
        }
    }

animateRemoveImpl 最后用属性动画实现删除动画

    private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
        ... ...
        animation.setDuration(getRemoveDuration()).alpha(0).setListener(
                new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationStart(Animator animator) {
                        ... ...
                    }

                    @Override
                    public void onAnimationEnd(Animator animator) {
                        ... ...
                    }
                }).start();
    }

总结

  1. 理解一下preLayout和postLayout.在RecyclerView执行动画时,会对RecyclerView执行两次layout操作.第一次我们称之为preLayout,计算的是数据未改变时的各个子View的状态.第二次称之为postLayout,这次布局使用的是变化了之后的数据,对RecyclerView进行一次重新layout.两次layout之后的目的是计算出所有的子View该做何种动画,动画的执行距离等.

  2. 删除动画主要是依靠layout过程来实现的,layout分两步来做,第一步preLayout,存储每个在RecyclerView上ViewHolder对象的信息,保存在record.preInfo中(mViewInfoStore.addToPreLayout);第二步,一般称为postLayout,进行动画完成后的布局计算,保存在record.postInfo里(mViewInfoStore.addToPostLayout),这两次绘制的信息也是动画计算的关键。

  3. 我们删除一个item的时候,执行这个动画的时候,有些ViewHolder是要被移除的,有的item是要被move的,有些是不要动的,我们如何判断呢?实际上是根据FLAG_PRE和FLAG_POST来判断,只有FLAG_PRE说明被删除了,只有FLAG_POST说明是新增的,两者都有可能是move动画。能被执行addToPreLayout的ItemInfo,都有FLAG_PRE,能被执行addToPostLayout的ItemInfo,都有FLAG_POST

  4. 关于preLayout和postLayout的意义,就是计算保存动画开始前和结束后的子view的位置信息,根据这些信息和设置的一些flag来计算执行何种动画,如果移动位置,可以用来计算移动多少距离。

  5. notifyDataSetChanged只是单纯的改变数据集合,然后重新绘制RecyclerView,并没有涉及到preLayout和postLayout。

  6. mChangedScrap用于更新动画中,记录的ViewHolder是需要重新绑定数据的。

  7. mPendingUpdates在preLayout之前就已经被全部consume了。