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() 为什么是昂贵的?


目录
相关文章
|
存储 数据采集 监控
云上数据安全保护:敏感日志扫描与脱敏实践详解
随着企业对云服务的广泛应用,数据安全成为重要课题。通过对云上数据进行敏感数据扫描和保护,可以有效提升企业或组织的数据安全。本文主要基于阿里云的数据安全中心数据识别功能进行深入实践探索。通过对商品购买日志的模拟,分析了如何使用阿里云的工具对日志数据进行识别、脱敏(3 种模式)处理和基于 StoreView 的查询脱敏方式,从而在保障数据安全的同时满足业务需求。通过这些实践,企业可以有效降低数据泄漏风险,提升数据治理能力和系统安全性。
1754 228
云上数据安全保护:敏感日志扫描与脱敏实践详解
|
人工智能 缓存 搜索推荐
百度/Bing/Google搜索引擎使用技巧
本文分享了百度、Bing和Google三大搜索引擎的实用技巧,涵盖精确匹配、排除关键词、站内及文件类型搜索等,如使用双引号进行精确搜索“人工智能应用”,排除特定词如“人工智能 -游戏”,以及在特定网站如“site:baidu.com 人工智能”内查找内容等,帮助提高搜索效率和准确性。
1229 7
百度/Bing/Google搜索引擎使用技巧
|
11月前
|
人工智能 Unix Java
[oeasy]python059变量命名有什么规则_惯用法_蛇形命名法_name_convention_snake
本文探讨了Python中变量命名的几种常见方式,包括汉语拼音变量名、蛇形命名法(snake_case)和驼峰命名法(CamelCase)。回顾上次内容,我们主要讨论了使用下划线替代空格以提高代码可读性。实际编程中,当变量名由多个单词组成时,合理的命名惯例变得尤为重要。
423 9
|
11月前
|
人工智能 搜索推荐 Serverless
云端问道22期——AI智能语音实时互动
《云端问道22期——AI智能语音实时互动》分享了构建用户与AI智能语音实时互动的方法,涵盖七个部分:进入解决方案页、方案介绍、操作步骤、创建AI智能体、实时工作模版、部署应用及应用体验。通过阿里云平台,用户可以快速部署并体验AI语音通话功能,包括语音转文字、文字转语音、个性化定制智能体人设及接入私有知识库等。整个过程简单流畅,适合开发者和企业快速上手。
657 8
|
安全 机器人
Nature子刊:人机融合即将成真!纳米机器人杀死癌细胞,肿瘤生长抑制70%
【7月更文挑战第9天】DNA纳米机器人成功抑制小鼠体内癌细胞生长70%,展示出人机融合治疗癌症的前景。卡罗林斯卡学院科学家利用DNA构造的纳米机器人,识别并选择性攻击癌细胞,其pH敏感设计确保只在肿瘤微环境中激活,减少对健康细胞的影响。尽管需进一步研究优化设计及进行临床试验,这一创新为癌症疗法带来新希望。[链接](https://www.nature.com/articles/s41565-024-01676-4)**
529 1
|
前端开发 Java Spring
springboot自定义拦截器的简单使用和一个小例子
本文介绍了如何在Spring Boot中创建和使用自定义拦截器,通过一个登录验证的示例,演示了拦截器在MVC流程中的preHandle、postHandle和afterCompletion三个环节的作用,并说明了如何在Spring Boot配置类中注册拦截器。
|
存储 缓存 NoSQL
Redis与数据库同步指南:订阅Binlog实现数据一致性
本文由开发者小米分享,探讨分布式系统中的一致性问题,尤其是数据库和Redis一致性。文章介绍了全量缓存策略的优势,如高效读取和稳定性,但也指出其一致性挑战。为解决此问题,提出了通过订阅数据库的Binlog实现数据同步的方法,详细解释了工作原理和步骤,并分析了优缺点。此外,还提到了异步校准方案作为补充,以进一步保证数据一致性。最后,提醒在实际线上环境中需注意日志记录、逐步优化和监控报警。
1420 3
|
安全 Java 应用服务中间件
Spring Boot 实现程序的优雅退出
Spring Boot 实现程序的优雅退出
|
搜索推荐 算法 安全
AIGC对未来高校教学的影响
【1月更文挑战第14天】AIGC对未来高校教学的影响
513 3
AIGC对未来高校教学的影响
|
Java 应用服务中间件 Shell
Springboot如何打包部署项目
Springboot如何打包部署项目
1060 0

热门文章

最新文章