一、动画规格:让动画有「灵魂」的四种配方

各位同学,今天我们来聊聊 Compose 动画里最核心的一个概念——动画规格

说白了,动画规格就是告诉系统:「你该怎么动?动多久?用什么样的节奏?」

我刚开始从 View 动画转到 Compose 时,最不习惯的就是这个。View 动画里你写个 setDuration(300) 就完事了,但 Compose 给了你四种选择:TweenSpecSpringSpecKeyframesSpecRepeatableSpec。每种都有自己的脾气。

嗯,咱们一个一个来看。

核心要点:动画规格决定了动画的「运动曲线」和「持续时间」。选对了,动画就自然;选错了,用户会觉得卡顿或突兀。

1.1 TweenSpec —— 最常用的「线性插值」

TweenSpec 是 Compose 动画的默认选项。它的原理很简单:在起始值和结束值之间,按照时间均匀地插值。

但「均匀」不代表「匀速」。你可以通过 easing 参数来控制加速和减速。

// 基础用法
val alpha by animateFloatAsState(
    targetValue = if (visible) 1f else 0f,
    animationSpec = tween(
        durationMillis = 300,
        delayMillis = 0,
        easing = LinearEasing
    )
)

// 我个人习惯用 FastOutSlowInEasing
// 它让动画看起来更自然
val offset by animateDpAsState(
    targetValue = if (expanded) 100.dp else 0.dp,
    animationSpec = tween(
        durationMillis = 500,
        easing = FastOutSlowInEasing
    )
)

我在项目中遇到过一个问题:用 LinearEasing 做弹窗出现动画,用户总觉得「太机械」。后来换成 FastOutSlowInEasing,感觉立马不一样了。

Easing 类型 效果 适用场景
LinearEasing 匀速运动 进度条、加载指示器
FastOutSlowInEasing 先快后慢 按钮点击、卡片展开
FastOutLinearInEasing 先快后匀速 列表滑动
SlowOutLinearInEasing 先慢后匀速 页面进入

小技巧:如果你不确定用哪个 easing,直接用 FastOutSlowInEasing 准没错。它是最接近物理世界的运动曲线。

1.2 SpringSpec —— 像弹簧一样自然

SpringSpec 是我个人最喜欢的动画规格。它模拟了弹簧的物理特性——有弹性、有阻尼、会回弹。

你不需要设置时长,因为弹簧动画的时长是由物理参数决定的。

// 弹簧动画
val scale by animateFloatAsState(
    targetValue = if (pressed) 0.9f else 1f,
    animationSpec = spring(
        dampingRatio = Spring.DampingRatioMediumBouncy,
        stiffness = Spring.StiffnessLow
    )
)

// 参数说明
// dampingRatio: 阻尼比,0.0 表示无限弹跳,1.0 表示无弹跳
// stiffness: 刚度,值越大弹簧越硬,运动越快

我曾经做过一个点赞动画,用 tween 做总觉得「死板」。换成 spring 后,按钮按下去会轻微弹跳,用户反馈说「手感很好」。

阻尼比 效果
DampingRatioHighBouncy 高弹跳,像橡皮球
DampingRatioMediumBouncy 中等弹跳,像鼠标点击
DampingRatioLowBouncy 低弹跳,像金属碰撞
DampingRatioNoBouncy 无弹跳,像阻尼门

注意:弹簧动画没有固定的时长。如果 stiffness 设置得太低,动画可能会持续好几秒。我建议在交互反馈中使用,不要用在页面转场这种需要精确控制时长的场景。

1.3 KeyframesSpec —— 自定义关键帧

有时候,你希望动画在某个时间点「停一下」或者「加速一下」。这时候就需要 KeyframesSpec 了。

它允许你定义多个关键帧,每个关键帧指定一个进度和对应的值。

// 关键帧动画
val offset by animateDpAsState(
    targetValue = if (expanded) 200.dp else 0.dp,
    animationSpec = keyframes {
        // 0ms 时值为 0.dp
        durationMillis = 1000
        0.dp at 0
        // 300ms 时值到 100.dp(快速)
        100.dp at 300
        // 700ms 时值到 150.dp(慢速)
        150.dp at 700
        // 1000ms 时值到 200.dp(再加速)
        200.dp at 1000
    }
)

你想想看,这种动画在 View 系统里得写 AnimatorSet 或者 PropertyValuesHolder,代码量翻倍。Compose 里一个 keyframes 就搞定了。

我记得有个项目要做「弹窗从底部滑入,到中间后轻微弹跳一下再停住」。用 keyframes 配合 spring 做中间段,效果非常棒。

关键帧的妙用:你可以把 keyframes 想象成动画的「路书」——每个时间点该到哪,清清楚楚。适合做复杂的序列动画。

1.4 RepeatableSpec —— 让动画循环起来

加载动画、呼吸灯、脉冲效果……这些都需要循环播放。RepeatableSpec 就是干这个的。

// 循环动画
val alpha by animateFloatAsState(
    targetValue = if (loading) 0.3f else 1f,
    animationSpec = repeatable(
        iterations = RepeatMode.Restart,  // 无限循环
        animation = tween(1000),
        repeatMode = RepeatMode.Reverse   // 往返
    )
)

// 有限次循环
val scale by animateFloatAsState(
    targetValue = if (pulsing) 1.2f else 1f,
    animationSpec = repeatable(
        iterations = 3,  // 循环3次
        animation = tween(200),
        repeatMode = RepeatMode.Restart
    )
)

RepeatMode 有两种:

  • Restart:每次循环都从起始值开始
  • Reverse:正向播放完再反向播放,像钟摆一样

我曾经犯过一个错:用 RepeatMode.Restart 做加载动画,结果每次循环都「跳」一下。换成 Reverse 后,动画就平滑了。

避坑指南:无限循环动画要注意性能。如果动画涉及大量重绘,建议用 infiniteTransition 配合 rememberInfiniteTransition,而不是在 animateXAsState 里用 repeatable

知识体系总览

下面这张图帮你理清四种动画规格的关系和适用场景:

Compose 动画规格体系 AnimationSpec TweenSpec SpringSpec KeyframesSpec RepeatableSpec 线性插值 可设时长 支持 easing 物理模拟 无固定时长 自然弹跳 自定义路径 多段控制 精确时间点 循环播放 支持往返 可设次数 如何选择? 简单动画 → TweenSpec 自然交互 → SpringSpec 复杂序列 → KeyframesSpec 循环效果 → RepeatableSpec

实战建议

说了这么多,我总结一下在实际项目中的选择策略:

  1. 按钮点击反馈:用 SpringSpec,阻尼比选 MediumBouncy,手感最好
  2. 页面转场:用 TweenSpec,时长控制在 300-500ms,easing 用 FastOutSlowIn
  3. 加载动画:用 RepeatableSpec + TweenSpec,配合 Reverse 模式
  4. 复杂交互:比如拖拽后回弹,用 SpringSpecdampingRatio 调出「韧性」感
  5. 品牌动画:用 KeyframesSpec 精确控制每个时间点的状态

记住:动画不是越花哨越好。好的动画应该让用户感觉「自然」,而不是「炫技」。我见过太多项目因为动画过度导致用户晕眩的案例了。

好了,四种动画规格就讲到这里。你可能会问:「那我怎么知道用哪个?」我的建议是——先从 TweenSpec 开始,如果觉得不够自然,换成 SpringSpec。这两个覆盖了 80% 的场景。

下一节我们会深入 animateXAsStateAnimatable 的底层原理,看看 Compose 是如何高效驱动这些动画规格的。


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