9、主题与样式:Material3 主题定制,颜色、字体、形状系统,暗黑模式适配
聊到 Jetpack Compose 的主题定制,我得先坦白一件事。早年做 Android 原生开发时,我对主题这件事其实挺不上心的。无非就是改改 colors.xml,调调 styles.xml,能跑就行。直到有一次,产品经理拿着设计稿,指着上面一套全新的配色方案说「这个版本我们要全面换肤」——我当时的表情,大概跟吃了柠檬差不多。
但 Compose 里的 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 里改了 titleLarge 和 bodyMedium 的 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,或者直接在 MaterialTheme 的 typography 参数里传入一个统一的 FontFamily。
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 之类的工具做动态适配。
9.6 知识体系总览
下面这张图,是我自己整理的主题定制核心逻辑。你可以把它当作一个快速参考。
这张图里,MaterialTheme 是总入口,下面挂了三根线:颜色、字体、形状。底部是暗黑模式,它通过 isSystemInDarkTheme() 或手动状态来决定使用哪套 ColorScheme。说白了,整个主题系统就是一个「配置 + 分发」的过程。
9.7 实战中的一些建议
最后,分享几个我在项目中积累的经验:
- 颜色命名要语义化:别用 color1、color2 这种名字。用 primary、secondary、surface 这种 Material3 标准命名,团队协作时沟通成本低很多。
- 字体文件别太大:中文字体动辄几 MB,建议只包含常用字重,或者使用动态下载字体。
- 形状要统一:整个 App 的圆角风格保持一致。别这里 4dp,那里 20dp,看起来会很乱。
- 暗黑模式测试要全面:不只是首页,弹窗、列表、输入框、Toast 都要测。我曾经漏了 Snackbar 的暗黑适配,被用户吐槽了。
好了,关于 Material3 主题定制,核心内容就这些。颜色、字体、形状,再加上暗黑模式,你完全可以打造出一套既美观又统一的 App 主题。动手试试吧,从改一个按钮的颜色开始,慢慢你就会发现,主题定制其实没那么复杂。