2、View动画进阶:组合动画(AnimationSet)、插值器(Interpolator)详解、自定义插值器实战、动画速率曲线分析

上一章我们把 View 动画的基础过了一遍。平移、缩放、旋转、透明度,单个动画玩起来挺顺手。但实际项目中,哪有这么简单?

你想想看,一个按钮弹出来,既要从小变大,又要从透明到可见,还得带点弹性效果。这时候,单打独斗的动画就不够用了。我们需要组合动画,还需要更精细的速率控制。

这一章,我们就来啃这两块硬骨头:AnimationSetInterpolator。我会结合我踩过的坑,把这两个东西彻底讲透。

核心知识点速览

  • AnimationSet 的两种使用姿势:XML 与代码
  • 插值器的工作原理:输入 0~1,输出 0~1
  • 系统内置插值器详解与选型
  • 自定义插值器:数学公式与实战案例
  • 动画速率曲线:如何用图表理解动画节奏
View 动画进阶知识体系 AnimationSet 组合动画 • XML 方式:set 标签包裹多个动画 • 代码方式:AnimationSet 对象 addAnimation • 共享插值器 vs 独立插值器 • startOffset 控制播放时序 • fillAfter/fillBefore 保持结束状态 ⚠ 注意:AnimationSet 的插值器优先级 ⚠ 注意:不要用 AnimationSet 做复杂链式 Interpolator 插值器 • 输入 t ∈ [0,1] → 输出 f(t) ∈ [0,1] • 内置:Linear / Accelerate / Decelerate • 内置:Overshoot / Bounce / Anticipate • 自定义:实现 Interpolator 接口 • 自定义:使用 PathInterpolator ✓ 实战:弹性效果自定义插值器 ✓ 实战:速率曲线可视化分析

2.1 AnimationSet:把多个动画装进一个篮子

AnimationSet 说白了就是一个动画容器。你把几个动画丢进去,它可以同时播放,也可以按顺序播放。我最早接触它是在做一个登录页面的欢迎动画——Logo 要旋转着放大,同时透明度从 0 变到 1。那时候我还在用 Handler 发消息一个个启动动画,后来发现 AnimationSet 一行代码就搞定了。

2.1.1 XML 方式:最直观

在 res/anim/ 目录下建一个 XML 文件,用 标签包裹多个动画。这是最推荐的方式,因为可读性强,也方便设计师调整参数。

<!-- res/anim/logo_enter.xml -->
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:shareInterpolator="false"
    android:fillAfter="true">

    <alpha
        android:fromAlpha="0.0"
        android:toAlpha="1.0"
        android:duration="800"
        android:interpolator="@android:anim/decelerate_interpolator" />

    <scale
        android:fromXScale="0.3"
        android:toXScale="1.0"
        android:fromYScale="0.3"
        android:toYScale="1.0"
        android:pivotX="50%"
        android:pivotY="50%"
        android:duration="1000"
        android:interpolator="@android:anim/overshoot_interpolator" />

    <rotate
        android:fromDegrees="0"
        android:toDegrees="360"
        android:pivotX="50%"
        android:pivotY="50%"
        android:duration="1200"
        android:startOffset="200"
        android:interpolator="@android:anim/linear_interpolator" />
</set>

💡 我的习惯:XML 里我一般把 shareInterpolator 设为 false。为什么?因为每个子动画的节奏往往不一样——透明度适合减速,缩放适合过冲,旋转用线性就好。共享插值器反而限制了灵活性。

2.1.2 代码方式:动态控制更灵活

有些场景下动画参数是运行时才能确定的,比如根据屏幕尺寸调整缩放比例。这时候就得用代码创建 AnimationSet。

val alphaAnim = AlphaAnimation(0f, 1f).apply {
    duration = 800
    interpolator = DecelerateInterpolator()
}

val scaleAnim = ScaleAnimation(
    0.3f, 1.0f, 0.3f, 1.0f,
    Animation.RELATIVE_TO_SELF, 0.5f,
    Animation.RELATIVE_TO_SELF, 0.5f
).apply {
    duration = 1000
    interpolator = OvershootInterpolator()
}

val rotateAnim = RotateAnimation(
    0f, 360f,
    Animation.RELATIVE_TO_SELF, 0.5f,
    Animation.RELATIVE_TO_SELF, 0.5f
).apply {
    duration = 1200
    startOffset = 200
    interpolator = LinearInterpolator()
}

val animSet = AnimationSet(false).apply {
    fillAfter = true
    addAnimation(alphaAnim)
    addAnimation(scaleAnim)
    addAnimation(rotateAnim)
}

view.startAnimation(animSet)

⚠ 我曾经踩过的坑:AnimationSet 的 fillAfterfillBefore 设置的是整个集合的默认值。如果你在子动画里单独设置了 fillAfter,它会被 AnimationSet 的对应设置覆盖掉。所以,要么统一在 Set 级别设置,要么每个子动画都显式声明——别问我怎么知道的,调试了一下午才发现的。

2.2 插值器(Interpolator)详解

插值器这个东西,理解它的本质其实很简单:输入一个 0 到 1 之间的进度值,输出一个 0 到 1 之间的实际值

举个例子。动画执行到 50% 的时间点,输入 t=0.5。如果是线性插值器,输出也是 0.5,动画匀速播放。如果是加速插值器,输出可能只有 0.3,意味着动画前半段走得慢,后半段才加速。

我刚开始做动画时,总觉得插值器是个黑盒。后来我画了一张速率曲线图,一下子就明白了——说白了就是 时间与进度的映射关系

2.2.1 系统内置插值器一览

插值器名称 类名 效果描述 适用场景
线性 LinearInterpolator 匀速运动,速率恒定 旋转、无限循环动画
加速 AccelerateInterpolator 开始慢,越来越快 物体掉落、弹出效果
减速 DecelerateInterpolator 开始快,越来越慢 淡入、卡片进入
加速减速 AccelerateDecelerateInterpolator 两头慢,中间快 平滑过渡、自然运动
过冲 OvershootInterpolator 超过目标值再回弹 缩放弹入、按钮点击
回弹 BounceInterpolator 像皮球落地一样弹跳 通知弹出、列表项出现
预备 AnticipateInterpolator 先后退再前进 投掷动作、蓄力效果
预备过冲 AnticipateOvershootInterpolator 先后退,再超过目标,再回弹 复杂物理模拟

💡 选型建议:我个人习惯是,进入动画用 Decelerate退出动画用 Accelerate。为什么?因为人眼对进入的东西更敏感,希望它一开始就吸引注意力;退出时则希望它快速消失,不要拖泥带水。

2.3 自定义插值器实战

系统内置的插值器虽然够用,但有时候就是差那么一点感觉。比如我想要一个「弹性缩放」效果——放大到 110% 再回弹到 100%,OvershootInterpolator 能做到,但它的回弹次数和幅度是固定的。我想控制得更精细,就得自己写。

2.3.1 实现 Interpolator 接口

自定义插值器只需要实现一个方法:getInterpolation(float input)。input 是 0~1 的时间进度,返回值是 0~1 的实际进度。注意,返回值可以超出 0~1 范围——这就是过冲和回弹效果的来源。

class ElasticInterpolator(private val amplitude: Float = 0.1f,
                          private val period: Float = 0.4f) : Interpolator {

    override fun getInterpolation(input: Float): Float {
        // 基于阻尼正弦波实现弹性效果
        if (input == 0f) return 0f
        if (input >= 1f) return 1f

        val s = period / 4f
        val postFix = Math.pow(2.0, (-10f * input).toDouble()).toFloat()
        val sinInput = ((input - s) * (Math.PI * 2f / period).toFloat())
        val sinValue = Math.sin(sinInput.toDouble()).toFloat()

        return postFix * sinValue * amplitude + 1f
    }
}

这段代码看起来有点数学,其实核心就是 阻尼正弦波。指数衰减控制回弹幅度逐渐减小,正弦波控制回弹的节奏。amplitude 控制弹多高,period 控制弹多快。

实战案例:弹性按钮

我在做一个点赞动画时,用了这个 ElasticInterpolator。用户点击心形图标,图标先放大到 120%,然后回弹几次稳定在 100%。配合颜色变化,效果非常生动。关键代码就一行:

scaleAnim.interpolator = ElasticInterpolator(0.15f, 0.3f)

调整 amplitude 和 period 两个参数,就能调出完全不同的手感。amplitude 越大,弹得越夸张;period 越小,回弹频率越快。

2.3.2 使用 PathInterpolator

如果你不想写数学公式,Android 还提供了 PathInterpolator。你只需要在贝塞尔曲线上画几个点,系统自动帮你算插值。

// 创建一个先快后慢再快的插值器
val path = Path().apply {
    moveTo(0f, 0f)
    cubicTo(0.2f, 0.8f, 0.6f, 0.2f, 1f, 1f)
}
val pathInterpolator = PathInterpolator(path)

// 使用
scaleAnim.interpolator = pathInterpolator

PathInterpolator 的好处是可视化。你可以在纸上画一条曲线,横轴是时间,纵轴是进度,然后直接把曲线上的关键点填进去。我经常用这个来微调动画的「手感」——说白了就是让动画看起来更自然。

⚠ 注意:PathInterpolator 的路径必须满足两个条件:x 坐标单调递增,且起点在 (0,0),终点在 (1,1)。否则会抛出 IllegalArgumentException。我曾经传错了一个点,结果动画直接崩了——嗯,调试信息里明确写了原因,但当时没仔细看。

2.4 动画速率曲线分析

理解了插值器的原理,我们来看看怎么分析动画的速率曲线。说白了,就是画一张 时间-进度 图。

横轴是时间进度 t(0→1),纵轴是实际进度 f(t)。曲线的斜率就是瞬时速率。斜率越大,动画跑得越快。

  • 线性插值器:一条 45 度直线,斜率恒为 1。匀速。
  • 加速插值器:曲线下凹,开始斜率小,后面斜率大。先慢后快。
  • 减速插值器:曲线上凸,开始斜率大,后面斜率小。先快后慢。
  • 过冲插值器:曲线在 t=1 时超过 y=1,然后回落。超出部分就是过冲。
  • 回弹插值器:曲线在 y=1 附近来回震荡,振幅逐渐衰减。

我在项目中调试动画时,经常用一个小技巧:把插值器的输出值打印出来,用 Excel 或者在线工具画成折线图。一眼就能看出动画的节奏是否符合预期。

// 调试插值器输出
val interpolator = OvershootInterpolator(2.0f)
for (i in 0..100) {
    val t = i / 100f
    val output = interpolator.getInterpolation(t)
    Log.d("Interpolator", "t=$t, output=$output")
}

💡 我的经验:动画调优时,不要只看最终效果。把速率曲线画出来,你就能理性地分析问题。比如动画感觉「卡顿」,可能是某个时间点斜率突变太大;感觉「拖沓」,可能是减速阶段太长。曲线会告诉你真相。

好了,这一章的内容就到这里。AnimationSet 让你能把多个动画组合起来,插值器让你能精确控制动画的节奏。这两个工具配合使用,基本上能覆盖 90% 的 View 动画需求。

下一章我们会进入更高级的话题——动画的监听、取消、复用,以及如何用 View 动画实现一些看似复杂的交互效果。到时候见。


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