18、Compose主题:MaterialTheme、颜色系统、字体系统、形状系统、暗黑模式、实战:主题定制
各位同学,今天我们来聊聊 Compose 里的主题系统。说实话,主题这个东西,在传统 View 体系里我一直觉得挺麻烦的——写一堆 style.xml,还要处理各种主题继承,稍不留神就乱了。到了 Compose 时代,一切都变得清晰了。
MaterialTheme 是 Compose 主题的核心。它把颜色、字体、形状这三个维度统一管理起来。我个人习惯是,拿到一个新项目,第一件事就是搭好主题框架。为什么?因为后面所有 UI 组件都会基于这个主题来渲染,你改一个地方,全局都跟着变。
颜色系统
颜色系统是主题里最直观的部分。Material Design 3 把颜色分成了几个角色:Primary、Secondary、Tertiary,以及对应的 Surface、Background、Error 等。每个角色又有自己的变体,比如 onPrimary 表示在 Primary 背景上显示的文字颜色。
我在项目中遇到过一个问题:设计师给了一套品牌色,但直接套到 MaterialTheme 里发现按钮文字看不清。后来排查发现,是 onPrimary 颜色没配好。说白了,颜色系统不是孤立的,你得保证前景色和背景色有足够的对比度。
// 定义颜色
private val LightColorScheme = lightColorScheme(
primary = Color(0xFF1976D2),
onPrimary = Color.White,
secondary = Color(0xFF03DAC6),
onSecondary = Color.Black,
background = Color(0xFFF5F5F5),
surface = Color.White,
error = Color(0xFFB00020)
)
private val DarkColorScheme = darkColorScheme(
primary = Color(0xFF90CAF9),
onPrimary = Color(0xFF0D47A1),
secondary = Color(0xFF03DAC6),
onSecondary = Color.Black,
background = Color(0xFF121212),
surface = Color(0xFF1E1E1E),
error = Color(0xFFCF6679)
)
你想想看,如果只定义了 primary 没定义 onPrimary,Compose 会怎么处理?它会用默认的黑色或白色。但默认值不一定适合你的品牌色。所以我的建议是:把每个颜色角色都显式定义一遍,别偷懒。
字体系统
字体系统在 Compose 里通过 Typography 类来管理。它定义了从 displayLarge 到 labelSmall 共 15 个文本样式。每个样式都可以单独设置字体、字号、行高、字重等属性。
嗯,这里要注意:不要每个文本组件都手动设置字体。你应该在主题层面统一配置,然后 Text 组件会自动匹配正确的样式。
val AppTypography = Typography(
displayLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Bold,
fontSize = 57.sp,
lineHeight = 64.sp
),
headlineMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.SemiBold,
fontSize = 28.sp,
lineHeight = 36.sp
),
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp
)
)
我曾经踩过一个坑:项目里用了自定义字体,但只在几个关键页面设置了 fontFamily,结果其他页面的字体不一致。后来我把自定义字体统一放到了 Typography 的默认配置里,问题就解决了。
形状系统
形状系统控制的是组件的圆角、裁剪方式。MaterialTheme 提供了 Shapes 类,包含 small、medium、large 三个层级。Card、Button、Dialog 等组件会分别使用不同层级的形状。
| 层级 | 典型组件 | 默认圆角 |
|---|---|---|
| small | Button、TextField | 4.dp |
| medium | Card、AlertDialog | 8.dp |
| large | ModalDrawer、BottomSheet | 16.dp |
val AppShapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(8.dp),
large = RoundedCornerShape(16.dp)
)
我个人习惯把形状也纳入设计系统的统一管理。设计师给了一个圆角规范,我就直接映射到这三个层级上。这样所有组件都遵循同一套规则,视觉上非常统一。
暗黑模式
暗黑模式在 Compose 里实现起来非常优雅。你只需要准备两套 ColorScheme:一套亮色、一套暗色。然后通过 isSystemInDarkTheme() 来判断当前系统主题,动态切换即可。
核心思路:亮色和暗色不是简单的颜色反转。暗色模式下,背景应该用深色,文字用浅色,但饱和度要降低,避免刺眼。
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
shapes = AppShapes,
content = content
)
}
为什么暗色模式要单独定义一套颜色?直接反转不行吗?我试过,效果很差。比如亮色下的 primary 是蓝色,反转后变成黄色,完全不符合设计规范。所以老老实实手写两套吧。
实战:主题定制
好了,理论讲完了,我们来动手做一个完整的主题定制。假设我们要做一个阅读类 App,主题要求:暖色调、圆角适中、字体偏大。
第一步,定义颜色。我选了一个暖橙色作为 primary,搭配米白色背景。
private val ReadingLightColors = lightColorScheme(
primary = Color(0xFFE65100),
onPrimary = Color.White,
secondary = Color(0xFFFFB74D),
onSecondary = Color(0xFF3E2723),
background = Color(0xFFFFF8E1),
surface = Color(0xFFFFF3E0),
onBackground = Color(0xFF3E2723),
onSurface = Color(0xFF3E2723)
)
第二步,定义字体。阅读类 App 需要大一点的字号,我把 bodyLarge 从 16sp 调到了 18sp。
val ReadingTypography = Typography(
bodyLarge = TextStyle(fontSize = 18.sp, lineHeight = 28.sp),
bodyMedium = TextStyle(fontSize = 16.sp, lineHeight = 24.sp),
headlineMedium = TextStyle(fontSize = 24.sp, lineHeight = 32.sp)
)
第三步,定义形状。阅读类 App 不需要太尖锐的边角,我把 medium 的圆角设成了 12.dp。
val ReadingShapes = Shapes(
small = RoundedCornerShape(6.dp),
medium = RoundedCornerShape(12.dp),
large = RoundedCornerShape(20.dp)
)
最后,组合起来使用:
@Composable
fun ReadingTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (darkTheme) ReadingDarkColors else ReadingLightColors
MaterialTheme(
colorScheme = colors,
typography = ReadingTypography,
shapes = ReadingShapes,
content = content
)
}
小技巧:如果你不确定暗色模式下的颜色是否合适,可以用 Material Theme Builder 这个在线工具。它可以根据你的亮色主题自动生成暗色主题,省去很多调试时间。
到这里,一个完整的主题定制就完成了。你可以在 App 的入口处包裹 ReadingTheme,所有子组件都会自动应用这套主题。以后要改设计风格,只需要改这一个地方就行。
注意:主题定制不是一次性的工作。随着项目迭代,设计师可能会调整颜色或字体。我建议把主题配置单独放在一个文件中,并加上清晰的注释,方便后续维护。
最后,我用一张图来总结本章的知识体系:
这张图展示了 MaterialTheme 的三大支柱:颜色、字体、形状,以及暗黑模式如何贯穿其中。你只需要定义好这三套配置,整个 App 的视觉风格就统一了。