第16章 主题与样式:MaterialTheme、ColorScheme、Typography、Shapes、动态主题、暗黑模式适配

说实话,主题和样式这块,是很多Android开发者容易忽视的环节。我记得刚入行那会儿,总觉得能跑起来就行,颜色随便写死,字体大小直接写个16sp完事。结果呢?后来产品说要加暗黑模式,我差点没把键盘砸了——几百个地方要改颜色,那酸爽,至今难忘。

今天我们就来彻底搞定Jetpack Compose的主题系统。你想想看,一套好的主题架构,能让你的App从「能用」变成「好用」,从「杂乱无章」变成「井井有条」。

Compose 主题系统核心结构 MaterialTheme ColorScheme primary / secondary / tertiary background / surface / error onPrimary / onSecondary ... Typography displayLarge / headlineMedium titleSmall / bodyLarge / labelMedium 可自定义 fontFamily / fontWeight Shapes small / medium / large RoundedCornerShape / CutCornerShape 可自定义每个组件的圆角 动态主题 + 暗黑模式适配

16.1 MaterialTheme 是什么?

MaterialTheme 是 Compose 里最顶层的主题容器。说白了,它就是整个App的「风格总管」。你所有的颜色、字体、形状,都得通过它来统一管理。

我个人习惯把 MaterialTheme 放在应用的最外层,像这样:

@Composable
fun MyApp() {
    MaterialTheme(
        colorScheme = lightColorScheme(),
        typography = Typography(),
        shapes = Shapes()
    ) {
        // 所有子组件都能访问到主题
        MainScreen()
    }
}

嗯,这里要注意:MaterialTheme 的三个参数——colorScheme、typography、shapes——分别对应颜色、字体和形状。你传什么进去,整个App就用什么风格。

16.2 ColorScheme:颜色的「调色盘」

ColorScheme 是 Material Design 3 的颜色系统。它定义了一整套语义化的颜色变量,比如 primary、secondary、background 这些。

我在项目中遇到过最头疼的事,就是设计师给了个「五彩斑斓的黑」。后来我学乖了,直接跟设计师对齐 Material 3 的配色规范,双方都省心。

核心颜色变量一览:
颜色变量 用途 示例
primary 主色调,用于主要按钮、选中状态 #1565C0
secondary 辅助色,用于次要元素 #7C4DFF
background 页面背景色 #FAFAFA
surface 卡片、弹窗等表面颜色 #FFFFFF
error 错误提示色 #B00020
onPrimary 主色调上的文字/图标颜色 #FFFFFF

自定义 ColorScheme 其实很简单:

val MyColorScheme = lightColorScheme(
    primary = Color(0xFF1565C0),
    secondary = Color(0xFF7C4DFF),
    background = Color(0xFFF5F5F5),
    surface = Color(0xFFFFFFFF),
    error = Color(0xFFB00020),
    onPrimary = Color.White,
    onSecondary = Color.White
)
小技巧: 用 lightColorScheme() 和 darkColorScheme() 分别定义亮色和暗色方案,然后根据系统主题切换。别偷懒只写一套,暗黑模式用户会感谢你的。

16.3 Typography:字体的「尺子」

Typography 定义了从大标题到小标签的完整字体层级。Material 3 提供了 13 种文本样式,从 displayLarge 到 labelSmall。

我记得有一次,产品经理说「这个标题再大一点,那个正文再小一点」,改来改去没完没了。后来我直接把 Typography 定义好,告诉他「所有标题都用 headlineMedium,正文用 bodyLarge」,从此天下太平。

val MyTypography = Typography(
    displayLarge = TextStyle(
        fontWeight = FontWeight.Bold,
        fontSize = 57.sp,
        lineHeight = 64.sp,
        letterSpacing = (-0.25).sp
    ),
    headlineMedium = TextStyle(
        fontWeight = FontWeight.SemiBold,
        fontSize = 28.sp,
        lineHeight = 36.sp
    ),
    bodyLarge = TextStyle(
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.5.sp
    ),
    labelSmall = TextStyle(
        fontWeight = FontWeight.Medium,
        fontSize = 11.sp,
        lineHeight = 16.sp,
        letterSpacing = 0.5.sp
    )
)
注意: 不要随意修改系统预设的字体大小比例。Material 3 的字体层级是经过无障碍设计验证的。你改了一个,可能整个页面的视觉层级就乱了。

16.4 Shapes:组件的「棱角」

Shapes 定义了组件圆角的大小。Material 3 把形状分为 small、medium、large 三个等级。

我曾经在一个项目里,所有按钮都用 8dp 圆角,卡片用 12dp,底部弹窗用 16dp。后来设计师说「统一改成 12dp 吧」,我只需要改一行代码——这就是 Shapes 的威力。

val MyShapes = Shapes(
    small = RoundedCornerShape(4.dp),   // 用于小按钮、标签
    medium = RoundedCornerShape(8.dp),  // 用于卡片、对话框
    large = RoundedCornerShape(16.dp)   // 用于底部弹窗、大卡片
)

你还可以用 CutCornerShape 做切角效果,或者用 AbsoluteRoundedCornerShape 做不对称圆角。不过我个人建议,除非设计有特殊要求,否则用 RoundedCornerShape 就够了。

16.5 动态主题:让用户自己选颜色

动态主题是 Material 3 的一大亮点。它可以根据用户的壁纸或系统设置,自动生成一套配色方案。

实现起来其实不复杂:

@Composable
fun DynamicTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context)
            else dynamicLightColorScheme(context)
        }
        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = MyTypography,
        shapes = MyShapes,
        content = content
    )
}

这里有个坑,我踩过:dynamicColor 只在 Android 12+ 上有效。低版本你得 fallback 到自定义配色。别直接拿 dynamicColorScheme 给所有版本用,会 crash 的。

16.6 暗黑模式适配:不只是「反色」

暗黑模式不是简单地把白色变成黑色。你得考虑对比度、可读性、品牌色的暗色表现。

我建议的做法是:

  • isSystemInDarkTheme() 检测系统主题
  • 分别定义 LightColorSchemeDarkColorScheme
  • 暗色模式下,primary 颜色要降低饱和度,避免刺眼
  • surface 不要用纯黑(#000000),用深灰(#121212)更舒适
private val LightColorScheme = lightColorScheme(
    primary = Color(0xFF1565C0),
    secondary = Color(0xFF7C4DFF),
    background = Color(0xFFF5F5F5),
    surface = Color(0xFFFFFFFF),
    onPrimary = Color.White,
    onSecondary = Color.White
)

private val DarkColorScheme = darkColorScheme(
    primary = Color(0xFF90CAF9),  // 暗色下用浅蓝,更柔和
    secondary = Color(0xFFB39DDB),
    background = Color(0xFF121212),
    surface = Color(0xFF1E1E1E),
    onPrimary = Color(0xFF003C8F),
    onSecondary = Color(0xFF4A148C)
)
核心原则: 暗黑模式下,文字和背景的对比度至少要达到 4.5:1(WCAG AA 标准)。别为了好看牺牲可读性。

16.7 在组件中使用主题

定义好主题后,怎么在组件里用呢?用 MaterialTheme.colorSchemeMaterialTheme.typographyMaterialTheme.shapes 就能拿到当前主题的值。

@Composable
fun MyCard() {
    val colors = MaterialTheme.colorScheme
    val typography = MaterialTheme.typography
    val shapes = MaterialTheme.shapes

    Card(
        modifier = Modifier.padding(16.dp),
        shape = shapes.medium,
        colors = CardDefaults.cardColors(
            containerColor = colors.surface
        )
    ) {
        Text(
            text = "Hello, Theme!",
            style = typography.headlineMedium,
            color = colors.onSurface
        )
    }
}

你看,所有颜色、字体、形状都来自主题。哪天产品说要换主题色,你只需要改 ColorScheme 的定义,所有组件自动更新。这就是 Compose 主题系统的魅力。

我的习惯: 在项目里建一个 Theme.kt 文件,专门放主题相关的代码。ColorScheme、Typography、Shapes、动态主题、暗黑模式,全放一起。找起来方便,改起来也快。

好了,这一章的内容就到这里。主题和样式是 Compose 开发的基石,花时间把这块理清楚,后面的开发会顺畅很多。记住:好的主题架构,能让你的 App 既好看又好维护。


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