本文继上篇 ItemDecoration 之后,是深入理解 RecyclerView 系列的第二篇,关注于 ItemAnimator,主要是分析 RecyclerView Animators 这个库的原理,然后总结如何自己编写自定义的 ItemAnimator。本文涉及到的完整代码可以在 Github 获取。
先看看类结构
-
DefaultItemAnimator
extendsSimpleItemAnimator
extendsRecyclerView.ItemAnimator
-
FadeInAnimator
extendsBaseItemAnimator
extendsSimpleItemAnimator
extendsRecyclerView.ItemAnimator
-
RecyclerView.ItemAnimator
定义了一系列 API 用于开发 item view 的动效-
animateDisappearance
,animateAppearance
,animatePersistence
,animateChange
这4个 API 用来对 item view 进行动画显示 -
recordPreLayoutInformation
,recordPostLayoutInformation
这2个 API 用来记录 item view 在 layout 前后的状态信息,这些信息封装在ItemHolderInfo
或者其子类中,并将传递给上述4个动画API中,以便进行动画展示 -
runPendingAnimations
可以用来延迟动画到下一帧,此时就需要在上述4个 API 的实现中返回true
,并且自行记录延迟的动画信息,以便在下一帧时执行 -
dispatchAnimationStarted
和dispatchAnimationFinished
是用来进行状态同步和事件通知的,子类必须在动画开始时调用 dispatchAnimationStarted,结束时调用 dispatchAnimationFinished,当然如果不展示动画,那就只需要直接调用 dispatchAnimationFinished
-
-
SimpleItemAnimator
则对RecyclerView.ItemAnimator
的 API 进行了一次封装- 把父类定义的4个动画 API 转换为了
animateRemove
,animateAdd
,animateMove
,animateChange
这4个,为什么这样?这一次封装就把对 preLayoutInfo 和 postLayoutInfo 的处理的公共代码封装了起来,把 ItemHolderInfo 转换为了 left, top, x, y 这样的位置信息,这样,大部分动画只需要根据位置变化信息的实现,专注实现自己的动画逻辑即可,一方面复用了代码,另一方面也更好的践行了单一职责原则 - 但是如果位置信息对于动画的展示不够,那就需要自己重写
RecyclerView.ItemAnimator
的相应动画 API 了 - 同时也定义了一系列
dispatch***
和on***
API,用于进行事件回调
- 把父类定义的4个动画 API 转换为了
-
DefaultItemAnimator
是 RecyclerView 包中的一个默认实现,而BaseItemAnimator
则是 RecyclerView Animators 库中 animator 的基类,它们都继承自SimpleItemAnimator
,两者具有很大相似性,只分析后者
BaseItemAnimator
BaseItemAnimator 实现了父类的 animateRemove
, animateAdd
, animateMove
, animateChange
这4个 API,而实现方式都是把参数包装一下,放入相应的 animation 列表中,并返回 true,然后在 runPendingAnimations 函数中集中显示动画。为什么要这样呢?因为 recycler view 的变化是随时都可能发生的,而这样的处理就可以把动画的显示按帧对其,即两帧之间的变化,都在下一帧开始时一起处理。但是这样做有什么优势呢?暂时不得而知,DefaultItemAnimator 就是这样处理的。
例如 animateRemove 的实现如下:
@Override
public boolean animateRemove(final ViewHolder holder) { endAnimation(holder); preAnimateRemove(holder); mPendingRemovals.add(holder); return true; }
那么下面重点看看 runPendingAnimations。
@Override
public void runPendingAnimations() { boolean removalsPending = !mPendingRemovals.isEmpty(); boolean movesPending = !mPendingMoves.isEmpty(); boolean changesPending = !mPendingChanges.isEmpty(); boolean additionsPending = !mPendingAdditions.isEmpty(); if (!removalsPending && !movesPending && !additionsPending && !changesPending) { // nothing to animate return; } // First, remove stuff for (ViewHolder holder : mPendingRemovals) { doAnimateRemove(holder); } mPendingRemovals.clear(); // Next, move stuff if (movesPending) { final ArrayList<MoveInfo> moves = new ArrayList<MoveInfo>(); 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(); } } // Next, change stuff, to run in parallel with move animations