RecycleView的Item Animator动画

RecyclerView能够通过mRecyclerView.setItemAnimator(ItemAnimator animator)设置添加、删除、移动、改变的动画效果

RecyclerView提供了默认的ItemAnimator实现类:DefaultItemAnimator。如果没有特殊的需求,默认使用这个动画即可。

// 设置Item添加和移除的动画
mRecyclerView.setItemAnimator(new DefaultItemAnimator());

下面就添加一下删除和添加Item的动作。在Adapter里面添加方法

public void addNewItem() {
    if(mData == null) {
        mData = new ArrayList<>();
    }
    mData.add(0, "new Item");
  更新数据集不是用adapter.notifyDataSetChanged()而是notifyItemInserted(position)与notifyItemRemoved(position) 否则没有动画效果。 
    notifyItemInserted(0);
}

public void deleteItem() {
    if(mData == null || mData.isEmpty()) {
        return;
    }
    mData.remove(0);
    notifyItemRemoved(0);
}

添加事件的处理。

public void onClick(View v) {
    int id = v.getId();
    if(id == R.id.rv_add_item_btn) {
        mAdapter.addNewItem();
        // 由于Adapter内部是直接在首个Item位置做增加操作,增加完毕后列表移动到首个Item位置
        mLayoutManager.scrollToPosition(0);
    } else if(id == R.id.rv_del_item_btn){
        mAdapter.deleteItem();
        // 由于Adapter内部是直接在首个Item位置做删除操作,删除完毕后列表移动到首个Item位置
        mLayoutManager.scrollToPosition(0);
    }
}

准备工作完毕后,来看一下运行的效果。

 

DefaultItemAnimator源码分析

这里我们通过分析DefaultItemAnimator的源码来介绍如何自定义Item Animator。

DefaultItemAnimator继承自SimpleItemAnimator,SimpleItemAnimator继承自ItemAnimator。

首先我们介绍ItemAnimator类的几个重要方法

  • animateAppearance(): 当ViewHolder出现在屏幕上时被调用(可能是add或move)。
  • animateDisappearance(): 当ViewHolder消失在屏幕上时被调用(可能是remove或move)。
  • animatePersistence(): 在没调用notifyItemChanged()notifyDataSetChanged()的情况下布局发生改变时被调用。
  • animateChange(): 在显式调用notifyItemChanged()notifyDataSetChanged()时被调用。
  • runPendingAnimations(): RecyclerView动画的执行方式并不是立即执行,而是每帧执行一次,比如两帧之间添加了多个Item,则会将这些将要执行的动画Pending住,保存在成员变量中,等到下一帧一起执行。该方法执行的前提是前面animateXxx()返回true。
  • isRunning(): 是否有动画要执行或正在执行。
  • dispatchAnimationsFinished(): 当全部动画执行完毕时被调用。

上面的方法比较难懂,不过没关系,因为Android提供了SimpleItemAnimator类(继承自ItemAnimator),该类提供了一系列更易懂的API,在自定义Item Animator时只需要继承SimpleItemAnimator即可:

  • animateAdd(ViewHolder holder): 当Item添加时被调用。
  • animateMove(ViewHolder holder, int fromX, int fromY, int toX, int toY): 当Item移动时被调用。
  • animateRemove(ViewHolder holder): 当Item删除时被调用。
  • animateChange(ViewHolder oldHolder, ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop): 当显式调用notifyItemChanged()notifyDataSetChanged()时被调用。

对于以上四个方法,注意两点:

  • 当Xxx动画开始执行前(在runPendingAnimations()中)需要调用dispatchXxxStarting(holder),执行完后需要调用dispatchXxxFinished(holder)
  • 这些方法的内部实际上并不是书写执行动画的代码,而是将需要执行动画的Item全部存入成员变量中,并且返回值为true,然后在runPendingAnimations()中一并执行。

DefaultItemAnimator类是RecyclerView提供的默认动画类。我们通过阅读该类源码学习如何自定义Item Animator。我们先看DefaultItemAnimator的成员变量

private ArrayList mPendingAdditions = new ArrayList<>();//存放下一帧要执行的一系列add动画
ArrayList> mAdditionsList = new ArrayList<>();//存放正在执行的一批add动画
ArrayList mAddAnimations = new ArrayList<>(); //存放当前正在执行的add动画

private ArrayList mPendingRemovals = new ArrayList<>();
ArrayList mRemoveAnimations = new ArrayList<>();

private ArrayList mPendingMoves = new ArrayList<>();
ArrayList> mMovesList = new ArrayList<>();
ArrayList mMoveAnimations = new ArrayList<>();

private ArrayList mPendingChanges = new ArrayList<>();
ArrayList> mChangesList = new ArrayList<>();
ArrayList mChangeAnimations = new ArrayList<>();

DefaultItemAnimator实现了SimpleItemAnimator的animateAdd()方法,该方法只是将该item添加到mPendingAdditions中,等到runPendingAnimations()中执行。

public boolean animateAdd(final ViewHolder holder) {
    resetAnimation(holder);  //重置清空所有动画
    ViewCompat.setAlpha(holder.itemView, 0); //将要做动画的View先变成透明
    mPendingAdditions.add(holder);
    return true;
}

接着看runPendingAnimations()的实现,该方法是执行remove,move,change,add动画,执行顺序为:remove动画最先执行,随后move和change并行执行,最后是add动画。为了简化,我们将remove,move,change动画执行过程省略,只看执行add动画的过程,如下:

public void runPendingAnimations() {
    //1、判断是否有动画要执行,即各个动画的成员变量里是否有值。
    //2、执行remove动画
    //3、执行move动画
    //4、执行change动画,与move动画并行执行
    //5、执行add动画
    if (additionsPending) {
        final ArrayList additions = new ArrayList<>();
        additions.addAll(mPendingAdditions);
        mAdditionsList.add(additions);
        mPendingAdditions.clear();
        Runnable adder = new Runnable() {
            @Override
            public void run() {
                for (ViewHolder holder : additions) {
                    animateAddImpl(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); //等remove,move,change动画全部做完后,开始执行add动画
        }
    }
}

为了防止在执行add动画时外面有新的add动画添加到mPendingAdditions中,从而导致执行add动画错乱,这里将mPendingAdditions的内容移动到局部变量additions中,然后遍历additions执行动画。

runPendingAnimations()中,animateAddImpl()是执行add动画的具体方法,其实就是将itemView的透明度从0变到1(在animateAdd()中已经将view的透明度变为0),实现如下:

void animateAddImpl(final ViewHolder holder) {
    final View view = holder.itemView;
    final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
    mAddAnimations.add(holder);
    animation.alpha(1).setDuration(getAddDuration()).
            setListener(new VpaListenerAdapter() {
                @Override
                public void onAnimationStart(View view) {
                    dispatchAddStarting(holder);  //在开始add动画前调用
                }
                @Override
                public void onAnimationCancel(View view) {
                    ViewCompat.setAlpha(view, 1);
                }

                @Override
                public void onAnimationEnd(View view) {
                    animation.setListener(null);
                    dispatchAddFinished(holder); //在结束add动画后调用
                    mAddAnimations.remove(holder);
                    if (!isRunning()) {
                        dispatchAnimationsFinished(); //结束所有动画后调用
                    }
                }
            }).start();
}

开源动画recyclerview-animators

从DefaultItemAnimator类的实现来看,发现自定义Item Animator好麻烦,需要继承SimpleItemAnimator类,然后实现一堆方法。
别急,recyclerview-animators解救你,原因如下:

  • 首先,recyclerview-animators提供了一系列的Animator,比如FadeInAnimator,ScaleInAnimator。
  • 其次,如果该库中没有你满意的动画,该库提供了BaseItemAnimator类,该类继承自SimpleItemAnimator,进一步封装了自定义Item Animator的代码,使得自定义Item Animator更方便,你只需要关注动画本身。如果要实现DefaultItemAnimator的代码,只需要以下实现:
public class DefaultItemAnimator extends BaseItemAnimator {

  public DefaultItemAnimator() {
  }

  public DefaultItemAnimator(Interpolator interpolator) {
    mInterpolator = interpolator;
  }

  @Override protected void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
    ViewCompat.animate(holder.itemView)
        .alpha(0)
        .setDuration(getRemoveDuration())
        .setListener(new DefaultRemoveVpaListener(holder))
        .setStartDelay(getRemoveDelay(holder))
        .start();
  }

  @Override protected void preAnimateAddImpl(RecyclerView.ViewHolder holder) {
    ViewCompat.setAlpha(holder.itemView, 0); //透明度先变为0
  }

  @Override protected void animateAddImpl(final RecyclerView.ViewHolder holder) {
    ViewCompat.animate(holder.itemView)
        .alpha(1)
        .setDuration(getAddDuration())
        .setListener(new DefaultAddVpaListener(holder))
        .setStartDelay(getAddDelay(holder))
        .start();
  }
}

是不是比继承SimpleItemAnimator方便多了。

局部刷新闪屏问题解决

对于RecyclerView的Item Animator,有一个常见的坑就是“闪屏问题”。
这个问题的描述是:当Item视图中有图片和文字,当更新文字并调用notifyItemChanged()时,文字改变的同时图片会闪一下。这个问题的原因是当调用notifyItemChanged()时,会调用DefaultItemAnimator的animateChangeImpl()执行change动画,该动画会使得Item的透明度从0变为1,从而造成闪屏。

解决办法很简单,在rv.setAdapter()之前调用((SimpleItemAnimator)rv.getItemAnimator()).setSupportsChangeAnimations(false)禁用change动画

 

你可能感兴趣的:(【android】)