RecyclerView 动画原理 | 如何存储并应用动画属性值?

简介: RecyclerView 动画原理 | 如何存储并应用动画属性值?

RecyclerView 表项动画的属性值是怎么获取的,又存储在哪里?这一篇继续通过 走查源码 的方式解答这个疑问。


通过上两篇的分析得知,为了做动画 RecyclerView 会布局两次:预布局+后布局,依次将动画前与动画后的表项填充到列表。表项被填充后,就确定了它相对于 RecyclerView 左上角的位置,在两次布局过程中,这些位置信息是如何被保存的?


这是 RecyclerView 动画原理的第三篇,系列文章目录如下:


  1. RecyclerView 动画原理 | 换个姿势看源码(pre-layout)


  1. RecyclerView 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系


  1. RecyclerView 动画原理 | 如何存储并应用动画属性值?


引子


这一篇源码分析还是基于下面这个 Demo 场景:


image.png


https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bc5dd76ef2d54998b7e95bcc294c71c2~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp


列表中有两个表项(1、2),删除 2,此时 3 会从屏幕底部平滑地移入并占据原来 2 的位置。


为了实现该效果,RecyclerView的策略是:为动画前的表项先执行一次pre-layout,将不可见的表项 3 也加载到布局中,形成一张布局快照(1、2、3)。再为动画后的表项执行一次post-layout,同样形成一张布局快照(1、3)。比对两张快照中表项 3 的位置,就知道它该如何做动画了。


在此援引上一篇已经得出的结论:


  1. RecyclerView为了实现表项动画,进行了 2 次布局(预布局 + 后布局),在源码上表现为LayoutManager.onLayoutChildren()被调用 2 次。


  1. State.mInPreLayout用于标识是否在预布局阶段。预布局的生命周期始于RecyclerView.dispatchLayoutStep1(),终于RecyclerView.dispatchLayoutStep2()


  1. 在预布局阶段,循环填充表项时,若遇到被移除的表项,则会忽略它占用的空间,多余空间被用来加载额外的表项,这些表项在屏幕之外,本来不会被加载。


其中第三点表现在源码上,是这样的:


public class LinearLayoutManager {
    // 布局表项
    public void onLayoutChildren() {
        // 不断填充表项
        fill() {
            while(列表有剩余空间){
                // 填充单个表项
                layoutChunk(){
                    // 让表项成为子视图
                    addView(view)
                }
                if (表项没有被移除) {
                    剩余空间 -= 表项占用空间  
                }
                ...
            }
        }
    }
}


这是RecyclerView填充表项的伪码。以 Demo 为例,预布局阶段,第一次执行onLayoutChildren(),因表项 2 被删除,所以它占用的空间不会被扣除,导致while循环多执行一次,这样表项 3 就被填充进列表。


存后布局动画属性值


RecyclerView用一个 Int 值mLayoutStep标记布局阶段,它有三种可能的取值。


public class RecyclerView {
    public static class State {
        static final int STEP_START = 1;
        static final int STEP_LAYOUT = 1 << 1;
        static final int STEP_ANIMATIONS = 1 << 2; // 布局的动画阶段
        int mLayoutStep = STEP_START; // 当前布局阶段
    }
}


只要全局查找下mLayoutStep什么时候被赋值为STEP_ANIMATIONS,就可以知道表项动画什么时候开始:


public class RecyclerView {
    final State mState = new State();
    private void dispatchLayoutStep2() {// 布局子表项第二阶段
        mState.mInPreLayout = false; // 预布局结束
        mLayout.onLayoutChildren(mRecycler, mState); // 开始后布局
        mState.mLayoutStep = State.STEP_ANIMATIONS; // 标记为布局的动画阶段
        ...
    }
}


RecyclerView 在后布局结束后,将mState.mLayoutStep置为State.STEP_ANIMATIONS,表示表项动画即将开始。


在紧接着的“布局子表项第三阶段”的开头,就断言:


public class RecyclerView {
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        ...
        dispatchLayout();// 开始布局 RecyclerView 的子表项
        ...
    }
    void dispatchLayout() {
        ...
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();// 布局子表项第一阶段
            ...
            dispatchLayoutStep2(); // 布局子表项第二阶段
        }
        ...
        dispatchLayoutStep3(); // 布局子表项第三阶段
    }
    private void dispatchLayoutStep3() {
        // 断言“在布局的动画阶段”
        mState.assertLayoutStep(State.STEP_ANIMATIONS);
        ...
    }
    public static class State {
        // 断言 mLayoutStep 是否为 accepted,否则抛异常
        void assertLayoutStep(int accepted) {
            if ((accepted & mLayoutStep) == 0) {
                throw new IllegalStateException("Layout state should be one of "
                        + Integer.toBinaryString(accepted) + " but it is "
                        + Integer.toBinaryString(mLayoutStep));
            }
        }
    }
}


由此可以断定,触发动画的逻辑将会出现在RecyclerView.dispatchLayoutStep3()中,继续往下走读源码:


public class RecyclerView {
    private void dispatchLayoutStep3() {
            // 遍历表项
            for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
                // 获取表项对应 ViewHolder
                ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
                // 获取表项动画信息
                final ItemHolderInfo animationInfo = mItemAnimator.recordPostLayoutInformation(mState, holder);
                ...
            }
    }                        
}


RecyclerView 在布局子表项的第三阶段中遍历了当前所有的表项(对于 Demo 场景,会遍历表项 1、3),调用ItemAnimator.recordPostLayoutInformation()逐个构建表项动画信息ItemHolderInfo


public class RecyclerView {
    public abstract static class ItemAnimator {
        // 记录后布局信息
        public ItemHolderInfo recordPostLayoutInformation(State state,ViewHolder viewHolder) {
            return obtainHolderInfo().setFrom(viewHolder);
        }
        // 构建表项信息
        public ItemHolderInfo obtainHolderInfo() {
            return new ItemHolderInfo();
        }
        // 表项信息实体类
        public static class ItemHolderInfo {
            // 上下左右相对于列表的距离
            public int left;
            public int top;
            public int right;
            public int bottom;
            public ItemHolderInfo setFrom(RecyclerView.ViewHolder holder) {
                return setFrom(holder, 0);
            }
            // 记录表项位置
            public ItemHolderInfo setFrom(RecyclerView.ViewHolder holder,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;
            }
        }
    }
}


构建的ItemHolderInfo实例记录了表项相对于列表左上角的位置(上下左右),然后调用addToPostLayout()将其添加到ViewInfoStore


public class RecyclerView {
    // 用于存放表项动画信息
    final ViewInfoStore mViewInfoStore = new ViewInfoStore();
    private void dispatchLayoutStep3() {
            // 遍历表项
            for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
                ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
                final ItemHolderInfo animationInfo = mItemAnimator.recordPostLayoutInformation(mState, holder);
                // 将后布局表项动画信息保存到 mViewInfoStore
                mViewInfoStore.addToPostLayout(holder, animationInfo);
                ...
            }
    }                        
}


ViewInfoStore专门用于存放表项动画信息:


class ViewInfoStore {
    // 存放 ViewHolder 与其对应动画信息 的 ArrayMap 结构
    final ArrayMap<RecyclerView.ViewHolder, InfoRecord> mLayoutHolderMap = new ArrayMap<>();
    // 存储后布局表项与其动画信息
    void addToPostLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
        InfoRecord record = mLayoutHolderMap.get(holder);
        if (record == null) {
            // 从池中获取 InfoRecord 实例
            record = InfoRecord.obtain();
            // 将 ViewHolder 和 InfoRecord 绑定
            mLayoutHolderMap.put(holder, record);
        }
        record.postInfo = info; // 将后布局表项动画信息存储在 postInfo 字段中
        record.flags |= FLAG_POST; // 追加 FLAG_POST 到标志位
    }
    static class InfoRecord {
        int flags; // 标记位
        static final int FLAG_PRE = 1 << 2; // pre-layout 标记
        static final int FLAG_POST = 1 << 3; // post-layout 标记
        static final int FLAG_APPEAR = 1 << 1; // 表项出现标志
        RecyclerView.ItemAnimator.ItemHolderInfo preInfo;// pre-layout 表项位置信息
        RecyclerView.ItemAnimator.ItemHolderInfo postInfo;// post-layout 表项位置信息
        // 池:为避免内存抖动
        static Pools.Pool<InfoRecord> sPool = new Pools.SimplePool<>(20);
        // 从池中获取 InfoRecord 实例
        static InfoRecord obtain() {
            InfoRecord record = sPool.acquire();
            return record == null ? new InfoRecord() : record;
        }
        ...
    }
}


表项动画信息被包装成InfoRecord实例并用一个int类型的标志位来标识表项经历过哪些布局阶段。若表项动画信息是在 post-layout 阶段被添加的,其标志位会追加FLAG_POST(该标记位用于判断做什么类型的动画)。最后将表项动画信息和对应的 ViewHolder 相互绑定并存储到 ArrayMap 结构中。


至此可以得出如下结论:


RecyclerView 在布局的第三个阶段会遍历后布局中填充的所有表项,为每个表项构建动画信息实例,该实例不仅保存了表项与列表的相对位置,还用一个标记位记录了表项经历过的布局阶段,并将表项与其动画信息的对应关系存储在ViewInfoStore中的mLayoutHolderMap结构中。


将结论应用到 Demo 的场景中:列表在布局第三阶段,会遍历表项 1、3,为它们构建动画信息实例,该实例的标志位被追加了FLAG_POST标志。这些信息都被存储在ViewInfoStore中的mLayoutHolderMap结构中。


存预布局动画属性值


InfoRecord中除了postInfo还有一个preInfo,分别表示后布局和预布局表项的动画信息。想必还有一个addToPreLayout()addToPostLayout()对应:


class ViewInfoStore {
    // 存储预布局表项与其动画信息
    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; // 将后布局表项动画信息存储在 preInfo 字段中
        record.flags |= FLAG_PRE; // 追加 FLAG_PRE 到标志位
    }
}


addToPreLayout()在预布局阶段被调用:


public class RecyclerView {
    private void dispatchLayoutStep1() {
            ...
            // 遍历可见表项
            int count = mChildHelper.getChildCount();
            for (int i = 0; i < count; ++i) {
                final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
                ...
                // 构建表项动画信息
                final ItemHolderInfo animationInfo = mItemAnimator
                        .recordPreLayoutInformation(mState, holder,
                                ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                                holder.getUnmodifiedPayloads());
                // 将表项动画信息保存到 mViewInfoStore
                mViewInfoStore.addToPreLayout(holder, animationInfo);
                ...
            }
            ...
            // 预布局
            mLayout.onLayoutChildren(mRecycler, mState);
    }
}


RecyclerView 布局的第一个阶段中,在第一次执行onLayoutChildren()之前,即预布局之前,遍历了所有的表项并逐个构建动画信息。以 Demo 为例,预布局之前,表项 1、2 的动画信息被构建并且标志位追加了FLAG_PRE,这些信息都被保存到mViewInfoStore实例中。


紧接着RecyclerView执行了onLayoutChildren(),即进行预布局。


public class RecyclerView {
    private void dispatchLayoutStep1() {
            // 遍历预布局前所有表项
            int count = mChildHelper.getChildCount();
            for (int i = 0; i < count; ++i) {
                final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
                ...
                final ItemHolderInfo animationInfo = mItemAnimator
                        .recordPreLayoutInformation(mState, holder,
                                ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                                holder.getUnmodifiedPayloads());
                mViewInfoStore.addToPreLayout(holder, animationInfo);
                ...
            }
            ...
            // 预布局
            mLayout.onLayoutChildren(mRecycler, mState);
            // 遍历预布局之后所有的表项
            for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
                final View child = mChildHelper.getChildAt(i);
                final ViewHolder viewHolder = getChildViewHolderInt(child);
                ...
                // 如果 ViewInfoStore 中没有对应的 ViewHolder 信息
                if (!mViewInfoStore.isInPreLayout(viewHolder)) {
                    ...
                    // 构建表项动画信息
                    final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
                    ...
                    // 将表项 ViewHolder 和其动画信息绑定并保存在 mViewInfoStore 中
                    mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
                }
            }
    }
}


RecyclerView 在预布局之后再次遍历了所有表项。因为预布局会把表项 3 也填充到列表中,所以表项 3 的动画信息也会被存入mViewInfoStore,不过调用的是ViewInfoStore.addToAppearedInPreLayoutHolders()


class ViewInfoStore {
    void addToAppearedInPreLayoutHolders(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
        InfoRecord record = mLayoutHolderMap.get(holder);
        if (record == null) {
            record = InfoRecord.obtain();
            mLayoutHolderMap.put(holder, record);
        }
        record.flags |= FLAG_APPEAR; // 追加 FLAG_APPEAR 到标志位
        record.preInfo = info; // 将预布局表项动画信息存储在 preInfo 字段中
    }
}


addToAppearedInPreLayoutHolders()addToPreLayout()的实现几乎一摸一样,唯一的不同是,标志位追加了FLAG_APPEAR,用于标记表项 3 是即将出现在屏幕中的表项。


分析至此,可以得出下面的结论:


RecyclerView 经历了预布局、后布局及布局第三阶段后,ViewInfoStore中就记录了每一个参与动画表项的三重信息:预布局位置信息 + 后布局位置信息 + 经历过的布局阶段。


以 Demo 为例,表项 1、2、3 的预布局和后布局位置信息都被记录在ViewInfoStore中,其中表项 1 在预布局和后布局中均出现了,所以标志位中包含了FLAG_PRE | FLAG_POSTInfoRecord中用一个新的常量表示了这种状态FLAG_PRE_AND_POST


class ViewInfoStore {
    static class InfoRecord {
        static final int FLAG_PRE = 1 << 2;
        static final int FLAG_POST = 1 << 3;
        static final int FLAG_PRE_AND_POST = FLAG_PRE | FLAG_POST;
    }
}


而表项 2 只出现在预布局阶段,所以标志位仅包含了FLAG_PRE。表项 3 出现在预布局之后及后布局中,所以标志位中包含了FLAG_APPEAR | FLAG_POST


应用动画属性值


public class RecyclerView {
    private void dispatchLayoutStep3() {
            // 遍历后布局表项并构建动画信息再存储到 mViewInfoStore
            for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
                ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
                long key = getChangedHolderKey(holder);
                final ItemHolderInfo animationInfo = mItemAnimator.recordPostLayoutInformation(mState, holder);
                ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
                 mViewInfoStore.addToPostLayout(holder, animationInfo);
            }
            // 触发表项执行动画
            mViewInfoStore.process(mViewInfoProcessCallback);
            ...
    }
}


RecyclerView 布局的第三个阶段中,在遍历完后布局表项后,调用了mViewInfoStore.process(mViewInfoProcessCallback)来触发表项执行动画:


class ViewInfoStore {
    void process(ProcessCallback callback) {
        // 遍历所有参与动画表项的位置信息
        for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
            // 获取表项 ViewHolder
            final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
            // 获取与 ViewHolder 对应的动画信息
            final InfoRecord record = mLayoutHolderMap.removeAt(index);
            // 根据动画信息的标志位确定动画类型以执行对应的 ProcessCallback 回调
            if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
                callback.unused(viewHolder);
            } else if ((record.flags & FLAG_DISAPPEARED) != 0) {
                if (record.preInfo == null) {
                    callback.unused(viewHolder);
                } else {
                    callback.processDisappeared(viewHolder, record.preInfo, record.postInfo);
                }
            } else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) {
                callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
            } else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
                callback.processPersistent(viewHolder, record.preInfo, record.postInfo);// 保持
            } else if ((record.flags & FLAG_PRE) != 0) {
                callback.processDisappeared(viewHolder, record.preInfo, null); // 消失动画
            } else if ((record.flags & FLAG_POST) != 0) {
                callback.processAppeared(viewHolder, record.preInfo, record.postInfo);// 出现动画
            } else if ((record.flags & FLAG_APPEAR) != 0) {
            }
            // 回收动画信息实例到池中
            InfoRecord.recycle(record);
        }
    }
}


ViewInfoStore.process()中遍历了包含所有表项动画信息的mLayoutHolderMap结构,并根据每个表项的标志位来确定执行的动画类型:


  • 表项 1 的标志位为FLAG_PRE_AND_POST所以会命中callback.processPersistent()


  • 表项 2 的标志位中只包含FLAG_PRE,所以(record.flags & FLAG_PRE) != 0成立,callback.processDisappeared()会命中。


  • 表项 3 的标志位中只包含FLAG_APPEAR | FLAG_POST,所以(record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST不成立,而(record.flags & FLAG_POST) != 0成立,callback.processAppeared()会命中。


作为参数传入ViewInfoStore.process()ProcessCallback是 RecyclerView 中预定义的动画回调:


class ViewInfoStore {
    // 动画回调
    interface ProcessCallback {
        // 消失动画
        void processDisappeared(RecyclerView.ViewHolder viewHolder, RecyclerView.ItemAnimator.ItemHolderInfo preInfo,RecyclerView.ItemAnimator.ItemHolderInfo postInfo);
        // 出现动画
        void processAppeared(RecyclerView.ViewHolder viewHolder, RecyclerView.ItemAnimator.ItemHolderInfo preInfo,RecyclerView.ItemAnimator.ItemHolderInfo postInfo);
        ...
    }
}
public class RecyclerView {
    // RecyclerView 动画回调默认实现
    private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
            new ViewInfoStore.ProcessCallback() {
                @Override
                public void processDisappeared(ViewHolder viewHolder, ItemHolderInfo info, ItemHolderInfo postInfo) {
                    mRecycler.unscrapView(viewHolder);
                    animateDisappearance(viewHolder, info, postInfo);//消失动画
                }
                @Override
                public void processAppeared(ViewHolder viewHolder,ItemHolderInfo preInfo, ItemHolderInfo info) {
                    animateAppearance(viewHolder, preInfo, info);//出现动画
                }
                ...
            };
    // 表项动画执行器
    ItemAnimator mItemAnimator = new DefaultItemAnimator();
    // 出现动画
    void animateAppearance(@NonNull ViewHolder itemHolder,ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) {
        itemHolder.setIsRecyclable(false);
        if (mItemAnimator.animateAppearance(itemHolder, preLayoutInfo, postLayoutInfo)) {
            postAnimationRunner();
        }
    }
    // 消失动画
    void animateDisappearance(@NonNull ViewHolder holder,ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) {
        addAnimatingView(holder);
        holder.setIsRecyclable(false);
        if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
            postAnimationRunner();
        }
    }
}


RecyclerView 执行表项动画的代码结构如下:


if (mItemAnimator.animateXXX(holder, preLayoutInfo, postLayoutInfo)) {
    postAnimationRunner();
}


根据ItemAnimator.animateXXX()的返回值来决定是否要在下一帧执行动画,以 Demo 中表项 3 的出现动画为例:


public abstract class SimpleItemAnimator extends RecyclerView.ItemAnimator {
    @Override
    public boolean animateAppearance(RecyclerView.ViewHolder viewHolder,ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) {
        // 如果预布局和后布局中表项左上角的坐标有变化 则执行位移动画
        if (preLayoutInfo != null 
            && (preLayoutInfo.left != postLayoutInfo.left || preLayoutInfo.top != postLayoutInfo.top)) {
            // 执行位移动画,并传入动画起点坐标(预布局表项左上角坐标)和终点坐标(后布局表项左上角坐标)
            return animateMove(viewHolder, 
                    preLayoutInfo.left, 
                    preLayoutInfo.top,
                    postLayoutInfo.left, 
                    postLayoutInfo.top);
        } else {
            return animateAdd(viewHolder);
        }
    }
}


之前存储的表项位置信息,终于在这里被用上了,它作为参数传入animateMove(),这是一个定义在SimpleItemAnimator中的抽象方法,DefaultItemAnimator实现了它:


public class DefaultItemAnimator extends SimpleItemAnimator {
    @Override
    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);
        }
        // 将待移动的表项动画包装成 MoveInfo 并存入 mPendingMoves 列表
        mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
        // 表示在下一帧执行动画
        return true;
    }
}


如果水平或垂直方向的位移增量不为 0,则将待移动的表项动画包装成MoveInfo并存入mPendingMoves列表,然后返回 true,表示在下一帧执行动画:


public class RecyclerView {  
    // 出现动画
    void animateAppearance(ViewHolder itemHolder,ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) {
        itemHolder.setIsRecyclable(false);
        if (mItemAnimator.animateAppearance(itemHolder, preLayoutInfo, postLayoutInfo)) {
            postAnimationRunner();// 触发动画执行
        }
    }
    // 将动画执行代码抛到 Choreographer 中的动画队列中
    void postAnimationRunner() {
        if (!mPostedAnimatorRunner && mIsAttached) {
            ViewCompat.postOnAnimation(this, mItemAnimatorRunner);
            mPostedAnimatorRunner = true;
        }
    }
    // 动画执行代码
    private Runnable mItemAnimatorRunner = new Runnable() {
        @Override
        public void run() {
            if (mItemAnimator != null) {
                // 在下一帧执行动画
                mItemAnimator.runPendingAnimations();
            }
            mPostedAnimatorRunner = false;
        }
    };
}


通过将一个Runnable抛到Choreographer的动画队列中来触发动画执行,当下一个垂直同步信号到来时,Choreographer会从动画队列中获取待执行的Runnable实例,并将其抛到主线程执行(关于Choreographer的详细解析可以点击读源码长知识 | Android卡顿真的是因为”掉帧“?)。执行的内容定义在ItemAnimator.runPendingAnimations()中:


public class DefaultItemAnimator extends SimpleItemAnimator {
    @Override
    public void runPendingAnimations() {
        // 如果位移动画列表不空,则表示有待执行的位移动画
        boolean movesPending = !mPendingMoves.isEmpty();
        // 是否有待执行的删除动画
        boolean removalsPending = !mPendingRemovals.isEmpty();
        ...
        // 处理位移动画
        if (movesPending) {
            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();
            }
        }
        ...
    }
}


遍历mPendingMoves列表,为每一个待执行的位移动画调用animateMoveImpl()构建动画:


public class DefaultItemAnimator extends SimpleItemAnimator {
    void animateMoveImpl(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
        final View view = holder.itemView;
        final int deltaX = toX - fromX;
        final int deltaY = toY - fromY;
        if (deltaX != 0) {
            view.animate().translationX(0);
        }
        if (deltaY != 0) {
            view.animate().translationY(0);
        }
        // 获取动画实例
        final ViewPropertyAnimator animation = view.animate();
        mMoveAnimations.add(holder);
        // 设置动画参数并启动
        animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animator) {
                dispatchMoveStarting(holder);
            }
            @Override
            public void onAnimationCancel(Animator animator) {
                if (deltaX != 0) {
                    view.setTranslationX(0);
                }
                if (deltaY != 0) {
                    view.setTranslationY(0);
                }
            }
            @Override
            public void onAnimationEnd(Animator animator) {
                animation.setListener(null);
                dispatchMoveFinished(holder);
                mMoveAnimations.remove(holder);
                dispatchFinishedWhenDone();
            }
        }).start();
    }
}


原来默认的表项动画是通过ViewPropertyAnimator实现的。


总结


  1. RecyclerView 将表项动画数据封装了两层,依次是ItemHolderInfoInfoRecord,它们记录了列表预布局和后布局表项的位置信息,即表项矩形区域与列表左上角的相对位置,它还用一个int类型的标志位来记录表项经历了哪些布局阶段,以判断表项应该做的动画类型(出现,消失,保持)。


  1. InfoRecord被集中存放在一个商店类ViewInfoStore中。所有参与动画的表项的ViewHolderInfoRecord都会以键值对的形式存储其中。


  1. RecyclerView 在布局的第三阶段会遍历商店类中所有的键值对,以InfoRecord中的标志位为依据,判断执行哪种动画。表项预布局和后布局的位置信息会一并传递给RecyclerView.ItemAnimator,以触发动画。


  1. RecyclerView.ItemAnimator收到动画指令和数据后,又将他们封装为MoveInfo,不同类型的动画被存储在不同的MoveInfo列表中。然后将执行动画的逻辑抛到 Choreographer 的动画队列中,当下一个垂直同步信号到来时,Choreographer 从动画队列中取出并执行表项动画,执行动画即遍历所有的MoveInfo列表,为每一个MoveInfo构建 ViewPropertyAnimator 实例并启动动画。


推荐阅读


RecyclerView 系列文章目录如下:


  1. RecyclerView 缓存机制 | 如何复用表项?


  1. RecyclerView 缓存机制 | 回收些什么?


  1. RecyclerView 缓存机制 | 回收到哪去?


  1. RecyclerView缓存机制 | scrap view 的生命周期


  1. 读源码长知识 | 更好的RecyclerView点击监听器


  1. 代理模式应用 | 每当为 RecyclerView 新增类型时就很抓狂


  1. 更好的 RecyclerView 表项子控件点击监听器


  1. 更高效地刷新 RecyclerView | DiffUtil二次封装


  1. 换一个思路,超简单的RecyclerView预加载


  1. RecyclerView 动画原理 | 换个姿势看源码(pre-layout)


  1. RecyclerView 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系


  1. RecyclerView 动画原理 | 如何存储并应用动画属性值?


  1. RecyclerView 面试题 | 列表滚动时,表项是如何被填充或回收的?


  1. RecyclerView 面试题 | 哪些情况下表项会被回收到缓存池?


  1. RecyclerView 性能优化 | 把加载表项耗时减半 (一)


  1. RecyclerView 性能优化 | 把加载表项耗时减半 (二)


  1. RecyclerView 性能优化 | 把加载表项耗时减半 (三)


  1. RecyclerView 的滚动是怎么实现的?(一)| 解锁阅读源码新姿势


  1. RecyclerView 的滚动时怎么实现的?(二)| Fling


  1. RecyclerView 刷新列表数据的 notifyDataSetChanged() 为什么是昂贵的?


目录
相关文章
|
2月前
|
前端开发
HarmonyNext动画大全02-显式动画
HarmonyNext动画大全02-显式动画
47 2
HarmonyNext动画大全02-显式动画
|
4月前
Framer 滚动动画效果集合 (讲解)
Framer 滚动动画效果集合 (讲解)
66 0
|
4月前
Framer 使用滚动变体创建动画
Framer 使用滚动变体创建动画
44 0
RecyclerView GridView模式同一行,使其高度平齐,自动适应高度最大item
RecyclerView GridView模式同一行,使其高度平齐,自动适应高度最大item
277 0
|
7月前
[Qt5&布局] 控件自动填满所在布局框架
[Qt5&布局] 控件自动填满所在布局框架
119 0
[Qt5&布局] 控件自动填满所在布局框架
|
XML Android开发 数据格式
一个Adapter+recycleview实现多种布局,区分布局中
最近因为需要所以学习了一下recycleview,使用Adapter修饰器修饰,使用一个Adapter+recycleview实现多种布局,而不是之前的三个Adapter在同一个recycleview中实现三个布局。点击区分布局中的gridview的图片和姓名。
58 0
|
XML Android开发 uml
Android 补间动画及动画组合AnimationSet常用方法整理
`Android`常用的四种补间动画分别为`RotateAnimation`、`ScaleAnimation`、`TranslateAnimation`、`AlphaAnimation`,他们的父类为`Animation`
161 0
|
计算机视觉
RecyclerView#Adapter支持无数据布局、错误布局和列表尾部的”没有更多了“布局
实际开发中,UI小姐姐都会提供通用的`无数据页面`、`错误提示页面`。 针对常见的`支持下拉刷新和上拉加载更多的列表页面`,将他们的通用逻辑抽取出来,这样我们在开发过程中就只需要关注具体的业务逻辑了,无需每次通过cv来完善`无数据页面`、`错误提示页面`的逻辑了。
|
存储 缓存 Android开发
RecyclerView 动画原理 | 如何存储并应用动画属性值?
RecyclerView 表项动画的属性值是怎么获取的,又存储在哪里?这一篇继续通过 走查源码 的方式解答这个疑问。
151 0
|
Dart 开发者
【Flutter】Animation 动画 ( AnimatedBuilder 动画使用流程 | 创建动画控制器 | 创建动画 | 创建动画作用的组件 | 关联动画与组件 | 动画执行 )(三)
【Flutter】Animation 动画 ( AnimatedBuilder 动画使用流程 | 创建动画控制器 | 创建动画 | 创建动画作用的组件 | 关联动画与组件 | 动画执行 )(三)
188 0
【Flutter】Animation 动画 ( AnimatedBuilder 动画使用流程 | 创建动画控制器 | 创建动画 | 创建动画作用的组件 | 关联动画与组件 | 动画执行 )(三)