14、高级动画:Animatable、AnimationSpec、关键帧动画、无限循环动画

动画,是让 UI 活起来的灵魂。

前面我们聊过 Compose 的基础动画,比如 animate*AsState。说实话,那些 API 用起来确实方便,但如果你做过稍微复杂点的交互,就会发现它们有点「不够用」。比如,你想让一个动画分阶段执行,或者让它无限旋转,又或者想精细控制每一帧的变化——这时候,就该轮到高级动画登场了。

14.1 Animatable:动画的「底层引擎」

Animatable 是什么?你可以把它理解成一个「可被动画驱动的值容器」。它不像 animateFloatAsState 那样绑定状态变化自动触发,而是给你一把钥匙,让你自己决定什么时候动、怎么动。

我个人习惯用 Animatable 的场景是:手势拖拽、自定义进度条、或者需要手动触发动画的地方。

@Composable
fun AnimatableDemo() {
    val animatable = remember { Animatable(0f) }

    LaunchedEffect(Unit) {
        // 手动触发动画
        animatable.animateTo(
            targetValue = 100f,
            animationSpec = tween(1000)
        )
    }

    Text("当前值: ${animatable.value}")
}

你看,这里没有状态变量,没有 by 委托。就是单纯地创建一个 Animatable,然后调用 animateTo。它内部会自己管理动画状态。

小技巧: 如果你需要同时控制多个属性(比如 x、y、alpha),可以创建多个 Animatable 实例。但要注意,别在重组中频繁创建,记得用 remember 包起来。

14.2 AnimationSpec:动画的「行为模板」

动画不只是「从 A 到 B」,更重要的是「怎么走」。是匀速?还是先快后慢?还是带点弹性?这些都由 AnimationSpec 决定。

Compose 内置了几种常用的 AnimationSpec

类型 说明 适用场景
tween 补间动画,可设置时长、延迟、缓动曲线 大多数常规动画
spring 弹簧效果,有弹性和阻尼 列表拖拽、弹性回弹
keyframes 关键帧动画,分阶段控制 复杂路径、分段动画
repeatable 重复执行指定次数 闪烁、脉冲效果
infiniteRepeatable 无限循环 加载旋转、呼吸灯

举个例子,我想让一个方块弹起来:

val offset = remember { Animatable(0f) }

LaunchedEffect(Unit) {
    offset.animateTo(
        targetValue = 200f,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )
}

嗯,这里要注意:spring 没有时长参数,因为它由物理参数决定。你调 dampingRatiostiffness,它自己算多久停。我在项目中做过一个下拉刷新,用 spring 做回弹效果,手感比 tween 好太多了。

14.3 关键帧动画:让动画「分段走」

有时候,动画不是一条直线走到底。比如,一个图标先快速放大,再慢慢缩小,最后弹一下。这种「分段控制」的需求,用 keyframes 最合适。

val scale = remember { Animatable(1f) }

LaunchedEffect(Unit) {
    scale.animateTo(
        targetValue = 1f,
        animationSpec = keyframes {
            durationMillis = 2000
            1.2f at 200   // 200ms 时放大到 1.2
            0.8f at 800   // 800ms 时缩小到 0.8
            1.0f at 1500  // 1500ms 时回到 1.0
            // 最后 500ms 默认缓动到目标值
        }
    )
}

你想想看,这种写法是不是很直观?每个时间点设定一个值,剩下的交给 Compose 去插值。我曾经在做一个点赞动画时,就用 keyframes 实现了「心跳」效果——先放大、再缩小、再弹回,用户反馈说「那个心像真的一样跳了一下」。

避坑指南: 关键帧的时间点是相对 durationMillis 的,不是绝对时间。如果你设了 durationMillis = 2000,那所有 at 的值都不能超过 2000。我曾经犯过这个错,动画跑到一半突然跳回起点,排查了半天才发现是时间写超了。

14.4 无限循环动画:转起来就别停

加载中的转圈、呼吸灯、脉冲按钮——这些场景需要动画一直跑。用 infiniteRepeatable 就对了。

val infiniteTransition = rememberInfiniteTransition()
val rotation by infiniteTransition.animateFloat(
    initialValue = 0f,
    targetValue = 360f,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Restart
    )
)

Image(
    painter = painterResource(R.drawable.ic_loading),
    contentDescription = "加载中",
    modifier = Modifier.rotate(rotation)
)

这里有个细节:repeatMode 有两种——RestartReverseRestart 是从头再来,Reverse 是正着走完再倒着走。比如呼吸灯效果,用 Reverse 会更自然。

我个人更推荐用 rememberInfiniteTransition 配合 animateFloat 来做无限循环,而不是自己写 LaunchedEffect 加 while 循环。因为前者是 Compose 原生支持的,性能更好,也不会因为重组而中断。

14.5 知识体系总览

下面这张图,是我梳理的高级动画知识结构。你可以把它当作一张「地图」,遇到动画需求时,先看属于哪一类,再选对应的 API。

高级动画体系 Animatable 手动控制动画启停 不依赖状态变化 适合手势/自定义交互 AnimationSpec tween / spring / keyframes repeatable / infiniteRepeatable 定义动画行为模板 关键帧动画 分段控制动画值 时间点 + 目标值 适合复杂路径 无限循环动画 rememberInfiniteTransition Restart / Reverse 模式 加载、呼吸灯、脉冲

14.6 组合实战:一个带弹性效果的加载动画

光说不练假把式。我们结合 AnimatablekeyframesinfiniteRepeatable,做一个「弹跳加载球」。

@Composable
fun BouncingLoadingBall() {
    val scale = remember { Animatable(1f) }

    LaunchedEffect(Unit) {
        scale.animateTo(
            targetValue = 1f,
            animationSpec = infiniteRepeatable(
                animation = keyframes {
                    durationMillis = 1200
                    1.5f at 200   // 快速放大
                    0.8f at 600   // 缩小
                    1.2f at 900   // 弹回
                    1.0f at 1200  // 归位
                },
                repeatMode = RepeatMode.Restart
            )
        )
    }

    Box(
        modifier = Modifier
            .size(60.dp)
            .graphicsLayer(scaleX = scale.value, scaleY = scale.value)
            .background(Color(0xFF4A90D9), CircleShape)
    )
}

这个动画看起来就像一个小球在「呼吸」——先膨胀、再收缩、再弹一下。用户等待加载时,看到这种有节奏的动画,会比死板的转圈更有耐心。

核心要点回顾:
  • Animatable 是手动控制动画的基石,适合复杂交互
  • AnimationSpec 决定动画的「走法」,选对类型事半功倍
  • 关键帧动画用 keyframes 分段控制,时间点要小于 durationMillis
  • 无限循环用 rememberInfiniteTransition,别自己写 while 循环

说实话,动画这东西,光看文档是学不会的。你得动手调参数,感受 spring 的弹性、keyframes 的节奏。我每次做动画,都会花不少时间微调那几个数字——但看到最终效果流畅自然时,那种成就感,值得。


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