android RecyclerView (三):ItemAnimator 详解
本文继上篇 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();
}
}