我的CSDN: ListerCi
我的简书: 东方未曦
俗话说,好看的皮囊千篇一律,有趣的灵魂万里挑一。但是对于我们这些俗人来说,肯定是选择好看的皮囊,咱们的用户也是如此。你看看应用市场上那些花枝招展的APP,哪个不是用上了五花八门的动画效果,就算你的内在安全省电性能好,没点儿花招可留不住花心的用户。所以我们今天就来看看怎么实现让用户眼前一亮的动画,当然原理也很重要,因此源码分析必不可少,本文的源码分析主要聚焦于动画是怎么触发的,以及动画是怎么实现的。
一、动画的触发与实现
当Adapter中的数据发生变化时,我们通过notifyItemXXX()等方法通知RecyclerView来改变数据的展示,这个过程必然伴随新的layout()。如果在layout()后直接显示新数据,效果比较僵硬,因此需要通过动画来制造良好的用户体验。
那么,为了实现动画,RecyclerView又额外做了哪些工作呢?抽象上来讲,RecyclerView实现动画的步骤如下。
① 数据发生改变时,保存当前的item信息为preInfo
② 根据新的数据Layout
③ Layout完毕,保存当前的item信息为postInfo
④ 根据preInfo和postInfo判断动画类型并交给ItemAnimator执行
可以发现,前3步保存了执行动画所需要的信息,最后整体交给ItemAnimator来执行动画。前3步涉及到内容较为复杂,我们先从简单的开始分析,来看ItemAnimator是怎么实现动画的。
1.1 动画的实现
由于RecyclerView设计时的低耦合性,ItemAnimator只需要关注怎么执行动画即可,其逻辑并不复杂。RecyclerView为我们实现了DefaultItemAnimator,在不设置动画的情况下默认使用它,其中实现了4个针对item的动画,分别为Remove、Move、Add和Change。以Remove动画为例,当一个item被移出RecyclerView时,DefaultItemAnimator中的animateRemove(holder)方法就会被调用,但是并没有马上开始执行动画,而是将动画添加到了mPendingRemovals中,这是一个待执行的Romove动画List。
@Override
public boolean animateRemove(final RecyclerView.ViewHolder holder) {
resetAnimation(holder);
mPendingRemovals.add(holder);
return true;
}
看一下DefaultItemAnimator的成员变量,原来有4个List分别存储4种动画。
private ArrayList mPendingRemovals = new ArrayList<>();
private ArrayList mPendingAdditions = new ArrayList<>();
private ArrayList mPendingMoves = new ArrayList<>();
private ArrayList mPendingChanges = new ArrayList<>();
当RecyclerView要执行动画时,ItemAnimator的runPendingAnimations()方法会被调用,DefaultItemAnimator重写后的方法如下,为了便于阅读,添加了注释并省略了部分代码。
@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;
}
// 执行Remove动画
for (RecyclerView.ViewHolder holder : mPendingRemovals) {
animateRemoveImpl(holder); // 实际执行Remove动画的方法
}
mPendingRemovals.clear();
// Move动画
if (movesPending) {
// 注意: Move动画并不是马上执行,会放入一个Runnable
Runnable mover = new Runnable() {
@Override
public void run() {
for (MoveInfo moveInfo : moves) {
animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
moveInfo.toX, moveInfo.toY);
}
}
};
// 如果有Remove动画,就在Remove动画结束之后执行Move动画
// 如果没有Remove动画就马上执行
if (removalsPending) {
View view = moves.get(0).holder.itemView;
ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
} else {
mover.run();
}
}
// Change动画,与Move动画一起执行
if (changesPending) {
// ......
Runnable changer = new Runnable() {
@Override
public void run() {
for (ChangeInfo change : changes) {
animateChangeImpl(change);
}
}
};
if (removalsPending) {
RecyclerView.ViewHolder holder = changes.get(0).oldHolder;
ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
} else {
changer.run();
}
}
// 最后执行Add动画
if (additionsPending) {
// ......
Runnable adder = new Runnable() {
@Override
public void run() {
for (RecyclerView.ViewHolder holder : additions) {
animateAddImpl(holder);
}
additions.clear();
mAdditionsList.remove(additions);
}
};
// 在Remove、Move、Change动画都完成之后开始执行Add动画
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();
}
}
}
我们发现动画的执行是有顺序的,Remove动画首先执行,之后Move和Change动画同时开始,等这3个动画全部结束之后开始执行Add动画。以RecyclerView删除一个item为例,动画如下。
gif-Remove动画.gif
我们发现动画有2段,首先是被删除item的Remove动画,等到完全不可见之后,下方的多个item同时执行向上的Move动画。相对的,如果向RecyclerView中添加一个item,会先执行item向下的Move动画,再执行插入item的Add动画。
在runPendingAnimations()中真正执行动画的是animateRemoveImpl()这样的方法,来看一下它是怎么实现的,这也是我们今后自定义动画的关键。
private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
final View view = holder.itemView;
final ViewPropertyAnimator animation = view.animate();
mRemoveAnimations.add(holder);
animation.setDuration(getRemoveDuration()).alpha(0).setListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
dispatchRemoveStarting(holder);
}
@Override
public void onAnimationEnd(Animator animator) {
animation.setListener(null);
view.setAlpha(1);
dispatchRemoveFinished(holder);
mRemoveAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
Remove动画很简单,通过ViewPropertyAnimator实现一个透明度到0的属性动画,不过要记得在动画开始和结束时调用dispatchRemoveStarting()和dispatchRemoveFinished()方法。相对的,Add动画就是透