android RecyclerView (三):ItemAnimator 详解

本文继上篇 ItemDecoration 以后,是深刻理解 RecyclerView 系列的第二篇,关注于 ItemAnimator,主要是分析 RecyclerView Animators 这个库的原理,而后总结如何本身编写自定义的 ItemAnimator。本文涉及到的完整代码能够在 Github 获取java

先看看类结构

  • DefaultItemAnimator extends SimpleItemAnimator extends RecyclerView.ItemAnimator
  • FadeInAnimator extends BaseItemAnimator extends SimpleItemAnimator extends RecyclerView.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,而且自行记录延迟的动画信息,以便在下一帧时执行
    • dispatchAnimationStarteddispatchAnimationFinished 是用来进行状态同步和事件通知的,子类必须在动画开始时调用 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,用于进行事件回调
  • DefaultItemAnimator 是 RecyclerView 包中的一个默认实现,而 BaseItemAnimator 则是 RecyclerView Animators 库中 animator 的基类,它们都继承自 SimpleItemAnimator,二者具备很大类似性,只分析后者

BaseItemAnimator

BaseItemAnimator 实现了父类的 animateRemove, animateAdd, animateMove, animateChange 这4个 API,而实现方式都是把参数包装一下,放入相应的 animation 列表中,并返回 true,而后在 runPendingAnimations 函数中集中显示动画。为何要这样呢?由于 recycler view 的变化是随时均可能发生的,而这样的处理就能够把动画的显示按帧对其,即两帧之间的变化,都在下一帧开始时一块儿处理。可是这样作有什么优点呢?暂时不得而知,DefaultItemAnimator 就是这样处理的。git

例如 animateRemove 的实现以下:github

@Override public boolean animateRemove(final ViewHolder holder) { endAnimation(holder); preAnimateRemove(holder); mPendingRemovals.add(holder); return true; } 

那么下面重点看看 runPendingAnimations。app

@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 if (changesPending) { final ArrayList<ChangeInfo> changes = new ArrayList<ChangeInfo>(); changes.addAll(mPendingChanges); mChangesList.add(changes); mPendingChanges.clear(); Runnable changer = new Runnable() { @Override public void run() { for (ChangeInfo change : changes) { animateChangeImpl(change); } changes.clear(); mChangesList.remove(changes); } }; if (removalsPending) { ViewHolder holder = changes.get(0).oldHolder; ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration()); } else { changer.run(); } } // Next, add stuff if (additionsPending) { final ArrayList<ViewHolder> additions = new ArrayList<ViewHolder>(); additions.addAll(mPendingAdditions); mAdditionsList.add(additions); mPendingAdditions.clear(); Runnable adder = new Runnable() { public void run() { for (ViewHolder holder : additions) { doAnimateAdd(holder); } additions.clear(); mAdditionsList.remove(additions); } }; if (removalsPending || movesPending || changesPending) { long removeDuration = removalsPending ? getRemoveDuration() : 0; long moveDuration = movesPending ? getMoveDuration() : 0; long changeDuration = changesPending ? getChangeDuration() : 0; long totalDelay = removeDuration + Math.max(moveDuration, changeDuration); View view = additions.get(0).itemView; ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); } else { adder.run(); } } } 

在这里,remove 最早执行,remove 执行完毕后,再同时开始 move 和 change,而它俩都结束后,最后再执行 add。BaseItemAnimator 对 add 和 remove 这两个动画的播放进行了再一次的封装,定义了 animateAddImplanimateRemoveImpl 这两个 API,以及 preAnimateAddImplpreAnimateRemoveImpl 供动画开始前进行须要的操做,而这个库内置的多种 animator,都只是在这两个 API 中实现了不一样的出现和消失的逻辑。add 和 remove 这两个动画的进一步封装,再次简化了编写 animator 的代码,具体的 animator 只须要专一于本身的动画显示逻辑便可。而 move 和 change 这两类动画,则是直接使用了 DefaultItemAnimator 的代码,move 就是经过 TranslationX 和 TranslationY 把 item view 从老位置移动到新位置,change 就是经过 TranslationX, setTranslationY 和 alpha 来完成内容的改变效果。ide

自定义的 move 和 change 实现

这部分有一篇不错的文章:InstaMaterial - RecyclerView animations done right 函数

基本原理仍是 RecyclerView.ItemAnimator 提供的 API,canReuseUpdatedViewHolder 控制动效时是否建立新的 ViewHolder,recordPreLayoutInformation/recordPostLayoutInformation 用来在 layout 以前/后记录须要的信息,animateChange/animateMove 来实现具体的动画逻辑,而这时可能会须要 layout 先后记录的信息。post

在这篇文章中,做者就是在 recordPreLayoutInformation 中把须要的信息记录在了自定义的 ItemHolderInfo 中,而且在 animateChange 使用记录的信息进行动画的显示。这个过程并无什么难点,主要仍是动画的设计,以及实现的效率和稳定性,例如避免反复建立没必要要的对象,避免出现闪退等。具体的例子能够看这篇文章。动画

Talk is cheap, show me the code

好了,说了这么多,仍是须要一个完整的 demo 才接地气。结合自家产品的需求,这个 demo 中将要实现这样的效果:列表可滑动,新数据加入时,若是正在滑动,则不自动滚到最新(最底部),若是超过5秒不滑动,则自动滚动到最新,若是原本就在最新,则动效塞入新数据(fade in),列表中的数据15秒自动消失,fade out 消失动效,每一个 item 点击以后有一个“桃心放大”的效果,模仿的是上一节中那篇文章的效果。关于滑动检测、自动滑动的内容,将在下一篇中展开,本篇聚焦于 ItemAnimator,因此这一版本包含的是增长、移除、点击的动效。spa

总体效果

增长和移除的动效,直接继承自 RecyclerView Animators 库的 FadeInAnimator 就能够实现了,而点击动效,则直接借用了 InstaMaterial 的部分代码。在这个过程当中,还发现了 RecyclerView Animators 的一个 bug,它的全部内置 animator 的实现,change 动效都不起做用,并且还会影响其余类型的动效展现,缘由比较简单,重载了 animateChange,可是既没有调用 super 的实现,也没有调用 dispatchAnimationFinished,具体能够查看这个 issue 下的评论设计

从最后的效果图中咱们能够看到,若是 item view 快要消失时,咱们点击了,播放点击动效以后,item 的消失会有闪烁的问题,这个问题本篇先暂且放下,后续的文章中会进行分析和解决。

相关文章
相关标签/搜索