14、Compose动画之显式动画:Animatable的使用、AnimationSpec详解、动画的取消与重置

显式动画,说白了就是你自己掌控动画的每一个细节。跟隐式动画那种「我帮你搞定」的思路不同,显式动画把方向盘交到你手里——什么时候开始、持续多久、中间怎么变,全由你说了算。

我个人习惯把显式动画理解为「手动挡」。虽然操作起来比自动挡麻烦一点,但你能玩出的花样也多得多。今天我们就来聊聊 Compose 里最核心的显式动画工具:Animatable

Animatable:动画的底层引擎

Animatable 是 Compose 动画体系里最基础的动画值容器。它不像 animate*AsState 那样帮你自动触发,而是需要你手动调用 animateTosnapTo 来驱动。

先看一个最简单的例子:

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

    LaunchedEffect(Unit) {
        animatable.animateTo(100f, animationSpec = tween(1000))
    }

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

嗯,这里要注意:Animatable 必须用 remember 包裹,否则每次重组都会重新创建。我在项目中遇到过有人忘了加 remember,结果动画永远停在初始值——排查了半天才发现是这个问题。

AnimationSpec 详解:动画的「配方」

AnimationSpec 决定了动画怎么「动」。它不是一个具体的类,而是一组规范,Compose 内置了几种常用的实现:

类型 特点 适用场景
tween 线性插值,可设置时长和缓动曲线 大多数常规动画
spring 弹簧效果,有弹性和阻尼 需要自然回弹的交互
keyframes 关键帧动画,可定义多个时间点的值 复杂的分段动画
repeatable 重复执行某个动画 循环动画、加载指示器
infiniteRepeatable 无限重复 持续旋转、脉冲效果

你想想看,tween 就像匀速开车,spring 像撞到弹簧上弹回来,keyframes 则像分段导航——每个时间点设定一个目标值。

来个 spring 的例子:

val animatable = remember { Animatable(0f) }

LaunchedEffect(Unit) {
    animatable.animateTo(
        100f,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )
}

我曾经在做一个下拉刷新组件时,用了 spring 来实现回弹效果。阻尼比调得太小,结果列表弹得像果冻一样晃个不停——用户反馈说「这 App 是不是坏了」。后来我把 dampingRatio 调到 0.7 左右,效果才自然。

动画的取消与重置

显式动画的另一个优势是:你可以随时叫停。

Animatable 提供了几个关键方法:

  • animateTo:启动动画到目标值
  • snapTo:瞬间跳转到某个值,没有过渡
  • stop:立即停止当前动画

为什么要取消动画?最常见的场景是:用户快速滑动列表时,之前的动画还没结束,新的动画又开始了。如果不取消,两个动画会打架,导致值乱跳。

看这段代码:

val animatable = remember { Animatable(0f) }

// 用户点击时触发
scope.launch {
    // 先取消当前动画
    animatable.stop()
    // 再启动新动画
    animatable.animateTo(newValue)
}

这里有个坑:stop() 必须在协程里调用,因为它是一个挂起函数。我刚开始用的时候,直接在点击事件里调 stop(),编译器直接报错——嗯,挂起函数不能在非协程环境调用。

重置动画就更简单了:

// 重置到初始值
animatable.snapTo(initialValue)

snapTo 不会触发任何过渡,值瞬间改变。适合用来「复位」动画状态。

核心要点Animatablestop()animateTo 都是挂起函数,必须在协程作用域内调用。推荐用 LaunchedEffectrememberCoroutineScope 来管理协程。

实战:一个可中断的进度条

光说不练假把式。我们做一个可中断的进度条动画:

@Composable
fun InterruptibleProgressBar() {
    val progress = remember { Animatable(0f) }
    val scope = rememberCoroutineScope()

    Column(modifier = Modifier.padding(16.dp)) {
        // 进度条
        LinearProgressIndicator(
            progress = { progress.value },
            modifier = Modifier.fillMaxWidth().height(20.dp)
        )

        Spacer(modifier = Modifier.height(8.dp))

        // 控制按钮
        Row {
            Button(onClick = {
                scope.launch {
                    progress.animateTo(1f, tween(3000))
                }
            }) {
                Text("开始")
            }

            Spacer(modifier = Modifier.width(8.dp))

            Button(onClick = {
                scope.launch {
                    progress.stop()
                }
            }) {
                Text("暂停")
            }

            Spacer(modifier = Modifier.width(8.dp))

            Button(onClick = {
                scope.launch {
                    progress.snapTo(0f)
                }
            }) {
                Text("重置")
            }
        }
    }
}

这个例子把 animateTostopsnapTo 都用上了。你点「开始」进度条慢慢走,点「暂停」立刻停住,点「重置」瞬间归零。

小技巧:如果你想让暂停后继续从当前位置开始,可以记录 progress.value,下次 animateTo 时从当前值开始。我做过一个下载进度条,就是靠这个思路实现的「断点续传」效果。

SVG:显式动画知识体系

下面这张图帮你理清显式动画的核心脉络:

显式动画 Animatable AnimationSpec 取消与重置 animateTo() snapTo() tween spring keyframes stop() snapTo(初始值) 核心:手动控制动画的启动、停止与跳转 所有操作都需在协程中执行

注意事项AnimatableanimateTo 如果被多次调用,后一次会打断前一次。如果你希望排队执行,需要用 Job 来管理协程。我曾经在快速点击按钮时,动画值来回跳,就是因为没处理好并发问题。

显式动画虽然需要你多写几行代码,但它带来的控制力是隐式动画无法比拟的。尤其是当你需要处理用户交互、中断、重置这些场景时,Animatable 几乎是唯一的选择。

好了,这一章的内容就到这里。记住三个关键词:animateTo 启动、stop 停止、snapTo 跳转。掌握了它们,你就能在 Compose 里随心所欲地控制动画了。

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