自定义 RecyclerView 动画:手写 ItemAnimator 实现 Remove 飞出效果
列表里删一条数据时,我们常常希望它"飞出去"、后面的项平滑上移填补空位——这正是 RecyclerView 的 ItemAnimator 负责的事。但很多人照着写会发现:要么完全没动画,要么被删项飞出时下方 item 瞬间跳上去、和飞出项重叠闪烁。问题不在动画本身,而在于没理解 ItemAnimator "延迟批量执行"的调度机制,也没处理好 remove 与 move 两个动作的配合。本文先拆解系统默认 DefaultItemAnimator 的调度原理(参考 Piasy 的 ItemAnimator 深入解析 与 腾讯云原文),再手写一个 FlyAnimator,把 remove 飞出效果完整实现出来。
1、ItemAnimator 是什么
当 Adapter 数据变化(如 notifyItemRemoved)后,ItemAnimator 负责播放对应的增、删、改、移动画。真正落地的是抽象类 SimpleItemAnimator,它对外暴露 4 个"动作"回调和 4 个"调度/生命周期"方法:
// import androidx.recyclerview.widget.RecyclerView; 下文 ViewHolder 即 RecyclerView.ViewHolder
// 4 个动作回调:返回 true 表示"我要播动画,先别动 view,等统一调度"
boolean animateRemove(ViewHolder holder) // 移除
boolean animateAdd(ViewHolder holder) // 新增
boolean animateMove(ViewHolder holder, int fromX, int fromY, int toX, int toY) // 位移
boolean animateChange(ViewHolder oldH, ViewHolder newH, int fl,int ft,int tl,int tt) // 变化
// 调度 / 生命周期
void runPendingAnimations() // 统一启动被"占位"的动画
void endAnimation(ViewHolder) // 结束某个 item 的动画
void endAnimations() // 结束所有动画
boolean isRunning() // 是否还有动画在跑核心实现(回调约定):
1. animateXxx 只负责"收集",真正播放放在 runPendingAnimations,这是后面所有写法的基石
2. 返回 true 后,RecyclerView 不会立即把 view 移除或复位,而是等待动画结束回调再清理
3. 动画开始/结束必须调用 dispatchRemoveStarting / dispatchRemoveFinished 等回调,否则 RecyclerView 的状态机会错乱(item 显示异常、复用错位)
4. 全部动画结束后调用 dispatchAnimationsFinished() 收尾
2、DefaultItemAnimator 的调度原理
DefaultItemAnimator 是 RecyclerView 默认的 Animator,源码不长但极值得读,它的设计核心是四个字:延迟批量执行。
// 1. animateRemove:只入队,不执行
@Override
public boolean animateRemove(ViewHolder holder) {
mPendingRemoves.add(holder);
return true; // 告诉 RecyclerView:先别移除 view,等我动画
}
// 2. animateMove:先用 translation 把 view "钉"在旧位置,再入队
@Override
public boolean animateMove(ViewHolder holder, int fromX, int fromY, int toX, int toY) {
View view = holder.itemView;
view.setTranslationX(fromX - toX); // 钉回旧位置,避免瞬间跳到新位置
view.setTranslationY(fromY - toY);
mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
return true;
}
// 3. runPendingAnimations:批量调度,顺序 remove -> move -> change -> add
@Override
public void runPendingAnimations() {
// 先启动所有 remove(120ms 淡出,最该先消失)
// 接着启动 move(其余 item 上移填补空位)
// 再启动 change,最后延迟启动 add(等位置腾出来)
}
// remove 真正的动画:alpha 1 -> 0
ViewCompat.animate(view).alpha(0).setDuration(120L).start();核心实现(机制要点):
1. animateRemove 只把 holder add 进 mPendingRemoves 队列并 return true,绝不直接开动画
2. runPendingAnimations 统一取出 4 个 pending 队列逐个启动,顺序固定:remove 先于 move
3. move 用"钉旧位 + 归零"实现平滑过渡——这正是消除"瞬间跳变"的关键
4. 四个动画默认时长都是 120ms(CHANGE_DURATION / MOVE_DURATION / REMOVE_DURATION / ADD_DURATION)
- 结论:模仿这套"收集 + 批量 + 分工"结构,自定义动画就不会乱
3、动手:FlyAnimator 骨架
目标只做 remove(飞出)和 move(上移),add / change 不需要就"直接结束并 return false"。仿 DefaultItemAnimator,为 remove 和 move 各准备两个 list:一个收集 pending,一个记录运行中。
public class FlyAnimator extends SimpleItemAnimator {
// remove:待执行 + 运行中
private final List<ViewHolder> mPendingRemoves = new ArrayList<>();
private final List<ViewHolder> mRemoveAnims = new ArrayList<>();
// move:待执行 + 运行中
private final List<ViewHolder> mPendingMoves = new ArrayList<>();
private final List<ViewHolder> mMoveAnims = new ArrayList<>();
@Override public boolean animateRemove(ViewHolder holder) { /* 见第 4 节 */ return true; }
@Override public boolean animateMove(ViewHolder h, int fx,int fy,int tx,int ty) { /* 见第 5 节 */ return true; }
@Override public void runPendingAnimations() { /* 见第 4 节 */ }
// 不需要的动作:直接 dispatchXxxFinished 后 return false,告诉 RecyclerView 跳过
@Override public boolean animateAdd(ViewHolder h) {
dispatchAddFinished(h);
return false;
}
@Override public boolean animateChange(ViewHolder o, ViewHolder n, int fl,int ft,int tl,int tt) {
dispatchChangeFinished(n, false);
return false;
}
@Override public void endAnimation(ViewHolder item) { }
@Override public void endAnimations() { }
// isRunning 必须综合 4 个 list,全部为空才算动画结束
@Override public boolean isRunning() {
return !(mPendingRemoves.isEmpty() && mRemoveAnims.isEmpty()
&& mPendingMoves.isEmpty() && mMoveAnims.isEmpty());
}
}核心实现:
1. 继承 SimpleItemAnimator,4 个 animateXxx + runPendingAnimations + endXxx + isRunning 都要实现
2. 用两组 list分别管理 remove 与 move 的"待执行"和"运行中"状态
3. 不需要的动作直接 dispatchXxxFinished 后 return false,避免 RecyclerView 干等
4. isRunning 综合全部队列——它是 RecyclerView 判断"能否安全刷新"的依据,写漏会导致后续动画错乱
4、实现 remove 飞出动画
按 DefaultItemAnimator 的套路:animateRemove 只收集,runPendingAnimations 统一启动。remove() 用补间动画 TranslateAnimation 让 itemView 向右飞出。
// ① 收集:只入队,不执行
@Override
public boolean animateRemove(ViewHolder holder) {
mPendingRemoves.add(holder);
return true;
}
// ② 调度:批量启动
@Override
public void runPendingAnimations() {
if (!mPendingRemoves.isEmpty()) {
for (ViewHolder holder : mPendingRemoves) {
remove(holder);
}
mPendingRemoves.clear();
}
if (!mPendingMoves.isEmpty()) { // move 部分见第 5 节
for (ViewHolder holder : mPendingMoves) {
move(holder);
}
mPendingMoves.clear();
}
}
// ③ 执行:向右平移 1000px,模拟飞出屏幕
private void remove(final ViewHolder holder) {
mRemoveAnims.add(holder);
TranslateAnimation anim = new TranslateAnimation(0, 1000, 0, 0);
anim.setDuration(500);
anim.setAnimationListener(new Animation.AnimationListener() {
@Override public void onAnimationStart(Animation a) {
dispatchRemoveStarting(holder); // 必须:通知动画开始
}
@Override public void onAnimationEnd(Animation a) {
mRemoveAnims.remove(holder);
holder.itemView.clearAnimation();
dispatchRemoveFinished(holder); // 必须:通知动画结束
if (!isRunning()) {
dispatchAnimationsFinished(); // 全部结束,收尾
}
}
@Override public void onAnimationRepeat(Animation a) { }
});
holder.itemView.startAnimation(anim);
}核心实现:
1. animateRemove 把 holder 加入 mPendingRemoves 并 return true,真正动画延迟到 runPendingAnimations
2. runPendingAnimations 遍历 mPendingRemoves 逐个 remove(),结束后 clear,与 DefaultItemAnimator 完全一致
3. remove() 用 TranslateAnimation 让 itemView 向右飞出 1000px,duration 500ms
4. dispatchRemoveStarting / dispatchRemoveFinished 必须配对调用,否则 RecyclerView 误以为 view 还在动画、不予复用,导致列表卡死或错位
5. 每个 remove 结束都检查 isRunning(),为 false 才 dispatchAnimationsFinished() 收尾
5、修复"下方瞬间上移"的重叠闪烁
只做上一节的 remove 飞出,运行后会看到:被删项飞出时,下方 item 已经瞬间上移,和飞出项重叠闪烁。原因正如第 2 节所说——删除瞬间 RecyclerView 已重新布局,下方 item 跳到了新位置,而我们缺少 move 动画把它"钉回旧位再平滑滑回"。
// ① 钉回旧位置:删除瞬间下方 item 已跳到新位置,用 translationY 钉回旧视觉位
@Override
public boolean animateMove(ViewHolder holder, int fromX, int fromY, int toX, int toY) {
holder.itemView.setTranslationY(fromY - toY);
mPendingMoves.add(holder);
return true;
}
// ② 平滑上移归位:translationY 从当前值动画到 0
private void move(final ViewHolder holder) {
mMoveAnims.add(holder);
final View view = holder.itemView;
ObjectAnimator animator = ObjectAnimator.ofFloat(
view, "translationY", view.getTranslationY(), 0f);
animator.setDuration(500);
animator.addListener(new AnimatorListenerAdapter() {
@Override public void onAnimationStart(Animator a) {
dispatchMoveStarting(holder);
}
@Override public void onAnimationEnd(Animator a) {
mMoveAnims.remove(holder);
dispatchMoveFinished(holder);
if (!isRunning()) {
dispatchAnimationsFinished();
}
}
});
animator.start();
}核心实现:
1. animateMove 用 setTranslationY(fromY - toY) 把已上跳的 item 钉回原视觉位置,这是消除闪烁的第一步
2. move() 用 ObjectAnimator 把 translationY 平滑归零,呈现"上移填补空位"的效果
3. remove 飞出 + move 上移同时进行(都在 runPendingAnimations 里启动),视觉上就是"一条飞走、后面的跟上来"
4. dispatchMoveStarting / dispatchMoveFinished 同样不可省,否则 RecyclerView 误以为 item 还没就位
- 至此 remove + move 配合完成,重叠闪烁消失,飞出效果丝滑
6、设置使用与避坑
写好 Animator,三行就能用上;但有几个坑要避开:
// 1. 设置自定义 Animator
recyclerView.setItemAnimator(new FlyAnimator());
// 2. 可选:关闭 change 的交叉淡入淡出,减少列表闪烁(常用优化)
((SimpleItemAnimator) recyclerView.getItemAnimator())
.setSupportsChangeAnimations(false);
// 3. 删除数据时务必用 notifyItemRemoved,才会触发 remove + move 动画
list.remove(position);
adapter.notifyItemRemoved(position);
// 若删除导致后续多项位置变化,配合 notifyItemRangeChanged 让 move 动画覆盖到位
adapter.notifyItemRangeChanged(position, list.size() - position);核心实现(避坑要点):
1. notifyDataSetChanged() 不会触发逐项动画,必须用 notifyItemRemoved / notifyItemRangeChanged 等带位置信息的方法
2. setSupportsChangeAnimations(false) 关闭 change 时"新旧两个 item 交叉淡入淡出"的默认行为,是消除列表闪烁的常用手段
3. 补间动画(TranslateAnimation)只改绘制、不改 view 真实位置——飞出场景因 view 即将被回收影响较小,但若该 view 会被复用,务必在 onAnimationEnd 里 clearAnimation()
4. 想更稳妥可用属性动画(ObjectAnimator / ViewCompat.animate)做飞出,从根上避免点击位置错位
- 默认动画时长 120ms 偏短,自定义时按需调大(本文示例用 500ms 效果更明显)
7、总结
列表删除的"飞出 + 上移"不是两个独立动画,而是 ItemAnimator 的 remove 与 move 两个动作的配合。理解了 DefaultItemAnimator 的"收集 + 批量调度",自定义就只剩"把动画填进对应动作"这一步。
关键要点:
- animateXxx 只入队、return true,真正动画放 runPendingAnimations,这是 ItemAnimator 的通用写法
- dispatchRemoveStarting/Finished、dispatchMoveStarting/Finished 必须配对调用,否则 RecyclerView 状态错乱
- isRunning 要综合所有队列,全空才算动画结束
- 消除"下方瞬间上移"靠 move 的"钉旧位(setTranslationY)+ 平滑归零"
- 用 notifyItemRemoved 触发,notifyDataSetChanged 不触发逐项动画;补间动画记得 clearAnimation
对于做列表交互、想给删除/拖拽加自定义动效的开发者而言,掌握 ItemAnimator 的"收集 + 批量 + 分工"三段式写法是必要的——它不只是"写个动画",更是与 RecyclerView 复用机制正确协作的契约。把 remove 与 move 这对搭档写好,再复杂的列表动画都能在这个骨架上扩展出来。

