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 代码
}

这里要注意,lightColorsdarkColors 是现成的调色板。你想想看,如果要做暗黑模式,只需要在 isSystemInDarkTheme() 判断一下,切换 colors 就行。我在项目中遇到过,很多新手直接把颜色写死,结果暗黑模式一开,界面全崩了。

核心要点:MaterialTheme 提供的是“默认值”,你可以覆盖任意属性。但不要改太多,保持 Material Design 的规范,不然设计走查时会很头疼。

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 系列函数。比如 animateFloatAsStateanimateColorAsStateanimateDpAsState 等等。

看个实际例子:一个按钮按下时缩放效果。

@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)
        }
    }
}

这里 enterexit 可以组合多个动画。比如 slideInVertically 加上 fadeIn,效果就是一边滑入一边淡入。我个人习惯,列表项的增删动画一定会用 AnimatedVisibility,用户体验提升很明显。

我曾经在做一个聊天应用时,消息列表的每条新消息都用 AnimatedVisibility 配合 expandIn 动画,用户反馈说“消息弹出来很舒服”。其实代码就多写了三行,但效果天差地别。

进阶用法:如果你需要控制动画的持续时间,可以用 tween(durationMillis = 300) 替换默认的弹簧动画。另外,AnimatedVisibility 还支持 MutableTransitionState,可以手动控制动画的播放和暂停。

5. 知识体系总览

下面这张图是我自己总结的,把主题和动画的核心关系画出来了。你看一眼就能明白整体脉络。

Compose 主题与动画 MaterialTheme 自定义主题 colors typography CompositionLocal shapes 动画 API animateXxxAsState AnimatedVisibility 核心思想:声明式 + 状态驱动 主题提供上下文,动画响应状态变化

6. 避坑指南

最后分享几个我踩过的坑:

  • 主题嵌套问题:我曾经在一个页面里嵌套了两个 MaterialTheme,结果颜色全乱了。记住,MaterialTheme 应该只在最外层用一次,内部用 CompositionLocalProvider 覆盖局部属性。
  • 动画性能:如果你在列表里用 animateFloatAsState,记得用 remember 缓存状态。不然每次重组都会创建新的动画实例,卡顿是必然的。
  • AnimatedVisibility 的 key:如果内容有多个相同类型的组件,记得给 AnimatedVisibility 设置 key 参数,否则动画会混乱。比如列表项删除时,没有 key 会导致动画错位。
我的习惯:每次新建 Compose 项目,第一件事就是搭好主题框架。哪怕只有几个页面,也值得花 10 分钟定义好颜色和字体。后面你会发现,改设计就像改配置文件一样简单。

好了,关于 Compose 主题和动画的核心内容就这些。记住,主题是骨架,动画是血肉。两者结合好了,你的 UI 才能既有颜值又有灵魂。


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