16、ItemAnimator:默认动画与自定义动画,让列表动起来

列表动起来,用户体验就上了一个台阶。你想想看,用户删掉一条数据,如果直接消失,是不是有点突兀?但如果有一个淡出加缩小的动画,感觉就自然多了。这就是 ItemAnimator 干的事。

说实话,我早期做 Android 开发时,对动画这块不太上心。觉得功能跑通就行,动画嘛,锦上添花而已。直到有一次做一款社交应用,用户反馈说「删除好友时感觉怪怪的,好像数据凭空消失了」。嗯,从那以后,我再也不敢小看 ItemAnimator 了。

16.1 默认动画:开箱即用

RecyclerView 自带了一套默认动画。你什么都不用配,它就有基本的添加、删除、移动效果。这套动画由 DefaultItemAnimator 提供。

它的核心行为是这样的:

  • 添加:新 item 从透明到不透明,同时有个轻微的缩放
  • 删除:item 淡出,同时高度塌缩
  • 移动:item 平移到新位置
  • 变更:旧 item 淡出,新 item 淡入

我个人习惯,在项目初期先用默认动画。够用,而且省事。你只需要确保调用了 notifyItemInserted()notifyItemRemoved() 这类方法,动画就会自动触发。

关键点:默认动画只在你使用「增量通知」时才会生效。如果你直接调 notifyDataSetChanged(),动画是不会触发的。这一点我踩过坑,后面会细说。

16.2 自定义动画:让列表更有灵魂

默认动画虽然省事,但有时候不够用。比如你想让 item 从左侧滑入,或者做一个弹跳效果。这时候就需要自定义 ItemAnimator 了。

自定义 ItemAnimator 的核心是继承 SimpleItemAnimatorRecyclerView.ItemAnimator。我建议你继承 SimpleItemAnimator,因为它帮你封装了一些模板代码,你只需要关注四个核心方法:

方法 作用
animateAdd() 处理 item 添加时的动画
animateRemove() 处理 item 删除时的动画
animateMove() 处理 item 移动时的动画
animateChange() 处理 item 内容变更时的动画

每个方法返回一个 boolean,表示你是否「接手」了这个动画。如果返回 false,RecyclerView 会认为你不处理,直接显示最终状态。

来看一个简单的例子。我想让 item 删除时,先缩小再淡出:

class ScaleFadeAnimator : SimpleItemAnimator() {

    override fun animateRemove(holder: RecyclerView.ViewHolder): Boolean {
        dispatchRemoveStarting(holder)
        val view = holder.itemView
        val animator = AnimatorSet().apply {
            playTogether(
                ObjectAnimator.ofFloat(view, "scaleX", 1f, 0f),
                ObjectAnimator.ofFloat(view, "scaleY", 1f, 0f),
                ObjectAnimator.ofFloat(view, "alpha", 1f, 0f)
            )
            duration = 300
            addListener(object : AnimatorListenerAdapter() {
                override fun onAnimationEnd(animation: Animator) {
                    dispatchRemoveFinished(holder)
                    // 记得重置,否则 item 复用时会出问题
                    view.scaleX = 1f
                    view.scaleY = 1f
                    view.alpha = 1f
                }
            })
        }
        animator.start()
        return true
    }

    // 其他方法类似,这里省略
}

小提示:动画结束后一定要重置 View 的状态。否则 item 被 RecyclerView 回收复用后,会出现奇怪的显示问题。我曾经因为这个 bug 被测试小姐姐追着问了一下午……

16.3 动画的生命周期管理

ItemAnimator 有一套完整的生命周期回调。你需要在合适的时机调用 dispatchAddStarting()dispatchAddFinished() 等方法。这些方法告诉 RecyclerView:「这个 item 的动画开始了」或「结束了」。

RecyclerView 内部会维护一个动画队列。如果你不调用 dispatchXxxFinished(),RecyclerView 会认为动画还在进行中,导致后续的动画被阻塞。说白了,就是卡住了。

我建议你遵循这个模式:

  1. 在动画开始前,调用 dispatchXxxStarting(holder)
  2. 执行你的自定义动画
  3. 在动画结束时,调用 dispatchXxxFinished(holder)
  4. 重置 View 的状态

16.4 增量通知:动画的触发器

刚才提到,动画需要增量通知才能触发。什么是增量通知?就是 notifyItemInserted()notifyItemRemoved()notifyItemMoved()notifyItemChanged() 这些方法。

与之相对的是 notifyDataSetChanged()。这个方法会刷新整个列表,但不会触发任何动画。为什么?因为 RecyclerView 不知道具体变化了什么,它只能重新绑定所有 item。

所以,如果你想让列表动起来,一定要用增量通知。配合 DiffUtil 使用效果更佳——DiffUtil 会自动计算出哪些 item 被添加、删除、移动或变更,然后帮你调用对应的 notify 方法。

注意:增量通知和 notifyDataSetChanged() 不要混用。如果你先调了 notifyItemInserted(),然后又调了 notifyDataSetChanged(),RecyclerView 会清空动画队列,直接刷新。我之前在项目里犯过这个错,动画时灵时不灵,排查了好久才发现是这个问题。

16.5 知识体系总览

下面这张图帮你梳理了 ItemAnimator 的核心脉络:

ItemAnimator 知识体系 ItemAnimator 默认动画 (DefaultItemAnimator) 自定义动画 (SimpleItemAnimator) 添加:淡入+缩放 删除:淡出+塌缩 animateAdd() animateRemove() 触发条件:增量通知 生命周期:dispatchXxx notifyItemInserted/Removed 动画结束后重置 View 状态 配合 DiffUtil 使用效果最佳

16.6 性能与注意事项

动画虽好,但别滥用。我见过一些项目,每个 item 的动画都搞得很复杂,结果列表滑动时掉帧严重。这里有几个建议:

  • 动画时长控制在 300ms 以内。太长了用户会觉得卡顿,太短了又没效果。300ms 是个黄金值。
  • 避免在动画中做耗时操作。比如网络请求、数据库读写。动画是在 UI 线程执行的,阻塞了就会掉帧。
  • 大量 item 同时动画时,考虑使用 supportsPredictiveItemAnimations()。这个方法返回 true 时,RecyclerView 会尝试预测动画的最终位置,提前布局。不过这个功能有点复杂,我建议你按需使用。
  • 动画和触摸事件不要冲突。如果 item 有点击或滑动删除功能,确保动画不会干扰触摸事件的传递。我遇到过一个问题:item 删除动画还没结束,用户又点击了同一个位置,结果触发了另一个 item 的事件。解决方案是在动画期间禁用 item 的交互。

个人经验:如果你只是想要一些简单的动画效果,比如淡入淡出、滑动,其实可以用 LayoutAnimationController 配合 RecyclerView 使用。它比自定义 ItemAnimator 简单得多,适合一次性动画场景。但如果你需要「增量更新时的动画」,那就必须用 ItemAnimator 了。

好了,关于 ItemAnimator 的内容就这些。记住一句话:动画是用户体验的润滑剂,不是主角。别让动画喧宾夺主,也别让列表死气沉沉。找到那个平衡点,你的列表就「活」了。

公众号:蓝海资料掘金营,微信 deep3321