RecyclerView
的强大之处相信大家已经体验到了,在上一篇RecyclerView详解 —— 自定义分割线我们学习了如何定义分割线,本篇将介绍如何自定义动画。
Google为我们提供了一个默认的动画实现(DefaultItemAnimator
),当数据添加、删除、更新时,会触发默认的动画效果:
通过本篇的学习,我们可以得到下面的效果:
如果觉得自定义比较繁琐,Github上也有相关的动画实现:https://github.com/wasabeef/recyclerview-animators
接下来我们将一步步分析这个实现类,最后在它的基础上修改默认的动画效果。
先来看DefaultItemAnimator
中的几个重要的方法:
void runPendingAnimations()
:当有动画需要执行时调用。boolean isRunning()
:返回当前是否有动画正在运行。boolean animateAdd()
:添加元素时调用,通常返回true。boolean animateRemove()
:移除数据时调用。boolean animateMove()
:列表项位置移动时调用。boolean animateChange()
:列表项数据发生改变时调用。void endAnimation()
:当某个动画需要被立即停止时调用,这里一般做视图的状态恢复。void endAnimations()
作用同上,区别是停止多个动画时调用。认识了八个主要的方法,我们再来看看具体的动画是如何实现的,我们从添加元素动画开始(其他动画的运行流程基本类似),当有新的元素添加进来时,首先调用animateAdd(ViewHolder holder)
:
@Override
public boolean animateAdd(final ViewHolder holder) {
resetAnimation(holder); // 该方法最终会调用endAnimation()
ViewCompat.setAlpha(holder.itemView, 0);
mPendingAdditions.add(holder);
return true;
}
开始动画之前需要取消之前正在播放的动画,同时将Item的状态设置为动画的终结状态:
@Override
public void endAnimation(ViewHolder item) {
// 省略部分代码...
// 遍历添加动画的队列,逐个移除队列,同时恢复Item的状态
for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
ArrayList additions = mAdditionsList.get(i);
if (additions.remove(item)) {
ViewCompat.setAlpha(view, 1); // 使用ViewCompat是为了向下兼容
dispatchAddFinished(item);
if (additions.isEmpty()) {
mAdditionsList.remove(i);
}
}
}
}
}
由于这是一个淡出动画,所以在Item出现之前需要设置Alpha
为0,然后将需要播放动画的对象传入动画播放队列,紧接着调用runPendingAnimations()
:
@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) {
return;
}
// 省略部分代码...
if (additionsPending) {
final ArrayList additions = new ArrayList<>();
additions.addAll(mPendingAdditions);
mAdditionsList.add(additions);
mPendingAdditions.clear();
// 在另一个线程中开启动画
Runnable adder = new Runnable() {
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);
} else {
adder.run();
}
}
}
我们接着看Runnable
中animateAddImpl(holder)
这个方法的实现:
private void animateAddImpl(final ViewHolder holder) {
final View view = holder.itemView;
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
mAddAnimations.add(holder);
animation.setDuration(getAddDuration())
.alpha(1).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchAddStarting(holder); // 通知动画开始
}
/**
* 如果是淡出的删除动画,需要在这里将Item的状态恢复正常
* ViewCompat.setAlpha(view, 1)
*/
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
dispatchAddFinished(holder); // 通知动画结束
mAddAnimations.remove(holder); // 从动画队列中移除
dispatchFinishedWhenDone();
}
}).start();
}
/**
* 如果当前没有正在播放的动画,则通知所有动画播放完毕
*/
private void dispatchFinishedWhenDone() {
if (!isRunning()) {
dispatchAnimationsFinished();
}
}
至此我们大致走了一遍添加元素动画的流程,回过头来看,如果说我们需要在这个基础添加一个从缩放动画只需要修改几个个地方即可:
/**
* 设置初始状态
*/
@Override
public boolean animateAdd(final ViewHolder holder) {
resetAnimation(holder);
ViewCompat.setAlpha(holder.itemView, 0);
// 设置动画播放之前的状态,当前设置大小为零,即不可见状态
ViewCompat.setScaleX(holder.itemView, 0);
ViewCompat.setScaleY(holder.itemView, 0);
mPendingAdditions.add(holder);
return true;
}
/**
* 设置终止状态
*/
private void animateAddImpl(final ViewHolder holder) {
// 省略部分代码...
animation.setDuration(getAddDuration())
.scaleX(1).scaleY(1) // 终止状态
.alpha(1)
.setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchAddStarting(holder);
}
}
// 省略部分代码...
}
好了,我们看看运行的效果如何:
可以看到, 当第一个动画紧接着第一个动画播放时,第一个动画就停了,而且一直保持这个状态,我们再往下滑动看看:
因为Item的复用,导致下面的Item也出现同样的问题。
前面我们说过开始动画之前需要取消之前正在播放的动画,同时将Item的状态设置为动画的终结状态,我们增加缩放动画时设置初始状态和终止状态,并没有做其他处理,这里还需要再修改两个部分:
/**
* 在新的动画开始之前,将正在播放的动画移除,并将Item恢复至正常状态
*/
@Override
public void endAnimation(ViewHolder item) {
// 省略部分代码...
// 遍历添加动画的队列,逐个移除队列,同时恢复Item的状态
for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
ArrayList additions = mAdditionsList.get(i);
if (additions.remove(item)) {
ViewCompat.setAlpha(view, 1);
ViewCompat.setScaleX(view,1);
ViewCompat.setScaleY(view,1);
dispatchAddFinished(item);
if (additions.isEmpty()) {
mAdditionsList.remove(i);
// 省略部分代码...
}
}
/**
* 当动画被取消时,也需要恢复至正常状态
*/
private void animateAddImpl(final ViewHolder holder) {
// 省略部分代码...
animation.setDuration(getAddDuration())
.scaleX(1).scaleY(1)
.alpha(1)
.setListener(new VpaListenerAdapter() {
@Override
public void onAnimationCancel(View view) {
ViewCompat.setAlpha(view, 1);
// 恢复至原始大小
ViewCompat.setScaleX(view, 1);
ViewCompat.setScaleY(view, 1);
}
}
// 省略部分代码...
}
自定义删除、移动、更新动画的流程与上面大致相同,结合旋转,平移,缩放等可以实现丰富的动画效果。
源码下载