在上一篇文章中,我们 了解如何使用 Compose 的 Canvas 和 DrawScope 绘制各种静态的几何图形,并实现了一个基础的"环形进度条"。
但静态的 UI 是没有灵魂的!在一个优秀的 App 中,当进度发生变化时,期望看到一个平滑过渡的动画效果,而不是突兀地跳变。
在传统的 Android View 体系中,给自定义 View 加动画通常意味着要使用 ValueAnimator,监听回调,然后手动调用 invalidate() 去重绘,代码往往显得割裂。但在 Compose 的声明式世界里,这一切只需要一行代码:animateFloatAsState。

1. animateFloatAsState
animateFloatAsState 是 Compose 中最简单、也是最常用的状态驱动动画 API。
它的工作原理非常直观:你给它一个"目标值"(Target Value),它就会自动在一段时间内(比如 300 毫秒),平滑地从当前值过渡到你的目标值,并在这期间不断地返回中间的插值。
基础语法:
Kotlin
val animatedProgress by animateFloatAsState(
targetValue = targetProgress, // 你想要到达的最终值
animationSpec = tween(durationMillis = 1000) // 动画配置:例如使用 1000ms 的补间动画
)
只要把这个 animatedProgress 喂给你的 Canvas,当值发生变化时,Compose 会自动触发画布的重新绘制(Redraw),动画效果就这么水到渠成地出现了。
2. 让"环形进度条"丝滑过渡
还记得上一篇里的 CircularProgressBar 吗?现在我们要为它注入灵魂。我们只需在调用它的地方,加上状态管理和动画包裹。
在组件内部或外部应用动画
我们既可以在组件内部封装动画,也可以在外部调用时包装动画。为了让组件更具通用性,我们通常推荐在组件外部(或者包装一层)处理动画逻辑。
下面是一个完整的示例:我们通过一个按钮点击来随机改变进度,并用 animateFloatAsState 实现平滑过渡。
Kotlin
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable
fun AnimatedCircularProgressDemo() {
// 1. 定义一个状态来保存目标进度 (Target Progress)
var targetProgress by remember { mutableStateOf(0.1f) }
// 2. 核心:使用 animateFloatAsState 将目标进度转化为带动画的进度
val animatedProgress by animateFloatAsState(
targetValue = targetProgress,
animationSpec = tween(
durationMillis = 1500, // 动画时长 1.5 秒
easing = FastOutSlowInEasing // 动画曲线:快出慢进,更符合真实物理反馈
),
label = "Progress Animation" // 用于在 Android Studio 的动画预览中标识
)
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// 3. 将带有动画插值的 animatedProgress 传递给我们的 Canvas 组件
CircularProgressBar(
progress = animatedProgress,
progressColor = Color(0xFF4CAF50),
modifier = Modifier.size(150.dp)
)
Spacer(modifier = Modifier.height(32.dp))
// 4. 点击按钮,随机改变目标进度
Button(onClick = {
targetProgress = (0..100).random() / 100f // 随机生成 0.0 到 1.0 的浮点数
}) {
Text("随机更新进度: ${(targetProgress * 100).toInt()}%")
}
}
}
// (这是上一篇中的绘制组件,代码完全无需修改!)
@Composable
fun CircularProgressBar(
progress: Float,
modifier: Modifier = Modifier,
trackColor: Color = Color.LightGray,
progressColor: Color = Color.Blue,
strokeWidth: Dp = 12.dp
) {
Canvas(modifier = modifier) {
val strokeWidthPx = strokeWidth.toPx()
// 底部背景圆环
drawCircle(
color = trackColor,
radius = (size.minDimension - strokeWidthPx) / 2f,
style = Stroke(width = strokeWidthPx)
)
// 上层进度圆弧
val arcSize = size.minDimension - strokeWidthPx
drawArc(
color = progressColor,
startAngle = -90f,
sweepAngle = 360f * progress, // 这里的 progress 已经是动画插值了!
useCenter = false,
topLeft = Offset(strokeWidthPx / 2, strokeWidthPx / 2),
size = Size(arcSize, arcSize),
style = Stroke(width = strokeWidthPx, cap = StrokeCap.Round)
)
}
}
发生了什么?
当你点击按钮,targetProgress 从 0.1 变为了 0.8。
此时 animateFloatAsState 监听到目标值的改变,它不会立刻返回 0.8,而是在接下来的 1500 毫秒内,不断地返回 0.11, 0.15, 0.23... 直到 0.8。
每一次返回新值,Canvas 都会用新的 progress 重新执行 drawArc。由于重绘发生得非常快(通常是 60fps 或 120fps),在用户的眼睛里,这就变成了一段极其丝滑的进度条增长动画。
3. 性能优化(推迟状态读取)
虽然上面的写法已经能完美运行,但在极致追求性能的场景下,还有一个高级技巧。
在 Compose 中,UI 渲染分为三个阶段:
- 组合 (Composition) :决定画什么(执行
@Composable函数)。 - 布局 (Layout):决定放在哪。
- 绘制 (Drawing) :画出来(执行
Canvas内部的DrawScope)。
在刚才的代码中,我们将 animatedProgress 作为参数传给了 CircularProgressBar。这意味着当动画运行的每一帧,进度值发生改变时,整个 CircularProgressBar 的组合阶段都会被重新执行(即发生重组 Recomposition)。
有没有办法只重新"绘制",而不重新"组合"呢?
答案是有的!我们可以把 progress 的类型从 Float 改为 () -> Float (一个返回 Float 的 Lambda 表达式),或者直接在 Canvas 内部读取 State<Float>。
优化后的组件代码:
Kotlin
@Composable
fun OptimizedCircularProgressBar(
progressProvider: () -> Float, // 使用 Lambda 延迟读取状态
modifier: Modifier = Modifier,
// ... 其他参数
) {
// Canvas 的组合阶段只会执行一次!
Canvas(modifier = modifier) {
// 在 DrawScope (绘制阶段) 内部读取状态
val currentProgress = progressProvider()
// ... drawArc 等绘制代码,同上
}
}
调用方式:
Kotlin
OptimizedCircularProgressBar(
progressProvider = { animatedProgress }, // 传入 Lambda
modifier = Modifier.size(150.dp)
)
通过这种写法,动画运行的每一帧变化,都被限制在了"绘制阶段"。Compose 会跳过"组合"和"布局"阶段,直接在屏幕上重绘图形。这不仅让动画极其流畅,还大大节省了 CPU 的开销,是 Compose 官方极力推荐的最佳实践!
4. 总结
将 animateFloatAsState 与 Canvas 结合,体现了声明式 UI 最大的魅力所在:数据即 UI,动画只是数据随时间的变化。
- 我们不再需要手动去编写冗长的属性动画监听器。
- 只需定义好目标状态,剩下的插值和重绘全部交给 Compose。
- 结合延迟读取状态 (Lambda/State) 的技巧,还能在不增加复杂度的前提下,白嫖极致的渲染性能。