在实际开发中,我们想要让自己的界面变得更加酷炫,免不了加入动画这个元素,而RecyclerView能够通过setItemAnimator(ItemAnimator animator)设置添加、删除、移动、改变的动画效果。RecyclerView提供了默认的ItemAnimator实现类:DefaultItemAnimator。
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
类,该类提供了一系列更易懂的API
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()时被调用。对于以上四个方法,注意两点:
runPendingAnimations()
中)需要调用dispatchXxxStarting(holder)
,执行完后需要调用dispatchXxxFinished(holder)
。runPendingAnimations()
中一并执行。现在我们看一下DefaultItemAnimator的成员变量:
private ArrayList<ViewHolder> mPendingRemovals = new ArrayList();
private ArrayList<ViewHolder> mPendingAdditions = new ArrayList(); //存放下一帧要执行一系列add动画
private ArrayList<DefaultItemAnimator.MoveInfo> mPendingMoves = new ArrayList();
private ArrayList<DefaultItemAnimator.ChangeInfo> mPendingChanges = new ArrayList();
ArrayList<ArrayList<ViewHolder>> mAdditionsList = new ArrayList(); //存放正在执行的一批add动画
ArrayList<ArrayList<DefaultItemAnimator.MoveInfo>> mMovesList = new ArrayList();
ArrayList<ArrayList<DefaultItemAnimator.ChangeInfo>> mChangesList = new ArrayList();
ArrayList<ViewHolder> mAddAnimations = new ArrayList(); //存放当前正在执行的add动画
ArrayList<ViewHolder> mMoveAnimations = new ArrayList();
ArrayList<ViewHolder> mRemoveAnimations = new ArrayList();
ArrayList<ViewHolder> mChangeAnimations = new ArrayList();
DefaultItemAnimator实现了SimpleItemAnimator的animateAdd()方法,该方法只是将该item添加到mPendingAdditions中,等到runPendingAnimations()中执行。
public boolean animateAdd(ViewHolder holder) {
this.resetAnimation(holder); //重置清空所有动画
holder.itemView.setAlpha(0.0F); //将要做动画的View先变成透明
this.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) {
additions = new ArrayList();
additions.addAll(this.mPendingAdditions);
this.mAdditionsList.add(additions);
this.mPendingAdditions.clear();
adder = new Runnable() {
public void run() {
Iterator var1 = additions.iterator();
while(var1.hasNext()) {
ViewHolder holder = (ViewHolder)var1.next();
DefaultItemAnimator.this.animateAddImpl(holder); //执行动画的方法
}
additions.clear();
DefaultItemAnimator.this.mAdditionsList.remove(additions);
}
};
if (!removalsPending && !movesPending && !changesPending) {
adder.run();
} else {
long removeDuration = removalsPending ? this.getRemoveDuration() : 0L;
long moveDuration = movesPending ? this.getMoveDuration() : 0L;
long changeDuration = changesPending ? this.getChangeDuration() : 0L;
long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
View view = ((ViewHolder)additions.get(0)).itemView;
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
}
}
}
为了防止在执行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 ViewPropertyAnimator animation = view.animate();
this.mAddAnimations.add(holder);
animation.alpha(1.0F).setDuration(this.getAddDuration()).setListener(new AnimatorListenerAdapter() {
public void onAnimationStart(Animator animator) {
DefaultItemAnimator.this.dispatchAddStarting(holder); //在开始add动画前调用
}
public void onAnimationCancel(Animator animator) {
view.setAlpha(1.0F);
}
public void onAnimationEnd(Animator animator) {
animation.setListener((AnimatorListener)null);
DefaultItemAnimator.this.dispatchAddFinished(holder); //在结束add动画后调用
DefaultItemAnimator.this.mAddAnimations.remove(holder);
DefaultItemAnimator.this.dispatchFinishedWhenDone(); //结束所有动画后调用
}
}).start();
}
从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();
}
}
对于RecyclerView的Item Animator,有一个常见的坑就是”闪屏问题”。这个问题的描述是:当Item视图中有图片和文字,当更新文字并调用notifyItemChanged()时,文字改变的同时图片会闪一下。这个问题的原因是当调用notifyItemChanged()时,会调用DefaultItemAnimator的animateChangeImpl()执行change动画,该动画会使得Item的透明度从0变为1,从而造成闪屏。
解决办法很简单,在rv.setAdapter()之前调用((SimpleItemAnimator)rv.getItemAnimator()).setSupportsChangeAnimations(false)禁用change动画。