本文继上篇 ItemDecoration 之后,是深入理解 RecyclerView 系列的第二篇,关注于 ItemAnimator,主要是分析 RecyclerView Animators 这个库的原理,然后总结如何自己编写自定义的 ItemAnimator。本文涉及到的完整代码可以在 Github 获取。
先看看类结构
DefaultItemAnimatorextendsSimpleItemAnimatorextendsRecyclerView.ItemAnimatorFadeInAnimatorextendsBaseItemAnimatorextendsSimpleItemAnimatorextendsRecyclerView.ItemAnimatorRecyclerView.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();
}
}