RecyclerView 表项动画的属性值是怎么获取的,又存储在哪里?这一篇继续通过 走查源码 的方式解答这个疑问。
通过上两篇的分析得知,为了做动画 RecyclerView 会布局两次:预布局+后布局,依次将动画前与动画后的表项填充到列表。表项被填充后,就确定了它相对于 RecyclerView 左上角的位置,在两次布局过程中,这些位置信息是如何被保存的?
这是 RecyclerView 动画原理的第三篇,系列文章目录如下:
引子
这一篇源码分析还是基于下面这个 Demo 场景:
列表中有两个表项(1、2),删除 2,此时 3 会从屏幕底部平滑地移入并占据原来 2 的位置。
为了实现该效果,RecyclerView
的策略是:为动画前的表项先执行一次pre-layout
,将不可见的表项 3 也加载到布局中,形成一张布局快照(1、2、3)。再为动画后的表项执行一次post-layout
,同样形成一张布局快照(1、3)。比对两张快照中表项 3 的位置,就知道它该如何做动画了。
在此援引上一篇已经得出的结论:
RecyclerView
为了实现表项动画,进行了 2 次布局(预布局 + 后布局),在源码上表现为LayoutManager.onLayoutChildren()
被调用 2 次。
State.mInPreLayout
用于标识是否在预布局阶段。预布局的生命周期始于RecyclerView.dispatchLayoutStep1()
,终于RecyclerView.dispatchLayoutStep2()
。
- 在预布局阶段,循环填充表项时,若遇到被移除的表项,则会忽略它占用的空间,多余空间被用来加载额外的表项,这些表项在屏幕之外,本来不会被加载。
其中第三点表现在源码上,是这样的:
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_POST
,InfoRecord
中用一个新的常量表示了这种状态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
实现的。
总结
- RecyclerView 将表项动画数据封装了两层,依次是
ItemHolderInfo
和InfoRecord
,它们记录了列表预布局和后布局表项的位置信息,即表项矩形区域与列表左上角的相对位置,它还用一个int
类型的标志位来记录表项经历了哪些布局阶段,以判断表项应该做的动画类型(出现,消失,保持)。
InfoRecord
被集中存放在一个商店类ViewInfoStore
中。所有参与动画的表项的ViewHolder
与InfoRecord
都会以键值对的形式存储其中。
- RecyclerView 在布局的第三阶段会遍历商店类中所有的键值对,以
InfoRecord
中的标志位为依据,判断执行哪种动画。表项预布局和后布局的位置信息会一并传递给RecyclerView.ItemAnimator
,以触发动画。
RecyclerView.ItemAnimator
收到动画指令和数据后,又将他们封装为MoveInfo
,不同类型的动画被存储在不同的MoveInfo
列表中。然后将执行动画的逻辑抛到 Choreographer 的动画队列中,当下一个垂直同步信号到来时,Choreographer 从动画队列中取出并执行表项动画,执行动画即遍历所有的MoveInfo
列表,为每一个MoveInfo
构建 ViewPropertyAnimator 实例并启动动画。
推荐阅读
RecyclerView 系列文章目录如下: