17、主题与样式:MaterialTheme、Colors、Typography、Shapes 的自定义
说实话,做 Android 开发这么多年,我见过太多「五彩斑斓的灰」—— 就是那种每个页面颜色都不一样、字体忽大忽小的 App。用户嘴上不说,但心里肯定觉得这产品不靠谱。
在 Compose 里,主题系统是咱们的救星。它把颜色、字体、形状这些视觉元素统一管理起来。你改一个地方,全局都跟着变。今天我就带你把这套东西玩明白。
17.1 MaterialTheme 是什么?
说白了,MaterialTheme 就是一个 Composable 函数。它把三个核心对象 —— Colors、Typography、Shapes —— 通过 CompositionLocal 传递给整个组件树。
你想想看,你在任何一个 Composable 里写 MaterialTheme.colorScheme.primary,拿到的就是最外层定义的那个颜色。这就是「声明式」的威力。
@Composable
fun MyApp(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = lightColorScheme(),
typography = Typography(),
shapes = Shapes(),
content = content
)
}
嗯,这里要注意:MaterialTheme 是可以嵌套的。内层会覆盖外层的部分属性。但我不建议你这么做 —— 容易把自己绕晕。
17.2 自定义 Colors:从「凑合」到「讲究」
颜色是 App 的脸面。我在项目中遇到过最头疼的事,就是设计师给了 20 多个色值,结果开发时到处硬编码。后来我学乖了:先定义一套完整的 ColorScheme。
Compose 把颜色分成了几类:
- Primary / Secondary / Tertiary:主色、辅色、第三色。这是品牌色。
- Background / Surface:背景和卡片底色。
- Error:错误提示色。
- OnXxx:比如
onPrimary,就是主色背景上的文字颜色。
private val MyLightColorScheme = lightColorScheme(
primary = Color(0xFF1976D2),
onPrimary = Color.White,
secondary = Color(0xFF26A69A),
background = Color(0xFFF5F5F5),
surface = Color.White,
error = Color(0xFFD32F2F)
)
private val MyDarkColorScheme = darkColorScheme(
primary = Color(0xFF90CAF9),
onPrimary = Color(0xFF0D47A1),
secondary = Color(0xFF80CBC4),
background = Color(0xFF121212),
surface = Color(0xFF1E1E1E),
error = Color(0xFFEF9A9A)
)
Light / Dark 前缀。这样切换时一目了然。
为什么要有 onPrimary 这种设计?因为你要保证对比度。我曾经见过一个 App,按钮是深蓝色,上面的字也是深蓝色 —— 根本看不清。有了 onPrimary,你只需要保证它和 primary 有足够的对比度就行。
17.3 自定义 Typography:字体是「气质」
字体这东西,很多人不重视。但你看那些大厂的 App,字体系统一定很讲究。Compose 的 Typography 定义了从 displayLarge 到 labelSmall 共 15 种样式。
你不需要全部自定义。我一般只改这几个:
titleLarge:页面标题bodyLarge:正文labelSmall:小标签
val MyTypography = Typography(
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Bold,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
)
fontFamily 里指定。否则 Compose 会回退到系统默认字体,效果可能和设计稿不一样。
17.4 自定义 Shapes:圆角是「细节」
形状决定了组件的「性格」。圆角大,显得亲和;直角多,显得硬朗。Compose 的 Shapes 分三类:
small:按钮、小卡片medium:对话框、大卡片large:底部弹窗、全屏组件
val MyShapes = Shapes(
small = RoundedCornerShape(8.dp),
medium = RoundedCornerShape(12.dp),
large = RoundedCornerShape(16.dp)
)
你可能会问:「就三个形状够用吗?」其实够。因为你可以通过 Modifier.clip() 或者 Surface 的 shape 参数单独覆盖。但全局统一用这三个,能保证视觉一致性。
17.5 把它们组合起来
好,现在我们把三个部分拼到一起:
@Composable
fun MyAppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (darkTheme) MyDarkColorScheme else MyLightColorScheme
MaterialTheme(
colorScheme = colorScheme,
typography = MyTypography,
shapes = MyShapes,
content = content
)
}
然后在你的 App 入口处:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
// 你的页面内容
}
}
}
}
这样,所有使用 MaterialTheme 提供的颜色、字体、形状的组件,都会自动应用你的设计。改一个地方,全局生效。
17.6 知识体系总览
下面这张图帮你理清整个主题系统的结构:
17.7 避坑指南
最后,分享几个我踩过的坑:
- 颜色不要硬编码:我曾经在十几个文件里写了
Color(0xFF1976D2),后来改品牌色时改到崩溃。一定要用MaterialTheme.colorScheme.primary。 - 暗色主题不是「亮色取反」:很多人直接把亮色的颜色取反当暗色用,结果对比度一塌糊涂。暗色主题需要单独设计。
- 字体文件要提前加载:如果你用了自定义字体,记得在
setContent之前用FontFamily.Load加载,否则会有闪白。 - Shape 不要滥用:每个组件都单独设 shape,最后就是「四不像」。能用主题默认的就用默认的。
好了,主题系统就讲到这里。你只要把 Colors、Typography、Shapes 这三个东西管好,整个 App 的视觉一致性就有了保障。下次设计师改需求时,你只需要改一个文件,而不是翻遍整个项目。