2、View动画进阶:组合动画(AnimationSet)、插值器(Interpolator)详解、自定义插值器实战、动画速率曲线分析
上一章我们把 View 动画的基础过了一遍。平移、缩放、旋转、透明度,单个动画玩起来挺顺手。但实际项目中,哪有这么简单?
你想想看,一个按钮弹出来,既要从小变大,又要从透明到可见,还得带点弹性效果。这时候,单打独斗的动画就不够用了。我们需要组合动画,还需要更精细的速率控制。
这一章,我们就来啃这两块硬骨头:AnimationSet 和 Interpolator。我会结合我踩过的坑,把这两个东西彻底讲透。
核心知识点速览
- AnimationSet 的两种使用姿势:XML 与代码
- 插值器的工作原理:输入 0~1,输出 0~1
- 系统内置插值器详解与选型
- 自定义插值器:数学公式与实战案例
- 动画速率曲线:如何用图表理解动画节奏
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 的 fillAfter 和 fillBefore 设置的是整个集合的默认值。如果你在子动画里单独设置了 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 动画实现一些看似复杂的交互效果。到时候见。