快速定制简单的Item Animation

Item Animation的基本原理

我们都知道,给RecyclerView添加Item Animation的方法是setItemAnimator(ItemAnimator animator)。因此我们可以以抽象类ItemAnimator为切入点,看看Recyclerview的Item Animation是怎样实现的。

首先列举一下ItemAnimator中几个比较重要的方法签名:

    • public ItemHolderInfo recordPreLayoutInformation(State state,ViewHolder viewHolder,int changeFlags,List<Object> payloads)
    • public ItemHolderInfo recordPostLayoutInformation(State state, ViewHolder viewHolder)

    这两个方法是成对出现的,分别用于记录同一个ViewHolder在layout执行前后view位置等信息,分别对应了动画开始前的view状态和动画结束后的view状态。

    • public abstract boolean animateDisappearance(ViewHolder viewHolder, ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo)
    • public abstract boolean animateAppearance(ViewHolder viewHolder, ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo)
    • public abstract boolean animatePersistence(ViewHolder viewHolder, ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo)
    • public abstract boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder, ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo)

    这四个方法都是由RecyclerView调用,调用时机分别为在执行layout过程之后viewHolderRecyclerView中移除(从有到无)、出现(从无到有)、不变(同一个viewHolder但是尺寸、位置可能发生变化)、改变(从oldHolder变为newHolderviewHolder发生了变化)。

    这四个方法中都包含preLayoutInfopostLayoutInfo,这两个参数就是由前面recordPreLayoutInformationrecordPostLayoutInformation方法计算出来的viewHolder信息。我们就可以根据viewHolder的初态(preLayoutInfo)和终态(postLayoutInfo)实现自定义的动画。但是动画并不是在这个方法中启动,在这四个方法中我们仅仅是根据preLayoutInfopostLayoutInfo判断是否需要动画和需要什么动画。如果不需要执行动画,就必须在这四个方法中调用dispatchAnimationFinished(ViewHolder)方法并且最后的return值必须为false;反之如果需要执行动画return值就必须为true,之后系统就会调用runPendingAnimations()方法,并在runPendingAnimations()中启动各种动画,在动画执行完毕后也必须要调用dispatchAnimationFinished(ViewHolder)方法。

  1. abstract public void runPendingAnimations()

    如果上面四个方法中有一个的返回值是true,在下一帧的时候就会执行此方法,所有的动画都可以在此方法中启动。

    • abstract public void endAnimation(ViewHolder item)
    • abstract public void endAnimations()
      结束动画,并确保各个viewHolder都处于最终态。

到此为止,我们可以缕一下整个过程。

首先RecyclerView在执行layout前后会调用recordPreLayoutInformationrecordPostLayoutInformation方法,并保存layout前后viewHodler的位置等信息;然后根据layout的情况执行animateXXX方法,我们在animateXXX方法中决定是否执行动画和执行何种动画;如果需要执行动画,则可以在runPendingAnimations方法中启动动画;最后在endAnimationendAnimations方法中确保viewHolder处于最终态。

快速实现自定义Item Animation

问当今如何code最快,那必然是ctrl-C加上ctrl-V。(开个玩笑~~~)

不过现在讲的是如何快速的自定义Item Animation,基于学习的目的,那当然还是需要copy一些代码的嘛。至于以后项目中需要或者兴趣使然要深入自定义Item Animation就需要自己参照原有实现进行封装了。

废话不多说,下面进入正题!!!

android support v7包里已经包含了一个Item Animation的默认实现:android.support.v7.widget.DefaultItemAnimatorDefaultItemAnimator继承了SimpleItemAnimator,而SimpleItemAnimator又继承了RecyclerView.ItemAnimatorDefaultItemAnimator实现了在添加、移除item时View的透明度发生变化,在移动item时view的位置发生变化。我们要做的仅仅是对DefaultItemAnimator进行局部的修改~~~

recordPreLayoutInformation和recordPostLayoutInformationItemAnimator中已经实现,animateDisappearanceanimateAppearanceanimatePersistenceanimateChangeSimpleItemAnimator中也已实现,runPendingAnimationsDefaultItemAnimator中已经实现,我们用这些默认实现就可以了。以添加Item动画为例,我们只要修改如下3个地方就可以实现Item旋转进入动画(仅列出部分关键代码,注释为原代码,////中间为修改后的代码):

  1. 设置动画的初始值,将view设置为旋转180度

    1
    2
    3
    4
    5
    6
    7
    8
    @Override
    public boolean animateAdd(final ViewHolder holder) {
    resetAnimation(holder);
    /**ViewCompat.setAlpha(holder.itemView, 0);**/
    ViewCompat.setRotation(holder.itemView, 180);
    mPendingAdditions.add(holder);
    return true;
    }
  2. 启动动画,让view再转180度。并在动画取消时将view设置为终态,也就是旋转0度

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    private void animateAddImpl(final RecyclerView.ViewHolder holder) {
    ...
    /**animation.alpha(1)**/
    animation.rotationBy(180)
    .setDuration(getAddDuration()).
    setListener(new VpaListenerAdapter() {
    ...
    @Override
    public void onAnimationCancel(View view) {
    /**ViewCompat.setAlpha(view, 1);**/
    ViewCompat.setRotation(view, 0);
    }
    ...
    }).start();
    }
  3. 结束动画,确保view在动画结束或者取消后能回到终态,也就是旋转0度。(这里省略了endAnimations()方法的修改)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @Override
    public void endAnimation(RecyclerView.ViewHolder item) {
    ...
    if (mPendingAdditions.remove(item)) {
    /**ViewCompat.setAlpha(view, 1);**/
    ViewCompat.setRotation(view, 0);
    dispatchAddFinished(item);
    }
    ...
    for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
    ArrayList<RecyclerView.ViewHolder> additions = mAdditionsList.get(i);
    if (additions.remove(item)) {
    /**ViewCompat.setAlpha(view, 1);**/
    ViewCompat.setRotation(view, 0);
    dispatchAddFinished(item);
    if (additions.isEmpty()) {
    mAdditionsList.remove(i);
    }
    }
    }
    ...
    }

通过以上3步,我们就是实现了Item旋转进入的动画,以下是演示效果。

Demo 演示效果

Demo地址

再次强调,以上步骤仅适用于学习。在项目中使用还需自己参照实现自己想要的效果。

PS:顺带说下如何解决调用notifyDataSetChanged后,没有触发Item Animation的问题。。

要想在调用notifyDataSetChanged之后触发Item Animation,必须要在自定义adapter中调用方法setHasStableIds(true);并实现public long getItemId(int position)方法返回各Item的id。。至于为啥,留待以后再探究吧。。。请原谅我这个拖延症重度患者。