Aug 15

理解RecyclerView中ItemAnimator的使用

Lrdcq , 2016/08/15 00:51 , 程序 , 閱讀(29330) , Via 本站原創
如果说在使用RecyclerView的过程中,我们偶尔还能提到ItemDecoration,那么ItemAnimator这张东西估计是写一百个列表都不会提一次,有些人一辈子都没动过了。初学RecyclerView都会稍微提到一下ItemAnimator,它是做RecyclerView的item增删改动画的工作,是RecyclerView一对一的标配,拥有默认实现DefaultItemAnimator而且效果还不错(真的效果还不错)。那么,如果DefaultItemAnimator可以满足我们的业务需求,我们在其之上,还需要做些什么呢?

深究

代码怎么写当然是可以随意的,因此我们要从原则性的角度来考虑,我们到底应该用ItemAnimator做些什么。
首先,我们清晰的知道,ItemAnimator就是负责为item做动画效果的,并且特别针对于RecyclerView的Adapter的notify系列操作(除了notifyDataSetChanged)。那么,使用notify系列操作的意义又在何处呢?回想以前listView的时代,每次我们更新了数据需要刷新界面的时候,对交互没有要求的话,直接暴力的刷notifyDataSetChanged就可以了,否则需要煞费苦心的找到单个item进行操作或者打破listView层,把它当做viewGroup下手,当然,这样也会带来各种各样不稳定的因素。后来到了RecyclerView时代了,一般对动画没什么要求的情况下,大家还是都使用的notifyDataSetChanged...因为懒得计算需要做动画的位置。再后来将就一点,才开始老老实实使用notify系列方法来操作列表了。
可以注意到,ItemAnimator和adapter的notify系列方法是密不可分的,甚至可以说是一对一的关系。那么,操作notify的意义也就是编写ItemAnimator动画的意义了。那么想想notify四大操作,添加add,删除remove,移动move,改变change,似乎就change并没有动画啊——想想也是可以理解的,change往往就是重新绑定一次数据,需要什么动画呢——或者是安卓是不知道这种情况下需要什么动画的——因此,这里就是我们的下手点。

以上思路来源于Android Dev Summit 2015 : RecyclerView Animations and Behind the Scenes(https://www.youtube.com/watch?v=imsr8NrIAMs&index=6&list=PLWz5rJ2EKKc_Tt7q77qwyKRgytF1RzRx8)。总的来说,在以往的做法中,我们在涉及到item数据改变的动画往往是notifyDataSetChanged然后自己做的,而讲道理,这个应该自定义ItemAnimator来完成并通过notifyItemChanged来发起。这不但是为了代码的功能分离与洁癖,用ItemAnimator统一管理动画也有它自己的优势。

基类拆分

先不扯大道理了,由于我们希望的是在DefaultItemAnimator的基础上实现自己的数据change动画,因此我们首先来追踪一下DefaultItemAnimator到底是如何实现的。我们看到它的继承链条是:DefaultItemAnimator -> SimpleItemAnimator -> RecyclerView.ItemAnimator,那么我们从最底层开始看吧。
RecyclerView.ItemAnimator

当然,这是RecyclerView的ItemAnimator最基础类,不过它其中已经为我们规划好了动画准备和运行的方式了。

1. 首先运行的是recordPreLayoutInformation和recordPostLayoutInformation方法。看他们的名字就知道它们在为动画前后状态准备数据,调用的生命周期是,每次执行notify系列方法时,先对所有item执行recordPreLayoutInformation,然后才执行notify所呼唤的操作,最后对所有item执行recordPostLayoutInformation。其中一个记录方法如下:
public @NonNull ItemHolderInfo recordPreLayoutInformation(@NonNull State state,
                @NonNull ViewHolder viewHolder, @AdapterChanges int changeFlags,
                @NonNull List<Object> payloads)

可以看到传递给他的最重要的是当前的viewHolder,而需要返回回去的是一个ItemHolderInfo类型的物体,实际需要的是整个item的外边界范围。

2. 剩下的就是实际完成动画的方法了:包括animateDisappearance, animateAppearance, animatePersistence, animateChange。这些方法都是使用需要操作的item对应item记录的信息来完成动画,比如这是其中一个:
public boolean animateChange(@NonNull ViewHolder oldHolder, @NonNull ViewHolder newHolder,
            @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo)

SimpleItemAnimator

这和上面一样,也是一个抽象的ItemAnimator,不过把其中的复杂部分简单化了。首先,record的信息是外边界没有问题,然后之后实际完成动画的接口方法,SimpleItemAnimator这些方法都实现了一下,但实现出来并没有完成动画,而是把ItemHolderInfo的信息给拆解来开,变成更加直观的抽象接口并甩出来给外层。例如:
abstract public boolean animateMove(ViewHolder holder, int fromX, int fromY,
            int toX, int toY);

abstract public boolean animateChange(ViewHolder oldHolder,
            ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop);

DefaultItemAnimator

默认的动画实现,当然没话说,就是把SimpleItemAnimator甩出来的动画实现方法全部实现了一次。阅读源码的时候可以注意到,在animateChange中,一开始是这么一段:
if (oldHolder == newHolder) {
    // Don't know how to run change animations when the same view holder is re-used.
    // run a move animation to handle position changes.
    return animateMove(oldHolder, fromX, fromY, toX, toY);
}

这一段代码就是我们上文说往往需要实现的notifyItemChanged对应的逻辑,当然源码中那样些往往什么也不会发生,这才是要我们实现的地方。

实际场景

我们用来编写demo的场景是selectable的列表demo,每当每次select之后,为item做一个小小的动画。那么我们从上倒下继承DefaultItemAnimator后,还需要做什么呢?

1. 记录更多数据。之前准备的数据只包涵界面上下左右信息,并不能包涵前后选中状态的信息,因此我们需要a.继承ItemHolderInfo(重写obtainHolderInfo()吐出这个对象)。b.重写recordPreLayoutInformation, recordPostLayoutInformation记录下我们需要的信息。这个过程例如:
@NonNull
@Override
public ItemHolderInfo recordPreLayoutInformation(@NonNull RecyclerView.State state, @NonNull RecyclerView.ViewHolder viewHolder, int changeFlags, @NonNull List<Object> payloads) {
    SelectHolderInfo info = (SelectHolderInfo) super.recordPreLayoutInformation(state, viewHolder,changeFlags, payloads);
    info.isSelected = viewHolder.itemView.isSelected();
    return info;
}

其中可以看到这种情况下需要记录的数据可以直接从界面中读出来,如果动画比较复杂需要大量数据做支撑的话,还是老老实实设tag吧。

2. 使用这些信息进行动画。重写DefaultItemAnimator的oldHolder=newHolder情况,绘制动画,例如下:
@Override
public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder, @NonNull RecyclerView.ViewHolder newHolder, @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
    if (oldHolder != newHolder)return super.animateChange(oldHolder, newHolder, preInfo, postInfo);

    SelectHolderInfo pInfo = (SelectHolderInfo) preInfo, tInfo = (SelectHolderInfo) postInfo;
    if (pInfo.isSelected == tInfo.isSelected) return false;

    Animation animation = new ScaleAnimation(0.6f, 1.0f, 0.6f, 1.0f);
    animation.setDuration(400);
    newHolder.itemView.startAnimation(animationInfo.animation);
    
    return true;
}

为什么我们重写的是ItemAnimator的animateChange呢——因为我们重写了HolderInfo需要自己拆信息啊,否则调用SimpleItemAnimator也可以。

完成一二两步之后,我们可以试一试,诶,已经可以出线动画了。不过多点几次就会发现,还会有很多问题。仔细想想,ItemAnimator的最大优势就是统一管理动画了,我们为何要把发起的动画丢在一边呢。因此:

3. 维护holder和animation映射关系。为了维护这个关系,我们新建了AnimationInfo储存holder和animation,在全局添加数组来储存,为animation添加完成listener来释放:
AnimationInfo animationInfo = new AnimationInfo();

animationInfo.animation = new ScaleAnimation(0.6f, 1.0f, 0.6f, 1.0f);
animationInfo.animation.setDuration(400);
animationInfo.animation.setAnimationListener(new Animation.AnimationListener() {
    @Override
    public void onAnimationStart(Animation animation) {}

    @Override
    public void onAnimationEnd(Animation animation) {
        penddingAnima.remove(animationInfo);
    }

    @Override
    public void onAnimationRepeat(Animation animation) {}
});
animationInfo.holder = newHolder;
animationInfo.holder.itemView.startAnimation(animationInfo.animation);
penddingAnima.add(animationInfo);


4. 当然,既然我们都管理起来了,就需要处理在各种中断的情况下动画的表现,一共有4种情况:
a. 重新发起动画:需要在运行动画前判断当前操作item有没有正在执行的动画,有的话,取消掉。
if (penddingAnima.size() != 0) {
     List<AnimationInfo> copy = new ArrayList<>(penddingAnima);
     for (AnimationInfo info : copy) {
          if (info.holder == newHolder) {
               info.cancel();
               penddingAnima.remove(info);
          }
     }
}

b. public void endAnimation(RecyclerView.ViewHolder item);public void endAnimations();public boolean isRunning(); ItemAnimator自带了这3个方法来响应和处理动画状态和终端,既然我们新增了一种动画,当然也要将其纳入暴露的管理方法中:
@Override
 public void endAnimation(RecyclerView.ViewHolder item) {
    super.endAnimation(item);
    for (AnimationInfo info : penddingAnima) {
        if (info.holder == item) {
            info.cancel();
            penddingAnima.remove(info);
            break;
        }
    }
=}

@Override
public void endAnimations() {
    super.endAnimations();
    for (AnimationInfo info : penddingAnima) {
        info.cancel();
    }
    penddingAnima.clear();
}

@Override
public boolean isRunning() {
    return super.isRunning() || !penddingAnima.isEmpty();
}

这样,添加一种动画的最简但是完整的流程就完成了。

其它

最终还是考虑我们面向b端的实际业务场景,就算要做动画,修改DefaultItemAnimator的原生动画应该并不存在那样的需求,就算存在,依葫芦画瓢仿照DefaultItemAnimator重写动画应该还是没问题的。反而在这种情况下需要添加一种新动画时,还有一些小坑,参考自带代码和第三方代码,这样的最小动画驱动方式应该足以应用在业务中。当然,说到底,动画构架都是小事儿,动画的创意和实现的实际代码才是最终的大boss呢。
logo