一、动画规格:让动画有「灵魂」的四种配方
各位同学,今天我们来聊聊 Compose 动画里最核心的一个概念——动画规格。
说白了,动画规格就是告诉系统:「你该怎么动?动多久?用什么样的节奏?」
我刚开始从 View 动画转到 Compose 时,最不习惯的就是这个。View 动画里你写个 setDuration(300) 就完事了,但 Compose 给了你四种选择:TweenSpec、SpringSpec、KeyframesSpec、RepeatableSpec。每种都有自己的脾气。
嗯,咱们一个一个来看。
核心要点:动画规格决定了动画的「运动曲线」和「持续时间」。选对了,动画就自然;选错了,用户会觉得卡顿或突兀。
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。
知识体系总览
下面这张图帮你理清四种动画规格的关系和适用场景:
实战建议
说了这么多,我总结一下在实际项目中的选择策略:
- 按钮点击反馈:用
SpringSpec,阻尼比选MediumBouncy,手感最好 - 页面转场:用
TweenSpec,时长控制在 300-500ms,easing 用FastOutSlowIn - 加载动画:用
RepeatableSpec+TweenSpec,配合Reverse模式 - 复杂交互:比如拖拽后回弹,用
SpringSpec的dampingRatio调出「韧性」感 - 品牌动画:用
KeyframesSpec精确控制每个时间点的状态
记住:动画不是越花哨越好。好的动画应该让用户感觉「自然」,而不是「炫技」。我见过太多项目因为动画过度导致用户晕眩的案例了。
好了,四种动画规格就讲到这里。你可能会问:「那我怎么知道用哪个?」我的建议是——先从 TweenSpec 开始,如果觉得不够自然,换成 SpringSpec。这两个覆盖了 80% 的场景。
下一节我们会深入 animateXAsState 和 Animatable 的底层原理,看看 Compose 是如何高效驱动这些动画规格的。
公众号:蓝海资料掘金营,微信deep3321