9、主题与样式:Material3 主题定制,颜色、字体、形状系统,暗黑模式适配

聊到 Jetpack Compose 的主题定制,我得先坦白一件事。早年做 Android 原生开发时,我对主题这件事其实挺不上心的。无非就是改改 colors.xml,调调 styles.xml,能跑就行。直到有一次,产品经理拿着设计稿,指着上面一套全新的配色方案说「这个版本我们要全面换肤」——我当时的表情,大概跟吃了柠檬差不多。

但 Compose 里的 Material3 主题系统,说实话,让我对「换肤」这件事彻底改观了。它不再是简单的颜色替换,而是一整套设计语言。颜色、字体、形状,三者联动,再加上暗黑模式,整个体验非常丝滑。今天我们就来拆解一下。

核心要点: Material3 的主题定制,本质上是定义一套 MaterialTheme 的配置数据。你改颜色、改字体、改形状,都是在改这个数据对象。数据变了,UI 自动刷新。

9.1 颜色系统:从 ColorScheme 说起

Material3 的颜色系统,核心是 ColorScheme。它不像以前那样只有 primary、accent 几个字段,而是分成了好几组角色:Primary、Secondary、Tertiary、Error、Neutral,以及它们对应的变体(Variant)和容器色(Container)。

我个人习惯,拿到设计稿后,先找出主色、辅色、第三色。然后直接调用 lightColorScheme()darkColorScheme() 来构建。

// 定义你自己的颜色
private val LightColors = lightColorScheme(
    primary = Color(0xFF6750A4),
    onPrimary = Color.White,
    primaryContainer = Color(0xFFEADDFF),
    onPrimaryContainer = Color(0xFF21005D),
    secondary = Color(0xFF625B71),
    tertiary = Color(0xFF7D5260),
    background = Color(0xFFFFFBFE),
    surface = Color(0xFFFFFBFE),
    error = Color(0xFFB3261E)
)

private val DarkColors = darkColorScheme(
    primary = Color(0xFFD0BCFF),
    onPrimary = Color(0xFF381E72),
    primaryContainer = Color(0xFF4F378B),
    onPrimaryContainer = Color(0xFFEADDFF),
    secondary = Color(0xFFCCC2DC),
    tertiary = Color(0xFFEFB8C8),
    background = Color(0xFF1C1B1F),
    surface = Color(0xFF1C1B1F),
    error = Color(0xFFF2B8B5)
)

你可能会问:「这么多颜色字段,我是不是都得填?」其实不用。Material3 的 lightColorScheme() 有默认值,你只填几个关键色,剩下的它会自动推算。但如果你追求精准,我建议至少把 primary、onPrimary、primaryContainer、secondary、background、surface 这六个填上。

小技巧: 我在项目中经常用 MaterialTheme.colorScheme.primary 来引用颜色。别写死颜色值,否则换主题时你会哭的。

9.2 字体系统:Typography 的灵活配置

字体这块,Material3 提供了 Typography 类。它定义了从 displayLarge 到 labelSmall 共 15 种文字样式。你不需要全部重写,只需要覆盖你关心的那几个。

我记得有一次,设计稿要求标题用思源黑体,正文用 Noto Sans。我当时直接在 Typography 里改了 titleLargebodyMedium 的 fontFamily。

val AppTypography = 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
    ),
    titleLarge = TextStyle(
        fontWeight = FontWeight.Medium,
        fontSize = 22.sp,
        lineHeight = 28.sp,
        fontFamily = FontFamily.Default
    ),
    bodyMedium = TextStyle(
        fontWeight = FontWeight.Normal,
        fontSize = 14.sp,
        lineHeight = 20.sp,
        letterSpacing = 0.25.sp
    )
)

这里有个坑,我踩过。如果你在 Typography 里只改了 bodyMedium,其他样式会使用默认值。但默认的 fontFamily 是系统字体。如果你希望全局统一字体,记得在 Typography 的每个样式里都指定 fontFamily,或者直接在 MaterialThemetypography 参数里传入一个统一的 FontFamily

注意: 字体文件需要放在 res/font 目录下,并在 build.gradle 中配置字体资源。否则运行时会直接崩溃,别问我怎么知道的。

9.3 形状系统:Shapes 让 UI 更有层次

形状系统,说白了就是控制卡片、按钮、对话框这些组件的圆角大小。Material3 把形状分成了三类:small、medium、large。分别对应不同的组件。

  • small:按钮、文本输入框、chip
  • medium:卡片、对话框、菜单
  • large:底部弹窗、导航抽屉

我一般会这样定义:

val AppShapes = Shapes(
    small = RoundedCornerShape(8.dp),
    medium = RoundedCornerShape(12.dp),
    large = RoundedCornerShape(16.dp)
)

你想想看,如果所有组件都是直角,界面会显得很生硬。稍微加点圆角,视觉上就柔和很多。但别加太多,圆角太大反而显得不专业。我个人经验,8dp 到 16dp 是比较安全的范围。

9.4 整合:构建完整的 MaterialTheme

颜色、字体、形状都定义好了,接下来就是组装。在 Compose 里,你只需要在顶层包裹一个 MaterialTheme,把这三个对象传进去即可。

@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colorScheme = if (darkTheme) DarkColors else LightColors

    MaterialTheme(
        colorScheme = colorScheme,
        typography = AppTypography,
        shapes = AppShapes,
        content = content
    )
}

然后,在你的 Activity 或 Composable 入口处,直接调用 AppTheme { ... }。所有子组件都会自动继承这些主题配置。

关键点: isSystemInDarkTheme() 会自动检测系统暗黑模式状态。你不需要手动监听。但如果你希望用户手动切换,可以自己维护一个状态变量来控制 darkTheme 参数。

9.5 暗黑模式适配:不只是换个背景色

暗黑模式适配,很多人以为就是把背景色改成黑色,文字改成白色。其实远不止这些。Material3 的暗黑模式,颜色饱和度会降低,对比度会调整,避免在暗背景下刺眼。

我曾经犯过一个错误:直接把亮色主题的颜色取反,结果暗黑模式下文字和背景的对比度太低,根本看不清。后来我学乖了,严格按照 Material3 的 darkColorScheme() 来定义暗色值。

暗黑模式下,有几个颜色要特别注意:

  • surface:不要用纯黑(#000000),用深灰(#1C1B1F)更护眼
  • onSurface:用浅灰(#E6E1E5)而不是纯白,减少眩光
  • primaryContainer:饱和度降低,避免亮色在暗背景下过于刺眼

另外,图片和图标也要考虑。有些图标在亮色下好看,到了暗黑模式下就糊成一团。我一般会准备两套图标资源,或者使用 LocalInverted 之类的工具做动态适配。

建议: 在开发阶段,用 Android Studio 的「Theme Editor」预览暗黑模式效果。或者直接在模拟器里切换系统主题,实时看效果。别等到上线了才发现暗黑模式下按钮都看不清。

9.6 知识体系总览

下面这张图,是我自己整理的主题定制核心逻辑。你可以把它当作一个快速参考。

MaterialTheme ColorScheme lightColorScheme() darkColorScheme() Typography displayLarge ~ labelSmall fontFamily / fontWeight / fontSize Shapes small / medium / large RoundedCornerShape 暗黑模式适配 isSystemInDarkTheme() 手动切换状态

这张图里,MaterialTheme 是总入口,下面挂了三根线:颜色、字体、形状。底部是暗黑模式,它通过 isSystemInDarkTheme() 或手动状态来决定使用哪套 ColorScheme。说白了,整个主题系统就是一个「配置 + 分发」的过程。

9.7 实战中的一些建议

最后,分享几个我在项目中积累的经验:

  1. 颜色命名要语义化:别用 color1、color2 这种名字。用 primary、secondary、surface 这种 Material3 标准命名,团队协作时沟通成本低很多。
  2. 字体文件别太大:中文字体动辄几 MB,建议只包含常用字重,或者使用动态下载字体。
  3. 形状要统一:整个 App 的圆角风格保持一致。别这里 4dp,那里 20dp,看起来会很乱。
  4. 暗黑模式测试要全面:不只是首页,弹窗、列表、输入框、Toast 都要测。我曾经漏了 Snackbar 的暗黑适配,被用户吐槽了。
避坑指南: 如果你在 Compose 中使用了第三方库的组件,记得检查它们是否支持 Material3 主题。有些老库只支持 Material2,混用时颜色会乱套。我遇到过这种情况,最后不得不自己封装一层适配器。

好了,关于 Material3 主题定制,核心内容就这些。颜色、字体、形状,再加上暗黑模式,你完全可以打造出一套既美观又统一的 App 主题。动手试试吧,从改一个按钮的颜色开始,慢慢你就会发现,主题定制其实没那么复杂。