12、主题与样式:Material3主题、ColorScheme、Typography、Shapes、动态主题适配
说实话,主题和样式这块,是很多初学者容易忽略的。大家总觉得「先把功能跑起来再说」。嗯,我以前也这么想。直到有一次,我接了一个外包项目,客户要求App能跟随系统切换深色模式,而且还要适配他们公司的品牌色。我当时的代码里,颜色全是硬编码的十六进制……改起来那叫一个酸爽。
从那以后,我养成了一个习惯:任何项目,哪怕只是个Demo,也先把主题搭好。今天我们就来聊聊Jetpack Compose里,Material3的主题系统到底怎么玩。
12.1 Material3 主题的核心:MaterialTheme
在Compose里,主题不是一堆配置文件,而是一个Composable函数——MaterialTheme。它就像一个大容器,把颜色、字体、形状这些全局配置统一管理起来。
你想想看,一个App有成百上千个组件。如果每个按钮、每个卡片都单独设置颜色,那维护成本得多高?MaterialTheme就是来解决这个问题的。它提供了一套「设计语言」,让所有组件风格统一。
- ColorScheme:颜色体系,包括主色、次要色、背景色、错误色等
- Typography:字体体系,定义标题、正文、标签等各级文字样式
- Shapes:形状体系,控制组件是圆角、直角还是其他形状
这三个东西,就是Material3主题的「三驾马车」。下面我们一个一个拆开来看。
12.2 ColorScheme:颜色体系
Material3的颜色系统,比Material2要复杂一些。它引入了「色调」的概念。说白了,就是每种颜色都有浅、中、深几个变体。
举个例子,你的品牌色是蓝色。Material3会自动生成一组蓝色系颜色:浅蓝、深蓝、对比色等等。你不需要手动配几十个颜色值,只需要提供几个核心色,系统帮你搞定。
// 自定义颜色方案
private val LightColorScheme = lightColorScheme(
primary = Color(0xFF1976D2), // 主色
onPrimary = Color.White, // 主色上的文字颜色
primaryContainer = Color(0xFFBBDEFB), // 主色容器
secondary = Color(0xFF26A69A), // 次要色
background = Color(0xFFF5F5F5), // 背景色
surface = Color.White, // 表面色(卡片、弹窗等)
error = Color(0xFFD32F2F) // 错误色
)
private val DarkColorScheme = darkColorScheme(
primary = Color(0xFF90CAF9),
onPrimary = Color(0xFF0D47A1),
primaryContainer = Color(0xFF1565C0),
secondary = Color(0xFF80CBC4),
background = Color(0xFF121212),
surface = Color(0xFF1E1E1E),
error = Color(0xFFEF9A9A)
)
这里有个坑,我踩过。Material3的lightColorScheme()和darkColorScheme()有很多可选参数。如果你只设置了primary,其他颜色系统会自动计算。但自动计算的结果不一定好看。我曾经就遇到过,自动生成的onPrimary(主色上的文字)和primary对比度不够,导致白色按钮上的白色文字看不清。
primary、onPrimary、background、surface这四个颜色显式设置一下。其他颜色可以依赖自动生成,但核心颜色要自己把控。
12.3 Typography:字体体系
字体这块,Material3定义了一套完整的「文字层级」。从最大的Display(展示用大标题)到最小的Label(标签文字),一共5个大类,每个大类又有大、中、小三个尺寸。
说实话,刚开始看到这么多层级,我也觉得有点晕。但用久了你会发现,它其实很科学。比如:
- Display:用在欢迎页、大Banner上,字号最大
- Headline:用在页面标题、Section标题
- Title:用在卡片标题、列表项标题
- Body:正文内容,最常用
- Label:按钮文字、标签、小提示
// 自定义字体体系
val AppTypography = Typography(
displayLarge = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 36.sp,
lineHeight = 44.sp,
letterSpacing = (-0.25).sp
),
headlineMedium = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 24.sp,
lineHeight = 32.sp
),
titleLarge = TextStyle(
fontWeight = FontWeight.Medium,
fontSize = 20.sp,
lineHeight = 28.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
)
)
bodyLarge和titleMedium,其他用默认值。Material3的默认字体是Roboto,在中文环境下会自动回退到系统默认中文字体。
12.4 Shapes:形状体系
形状决定了组件的「圆角程度」。Material3把形状分成了三类:
| 形状层级 | 说明 | 典型应用 |
|---|---|---|
| small | 小圆角,4dp左右 | 按钮、输入框、标签 |
| medium | 中等圆角,8dp左右 | 卡片、对话框 |
| large | 大圆角,16dp左右 | 底部弹窗、侧边抽屉 |
// 自定义形状
val AppShapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(8.dp),
large = RoundedCornerShape(16.dp)
)
我个人习惯把small设成4dp,medium设成8dp,large设成16dp。这个比例比较协调。当然,如果你的设计稿有特殊要求,直接改就行。
12.5 整合到一起:完整的主题配置
把上面三个东西组合起来,就是一个完整的Material3主题:
@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
)
}
然后在你的App入口处,用AppTheme包裹所有内容:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppTheme {
// 你的页面内容
Greeting("Android")
}
}
}
}
这样,整个App就自动应用了统一的主题。而且,isSystemInDarkTheme()会自动检测系统是否处于深色模式,实现动态切换。
12.6 动态主题适配:跟随系统还是手动切换?
动态主题适配,说白了就是「App能根据系统设置,自动切换亮色/深色模式」。Material3天然支持这个功能,你只需要在MaterialTheme里根据isSystemInDarkTheme()选择不同的ColorScheme就行。
但有时候,用户可能想在App内部手动切换主题,不跟随系统。这时候就需要用CompositionLocal来保存当前的主题状态。
// 定义一个主题状态类
class ThemeState(
var isDarkTheme: Boolean by mutableStateOf(false)
)
// 用CompositionLocal提供全局访问
val LocalThemeState = staticCompositionLocalOf { ThemeState() }
// 在AppTheme中使用
@Composable
fun AppTheme(
themeState: ThemeState = remember { ThemeState() },
content: @Composable () -> Unit
) {
val colorScheme = if (themeState.isDarkTheme) DarkColorScheme else LightColorScheme
CompositionLocalProvider(LocalThemeState provides themeState) {
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
shapes = AppShapes,
content = content
)
}
}
这样,在任何子组件里,你都可以通过LocalThemeState.current来读取或修改主题状态。
isDarkTheme值。
12.7 知识体系总览
下面这张图,帮你把本章的核心知识点串起来:
12.8 实战建议
最后,分享几个我在项目里总结的经验:
- 先搭主题,再写页面。哪怕只是个小项目,也花10分钟把
AppTheme搭好。后面你会感谢自己的。 - 颜色命名要语义化。不要用
color_blue这种名字,用primary、secondary。这样换主题时,改一个值就行。 - 字体大小用sp,间距用dp。这是Android的基本规范,别搞混了。
- 测试深色模式。在开发者选项里强制开启深色模式,看看你的App有没有「亮瞎眼」的地方。
- 善用Material3的默认值。如果你不想自己配Typography,直接用
Typography()空构造,系统会给你一套合理的默认值。
嗯,关于Material3主题,今天就聊这么多。记住一句话:主题不是装饰,是基础设施。把主题搭好了,后面的开发会顺畅很多。
公众号:蓝海资料掘金营,微信deep3321