Compose 动画的心智模型
Compose 动画的核心理念和声明式 UI 一致:你不写逐帧,只声明"当状态变成什么时,值应如何补间"。状态一动,动画自动插入中间帧。比起传统属性动画手写 start/end,这是另一种思路。
把动画当成状态的另一层:状态决定目标值,动画决定怎么平滑地到达。
这篇从值动画到自定义布局,把 Compose 的进阶能力串起来。
animate*AsState:值动画
最常用的是 animateFloatAsState、animateColorAsState。给它一个目标值,它返回一个实时变化的状态,界面读取这个状态就是动画过程。
val alpha by animateFloatAsState(
targetValue = if (visible) 1f else 0f,
animationSpec = tween(300)
)
visible 一变,alpha 在 300 毫秒内平滑过渡。
AnimatedVisibility 与交叉淡入
控制视图出现消失,用 AnimatedVisibility。包在里面的内容在条件为真时淡入展开,为假时淡出收起,不用手写透明度和高度补间。这是列表项增删时最顺手的动效。
- animate*AsState单值补间动画。
- AnimatedVisibility显隐转场。
- Animatable细粒度可控动画。
- Crossfade两视图交叉淡入。
Animatable 与手势联动
要做手势驱动的动画——比如拖拽松手后弹回——用 Animatable。它可以手动 snapTo 到手指位置,再 launch 到目标值回弹。拖拽、滑动删除这类交互动效都靠它。
共享元素与转场
页面间导航的转场用 AnimatedContent,给进出内容定义 fade、slide。两个页面间共享同一个元素的连续放大,用 SharedTransitionLayout。这类高级转场让应用像一体,而不是一帧一帧跳。
自定义布局 Layout 协议
Column、Row、Box 满足不了时,实现 Layout 接口自己量稿子。重写 measurePolicy:拿到每个子项的约束,测量它们,再决定放在哪里。流式布局、重叠卡片、标签云都能这样实现。
@Composable
fun Staggered(vararg items) {
Layout(content = { items.forEach { it() } }) { meas, c ->
val placed = meas.map { it.measure(c) }
layout(maxWidth, maxHeight) { placed.forEach { it.place(0, 0) } }
}
}
布局测量与摆放
自定义布局要懂约束传递:measure 给子项什么约束,它就能长多大。不要测一个自己放不下的尺寸,也别硬编码像素。合理做法是把可用约束分发给子项,汇总后定自己的尺寸,再逐个子项 place。
- 先问现有容器能否满足。
- 再用自定义布局,别过早优化。
- 摆放时注意对齐与间距。
实战:一个展开卡片
点一下卡片,高度和透明度平滑展开。
var expanded by remember { mutableStateOf(false) }
val h by animateDpAsState(if (expanded) 220.dp else 90.dp)
Box(Modifier.height(h).clickable { expanded = !expanded }) {
Text("点我展开")
}
点一下,高度在弹簧曲线里从 90 长到 220。状态驱动目标值,动画负责补间——这就是 Compose 进阶的套路:把动效翻译成状态变化,剩下交给框架。