Compose主题与动画:MaterialTheme、自定义主题、animateXxxAsState、AnimatedVisibility
说实话,Compose 里我最喜欢的就是主题和动画这一块。以前用 View 系统做主题切换,那叫一个痛苦——写一堆 style 文件,还要手动刷新。动画更是噩梦,动不动就写个 Animator 文件。但在 Compose 里,这些事变得特别自然。今天我们就来聊聊 MaterialTheme、自定义主题,还有 animateXxxAsState 和 AnimatedVisibility 这几个核心 API。
1. MaterialTheme:你的设计系统底座
MaterialTheme 是 Compose 里最基础的“皮肤”。它把颜色、字体、形状统一管理起来。我个人习惯,每个项目一开始就定义好主题,不然后面改起来很麻烦。
看个最简单的例子:
MaterialTheme(
colors = lightColors(
primary = Color(0xFF6200EE),
secondary = Color(0xFF03DAC5)
),
typography = Typography(
h1 = TextStyle(fontSize = 28.sp, fontWeight = FontWeight.Bold)
),
shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(8.dp),
large = RoundedCornerShape(16.dp)
)
) {
// 你的 UI 代码
}
这里要注意,lightColors 和 darkColors 是现成的调色板。你想想看,如果要做暗黑模式,只需要在 isSystemInDarkTheme() 判断一下,切换 colors 就行。我在项目中遇到过,很多新手直接把颜色写死,结果暗黑模式一开,界面全崩了。
2. 自定义主题:从 Material 到你的品牌
有时候 Material 的配色不够用,比如你的品牌色是渐变的,或者需要自定义一些语义颜色。这时候就需要自己定义主题对象。
我一般这样做:
data class AppColors(
val brand: Color,
val brandVariant: Color,
val success: Color,
val error: Color,
val surface: Color,
val onSurface: Color
)
object AppTheme {
val colors: AppColors
@Composable get() = LocalAppColors.current
}
val LocalAppColors = staticCompositionLocalOf {
AppColors(
brand = Color.Unspecified,
brandVariant = Color.Unspecified,
success = Color.Unspecified,
error = Color.Unspecified,
surface = Color.Unspecified,
onSurface = Color.Unspecified
)
}
然后在顶层提供:
@Composable
fun MyAppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (darkTheme) darkAppColors else lightAppColors
CompositionLocalProvider(LocalAppColors provides colors) {
MaterialTheme(content = content)
}
}
这样做的好处是,你可以在任何地方通过 AppTheme.colors.brand 拿到颜色。我曾经在一个大项目里,因为没做自定义主题,导致 20 多个页面都要手动传颜色参数,改一次设计就要改所有文件。后来重构了,才彻底解脱。
staticCompositionLocalOf 换成 compositionLocalOf 如果值会频繁变化。前者性能更好,但后者支持重组。
3. animateXxxAsState:让数值动起来
动画在 Compose 里变得特别简单。核心就是 animateXxxAsState 系列函数。比如 animateFloatAsState、animateColorAsState、animateDpAsState 等等。
看个实际例子:一个按钮按下时缩放效果。
@Composable
fun ScaleButton() {
var pressed by remember { mutableStateOf(false) }
val scale by animateFloatAsState(
targetValue = if (pressed) 0.9f else 1.0f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
Button(
onClick = { pressed = !pressed },
modifier = Modifier.graphicsLayer {
scaleX = scale
scaleY = scale
}
) {
Text("点我缩放")
}
}
这里我用了 spring 动画规格,效果特别 Q 弹。你想想看,如果用传统的 Animator 实现同样的效果,代码量至少翻倍。而且 Compose 的动画是声明式的,你只需要关心目标值,中间过程它自动处理。
animateXxxAsState 的 targetValue 变化时,动画会自动启动。但如果 targetValue 没变,动画不会重复播放。如果你需要重复播放,可以用 Animatable 配合 LaunchedEffect。
4. AnimatedVisibility:优雅的显隐动画
这个 API 我几乎每个页面都会用。它让 View 的出现和消失变得丝滑。用法很简单:
@Composable
fun AnimatedVisibilityDemo() {
var visible by remember { mutableStateOf(true) }
Column {
Button(onClick = { visible = !visible }) {
Text(if (visible) "隐藏" else "显示")
}
AnimatedVisibility(
visible = visible,
enter = slideInVertically() + fadeIn(),
exit = slideOutVertically() + fadeOut()
) {
Text("我是会动的内容", fontSize = 24.sp)
}
}
}
这里 enter 和 exit 可以组合多个动画。比如 slideInVertically 加上 fadeIn,效果就是一边滑入一边淡入。我个人习惯,列表项的增删动画一定会用 AnimatedVisibility,用户体验提升很明显。
我曾经在做一个聊天应用时,消息列表的每条新消息都用 AnimatedVisibility 配合 expandIn 动画,用户反馈说“消息弹出来很舒服”。其实代码就多写了三行,但效果天差地别。
tween(durationMillis = 300) 替换默认的弹簧动画。另外,AnimatedVisibility 还支持 MutableTransitionState,可以手动控制动画的播放和暂停。
5. 知识体系总览
下面这张图是我自己总结的,把主题和动画的核心关系画出来了。你看一眼就能明白整体脉络。
6. 避坑指南
最后分享几个我踩过的坑:
- 主题嵌套问题:我曾经在一个页面里嵌套了两个
MaterialTheme,结果颜色全乱了。记住,MaterialTheme应该只在最外层用一次,内部用CompositionLocalProvider覆盖局部属性。 - 动画性能:如果你在列表里用
animateFloatAsState,记得用remember缓存状态。不然每次重组都会创建新的动画实例,卡顿是必然的。 - AnimatedVisibility 的 key:如果内容有多个相同类型的组件,记得给
AnimatedVisibility设置key参数,否则动画会混乱。比如列表项删除时,没有 key 会导致动画错位。
好了,关于 Compose 主题和动画的核心内容就这些。记住,主题是骨架,动画是血肉。两者结合好了,你的 UI 才能既有颜值又有灵魂。