RecyclerView动画源码浅析

本文是RecyclerView源码分析系列第四篇文章,内容主要是基于前三篇文章来叙述的,因此在阅读之前推荐看一下前3篇文章:

RecylcerView的基本设计结构

RecyclerView的刷新机制

RecyclerView的复用机制

本文主要分析RecyclerView删除动画的实现原理,不同类型动画的大体实现流程其实都是差不多的,所以对于添加、交换这种动画就不再做分析。本文主要目标是理解清楚的是RecyclerViewItem删除动画源码实现逻辑。文章比较长。

可以通过下面这两个方法触发RecyclerView的删除动画:

    //一个item的删除动画
    dataSource.removeAt(1)
    recyclerView.adapter.notifyItemRemoved(1)

    //多个item的删除动画
    dataSource.removeAt(1)
    dataSource.removeAt(1)
    recyclerView.adapter.notifyItemRangeRemoved(1,2)

下面这个图是设置10倍动画时长时删除动画的执行效果,可以先预想一下这个动画时大致可以怎么实现:

RecyclerViewRemoveAnimation.gif

接下来就结合前面几篇文章的内容并跟随源码来一块看一下RecyclerView是如何实现这个动画的:

adapter.notifyItemRemoved(1)会回调到RecyclerViewDataObserver:

    public void onItemRangeRemoved(int positionStart, int itemCount) {
        if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {
            triggerUpdateProcessor();
        }
    }

其实按照onItemRangeRemoved()这个的执行逻辑方法可以将Item删除动画分为两个部分:

  1. 添加一个UpdateOpAdapterHelper.mPendingUpdates中。
  2. triggerUpdateProcessor()调用了requestLayout, 即触发了RecyclerView的重新布局。

先来看mAdapterHelper.onItemRangeRemoved(positionStart, itemCount):

AdapterHelper

这个类可以理解为是用来记录adapter.notifyXXX动作的,即每一个Operation(添加、删除)都会在这个类中有一个对应记录UpdateOpRecyclerView在布局时会检查这些UpdateOp,并做对应的操作。
mAdapterHelper.onItemRangeRemoved其实是添加一个Remove UpdateOp:

    mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount, null));
    mExistingUpdateTypes |= UpdateOp.REMOVE;

即把一个Remove UpdateOp添加到了mPendingUpdates集合中。

RecyclerView.layout

RecyclerView的刷新机制中知道RecyclerView的布局一共分为3分步骤:dispatchLayoutStep1()、dispatchLayoutStep2()、dispatchLayoutStep3(),接下来我们就分析这3步中有关Item删除动画的工作。

dispatchLayoutStep1(保存动画现场)

直接从dispatchLayoutStep1()开始看,这个方法是RecyclerView布局的第一步:

dispatchLayoutStep1():

    private void dispatchLayoutStep1() {
        ...
        processAdapterUpdatesAndSetAnimationFlags();
        ...
        if (mState.mRunSimpleAnimations) {
            ...
        }
        ...
    }

上面我只贴出了Item删除动画主要涉及到的部分, 先来看一下processAdapterUpdatesAndSetAnimationFlags()所触发的操作,整个操作链比较长,就不一一跟了,它最终其实是调用到AdapterHelper.postponeAndUpdateViewHolders():

private void postponeAndUpdateViewHolders(UpdateOp op) {
    mPostponedList.add(op); //op其实是从mPendingUpdates中取出来的
    switch (op.cmd) {
        case UpdateOp.ADD:
            mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount); break;
        case UpdateOp.MOVE:
            mCallback.offsetPositionsForMove(op.positionStart, op.itemCount); break;
        case UpdateOp.REMOVE:
            mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart, op.itemCount); break;  
        case UpdateOp.UPDATE:
            mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload); break;    
        ...
    }
}

即这个方法做的事情就是把mPendingUpdates中的UpdateOp添加到mPostponedList中,并回调根据op.cmd来回调mCallback,其实这个mCallback是回调到了RecyclerView中:

 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.mPosition >= positionEnd) {
                holder.offsetPosition(-itemCount, applyToPreLayout);
                mState.mStructureChanged = true;
            }
            ...
        }
        ...
    }

offsetPositionRecordsForRemove方法:主要是把当前显示在界面上的ViewHolder的位置做对应的改变,即如果item位于删除的item之后,那么它的位置应该减一,比如原来的位置是3现在变成了2。

接下来继续看dispatchLayoutStep1()中的操作:

    if (mState.mRunSimpleAnimations) {
        int count = mChildHelper.getChildCount();
        for (int i = 0; i < count; ++i) {
            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
            //根据当前的显示在界面上的ViewHolder的布局信息创建一个ItemHolderInfo
            final ItemHolderInfo animationInfo = mItemAnimator
                    .recordPreLayoutInformation(mState, holder,
                            ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                            holder.getUnmodifiedPayloads());
            mViewInfoStore.addToPreLayout(holder, animationInfo); //把 holder对应的animationInfo保存到 mViewInfoStore中
            ...
        }
    }

即就做了两件事:

  1. 为当前显示在界面上的每一个ViewHolder创建一个ItemHolderInfoItemHolderInfo其实就是保存了当前显示itemview的布局的top、left等信息
  2. 拿着ViewHolder和其对应的ItemHolderInfo调用mViewInfoStore.addToPreLayout(holder, animationInfo)

mViewInfoStore.addToPreLayout()就是把这些信息保存起来:

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.flags |= FLAG_PRE;
}

即把holder 和 info保存到mLayoutHolderMap中。可以理解为它是用来保存动画执行前当前界面ViewHolder的信息一个集合。

到这里大致理完了在执行Items删除动画AdapterHelperdispatchLayoutStep1()的执行逻辑,这里用一张图来总结一下:

Remove动画dispatchLayoutStep1.png

其实这些操作可以简单的理解为保存动画前View的现场 。其实这里有一次预布局,预布局也是为了保存动画前的View信息,不过这里就不讲了。

dispatchLayoutStep2

这一步就是摆放当前adapter中剩余的Item,在本文的例子中,就是依次摆放剩余的5个Item。在前面的文章RecyclerView的刷新机制中,我们知道LinearLayoutManager会向RecyclerView来填充RecyclerView,所以RecyclerView中填几个View,其实和Recycler有很大的关系,因为Recycler不给LinearLayoutManager的话,RecyclerView中就不会有View填充。那RecyclerLinearLayoutManager``View的边界条件是什么呢?
我们来看一下tryGetViewHolderForPositionByDeadline()方法:

ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
        if (position < 0 || position >= mState.getItemCount()) {
            throw new IndexOutOfBoundsException("Invalid item position " + position
                    + "(" + position + "). Item count:" + mState.getItemCount()
                    + exceptionLabel());
        }
}

即如果位置大于mState.getItemCount(),那么就不会再向RecyclerView中填充子View。而这个mState.getItemCount()一般就是adapter中当前数据源的数量。所以经过这一步布局后,View的状态如下图:

Remove动画布局.png

这时你可能就有疑问了? 动画呢? 怎么直接成最终的模样了?别急,这一步只不过是布局,至于动画是怎么执行的我们继续往下看:

dispatchLayoutStep3(执行删除动画)

在上一步中对删除操作已经完成了布局,接下来dispatchLayoutStep3()就会做删除动画:

private void dispatchLayoutStep3() {
    ...
    if (mState.mRunSimpleAnimations) {
        ...
        mViewInfoStore.process(mViewInfoProcessCallback); //触发动画的执行
    }
    ...
}

可以看到主要涉及到动画的是mViewInfoStore.process(), 其实这一步可以分为两个操作:

  1. 先把Item View动画前的起始状态准备好
  2. 执行动画使Item View到目标布局位置

下面我们来继续跟一下mViewInfoStore.process()这个方法

Item View动画前的起始状态准备好

 void process(ProcessCallback callback) {
        for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) { //对mLayoutHolderMap中每一个Holder执行动画
            final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
            final InfoRecord record = mLayoutHolderMap.removeAt(index);
            if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
                callback.unused(viewHolder);
            } else if ((record.flags & FLAG_DISAPPEARED) != 0) {
                callback.processDisappeared(viewHolder, record.preInfo, record.postInfo);  //被删除的那个item会回调到这个地方
            }else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
                callback.processPersistent(viewHolder, record.preInfo, record.postInfo);   //需要上移的item会回调到这个地方
            }  
            ...
            InfoRecord.recycle(record);
        }
    }

这一步就是遍历mLayoutHolderMap对其中的每一个ViewHolder做对应的动画。这里callback会调到了RecyclerView,RecyclerView会对每一个Item执行相应的动画:

ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
        new ViewInfoStore.ProcessCallback() {
            @Override
            public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info,@Nullable ItemHolderInfo postInfo) {
                mRecycler.unscrapView(viewHolder);   //从scrap集合中移除,
                animateDisappearance(viewHolder, info, postInfo);
            }

            @Override
            public void processPersistent(ViewHolder viewHolder, @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
                ...
                if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) {
                    postAnimationRunner();
                }
            }
            ...
        }
}

先来分析被删除那那个Item的消失动画:

将Item的动画消失动画放入到mPendingRemovals待执行队列

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

先把Holderattch到RecyclerView上(这是因为在dispatchLayoutStep1dispatchLayoutStep2中已经对这个Holder做了Dettach)。即它又重新出现在了RecyclerView的布局中(位置当然还是未删除前的位置)。然后调用了mItemAnimator.animateDisappearance()其执行这个删除动画,mItemAnimatorRecyclerView的动画实现者,它对应的是DefaultItemAnimator。继续看animateDisappearance()它其实最终调用到了DefaultItemAnimator.animateRemove():

public boolean animateRemove(final RecyclerView.ViewHolder holder) {
    resetAnimation(holder);
    mPendingRemovals.add(holder);
    return true;
}

即,其实并没有执行动画,而是把这个holder放入了mPendingRemovals集合中,看样是要等下执行。

将未被删除的Item的移动动画放入到mPendingMoves待执行队列

其实逻辑和上面差不多DefaultItemAnimator.animatePersistence():

public boolean animatePersistence(@NonNull RecyclerView.ViewHolder viewHolder,@NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
    if (preInfo.left != postInfo.left || preInfo.top != postInfo.top) {  //和预布局的状态不同,则执行move动画
        return animateMove(viewHolder,preInfo.left, preInfo.top, postInfo.left, postInfo.top);
    }
    ...
}

animateMove的逻辑也很简单,就是根据偏移构造了一个MoveInfo然后添加到mPendingMoves中,也没有立刻执行:

public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
    final View view = holder.itemView;
    fromX += (int) holder.itemView.getTranslationX();
    fromY += (int) holder.itemView.getTranslationY();
    resetAnimation(holder);
    int deltaX = toX - fromX;
    int deltaY = toY - fromY;
    if (deltaX == 0 && deltaY == 0) {
        dispatchMoveFinished(holder);
        return false;
    }
    if (deltaX != 0) {
        view.setTranslationX(-deltaX);  //设置他们的位置为负偏移!!!!!
    }
    if (deltaY != 0) {
        view.setTranslationY(-deltaY);  //设置他们的位置为负偏移!!!!!
    }
    mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
    return true;
}

但要注意这一步把要做滚动动画的View的TranslationXTranslationY都设置负的被删除的Item的高度,如下图

DefaultItemAnimator.animateMove.png

即被删除的Item之后的Item都下移了

postAnimationRunner()执行所有的pending动画

上面一步操作已经把动画前的状态准备好了,postAnimationRunner()就是将上面pendding的动画开始执行:

//DefaultItemAnimator.java

    public void runPendingAnimations() {
        boolean removalsPending = !mPendingRemovals.isEmpty();
        ...
        for (RecyclerView.ViewHolder holder : mPendingRemovals) {
            animateRemoveImpl(holder); //执行pending的删除动画
        }
        mPendingRemovals.clear();

        if (!mPendingMoves.isEmpty()) { //执行pending的move动画
            final ArrayList<MoveInfo> moves = new ArrayList<>();
            moves.addAll(mPendingMoves);
            mMovesList.add(moves);
            mPendingMoves.clear();
            Runnable mover = new Runnable() {
                @Override
                public void run() {
                    for (MoveInfo moveInfo : moves) {
                        animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
                                moveInfo.toX, moveInfo.toY);
                    }
                    moves.clear();
                    mMovesList.remove(moves);
                }
            };
            if (removalsPending) {
                View view = moves.get(0).holder.itemView;
                ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
            } else {
                mover.run();
            }
        }
        ...
    }

至于animateRemoveImplanimateMoveImpl的源码具体我就不贴了,直接说一下它们做了什么操作吧:

  1. animateRemoveImpl 把这个被Remove的Item做一个透明度由(1~0)的动画
  2. animateMoveImpl把它们的TranslationXTranslationY移动到0的位置。

我再贴一下删除动画的gif, 你感受一下是不是这个执行步骤:

RecyclerViewRemoveAnimation.gif

欢迎关注我的Android进阶计划。看更多干货

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容