13、动画基础:animateFloatAsState、animateColorAsState、animateContentSize
动画这东西,说白了就是让UI「动起来」。很多新手觉得动画是锦上添花,其实不然。我做过一个天气App,没有动画时切换温度单位就是数字一闪,用户反馈说「感觉卡卡的」。加上过渡动画后,同样的逻辑,用户评价直接上了一个档次。
Compose的动画API设计得很聪明。你不需要像传统View系统那样写AnimationSet、监听回调、手动刷新。在Compose里,动画就是一个状态变化的过程。你告诉它「从A变到B」,剩下的交给框架。
今天咱们就聊三个最常用的动画API:animateFloatAsState、animateColorAsState、animateContentSize。这三个基本覆盖了日常开发80%的动画场景。
13.1 animateFloatAsState:数值变化的平滑过渡
这个API是我用得最多的。任何Float类型的值变化,都可以用它来做动画。比如进度条、透明度、旋转角度、缩放比例……
先看一个最简单的例子:点击按钮让一个方块旋转。
@Composable
fun RotateBox() {
var rotated by remember { mutableStateOf(false) }
val angle by animateFloatAsState(
targetValue = if (rotated) 45f else 0f,
animationSpec = tween(durationMillis = 500)
)
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
.rotate(angle)
.clickable { rotated = !rotated }
)
}
这里有个细节要注意:animateFloatAsState返回的是一个State<Float>,所以要用by委托来获取值。每次rotated变化时,targetValue会重新计算,动画就会自动触发。
你可能会问:animationSpec是什么?它是动画规格,控制动画怎么跑。除了tween(补间动画),还有:
spring:弹簧效果,有回弹感。适合列表拖拽、点赞动画repeatable:重复动画。比如加载中的旋转infiniteRepeatable:无限重复。注意要配合rememberInfiniteTransition
我在项目中遇到过一个问题:用animateFloatAsState做进度条动画时,如果进度值频繁变化(比如每10ms更新一次),动画会变得很卡。后来发现是animationSpec用了默认的spring,弹簧动画对频繁变化的值不太友好。换成tween就好了。
13.2 animateColorAsState:颜色变化的丝滑过渡
颜色变化在UI中很常见:按钮按下变色、状态标签切换、主题切换……用animateColorAsState可以轻松实现。
@Composable
fun ColorfulButton() {
var isActive by remember { mutableStateOf(false) }
val bgColor by animateColorAsState(
targetValue = if (isActive) Color.Green else Color.Gray,
animationSpec = tween(400)
)
Button(
onClick = { isActive = !isActive },
colors = ButtonDefaults.buttonColors(containerColor = bgColor)
) {
Text(if (isActive) "已激活" else "未激活")
}
}
这个API支持Color、ColorStop等类型。底层用的是lerp(线性插值)算法,在RGB或Lab色彩空间之间平滑过渡。
animateColorAsState做深色模式切换时,如果颜色值直接从Color.White变到Color.Black,中间会经过灰色,看起来像「脏了」一样。后来我改用Color.Argb指定具体的中间色,或者用Color.Lerp手动控制插值曲线,效果就好多了。
还有一个实用技巧:animateColorAsState可以配合Color.lerp做多段颜色动画。比如一个状态从红到黄再到绿,你可以拆成两个动画阶段,或者用animate配合Animatable来实现。
13.3 animateContentSize:内容变化的自适应动画
这个API是我觉得最「神奇」的一个。它能让布局大小变化时自动产生动画,不需要你手动计算宽高。
举个例子:一个可展开的卡片。
@Composable
fun ExpandableCard() {
var expanded by remember { mutableStateOf(false) }
Card(
modifier = Modifier
.fillMaxWidth()
.animateContentSize(
animationSpec = tween(300, easing = FastOutSlowInEasing)
)
.clickable { expanded = !expanded }
) {
Column(modifier = Modifier.padding(16.dp)) {
Text("点击展开/收起")
if (expanded) {
Text("这里是详细内容……")
Text("更多信息……")
Text("甚至更多……")
}
}
}
}
注意看:animateContentSize加在Card的Modifier上。当expanded变化时,Column的高度会变化,Card的大小也会跟着变。这个动画是自动的,你什么都不用管。
animateContentSize实际上监听的是布局的IntrinsicSize变化。当内容大小改变时,它会用动画插值来过渡新旧尺寸。说白了就是「测量两次,动画一次」。
我个人习惯在列表项中使用animateContentSize。比如一个消息列表,点击某条消息展开详情。但要注意:如果列表项很多,每个都加animateContentSize可能会影响滑动性能。我一般只在展开状态稳定的场景下用,比如「查看更多」按钮。
13.4 三个API的对比与选择
这三个API各有侧重,我整理了一个表格方便你对比:
| API | 适用场景 | 返回值类型 | 典型用法 |
|---|---|---|---|
| animateFloatAsState | 数值变化(角度、透明度、进度) | State<Float> | 旋转、缩放、进度条 |
| animateColorAsState | 颜色变化 | State<Color> | 按钮颜色、状态标签 |
| animateContentSize | 布局大小变化 | Modifier | 展开/收起卡片 |
你想想看,这三个API其实覆盖了UI动画的三个维度:数值、颜色、尺寸。掌握了它们,大部分日常动画需求都能搞定。
13.5 组合使用:一个完整的例子
咱们把三个API组合起来,做一个「点赞按钮」的动画效果:
@Composable
fun LikeButton(isLiked: Boolean, onToggle: () -> Unit) {
val scale by animateFloatAsState(
targetValue = if (isLiked) 1.2f else 1f,
animationSpec = spring(dampingRatio = 0.5f)
)
val color by animateColorAsState(
targetValue = if (isLiked) Color.Red else Color.Gray,
animationSpec = tween(200)
)
IconButton(
onClick = onToggle,
modifier = Modifier
.scale(scale)
.animateContentSize()
) {
Icon(
imageVector = if (isLiked) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
contentDescription = "点赞",
tint = color
)
}
}
这个例子中:
animateFloatAsState控制点赞时的缩放效果(1.0 → 1.2 → 1.0)animateColorAsState控制颜色从灰色变红色animateContentSize确保图标大小变化时平滑过渡
三个动画同时运行,互不干扰。这就是Compose声明式动画的魅力——你只管描述「最终状态」,中间过程交给框架。
animationSpec时长一致,或者用spring让它们自然同步。否则可能会出现「一个动画结束了,另一个还在跑」的割裂感。
13.6 知识体系总览
下面这张图帮你理清今天讲的核心内容:
嗯,今天的内容就到这里。这三个API是Compose动画的基石,理解它们之后,再去学Animatable、Transition、AnimatedVisibility就会轻松很多。记住一点:在Compose里,动画不是「操作」,而是「状态」。你变了,它自然就动了。
公众号:蓝海资料掘金营,微信deep3321