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)
)
我的习惯:我会把LightColorScheme和DarkColorScheme定义成顶层变量,放在一个单独的Theme.kt文件里。这样整个项目都能引用,改起来也方便。

这里有个坑,我踩过。Material3的lightColorScheme()darkColorScheme()有很多可选参数。如果你只设置了primary,其他颜色系统会自动计算。但自动计算的结果不一定好看。我曾经就遇到过,自动生成的onPrimary(主色上的文字)和primary对比度不够,导致白色按钮上的白色文字看不清。

避坑指南:建议至少把primaryonPrimarybackgroundsurface这四个颜色显式设置一下。其他颜色可以依赖自动生成,但核心颜色要自己把控。

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
    )
)
小技巧:如果你不想自己配这么多,可以只修改几个常用的。比如只改bodyLargetitleMedium,其他用默认值。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 知识体系总览

下面这张图,帮你把本章的核心知识点串起来:

Material3 主题 ColorScheme primary / secondary background / surface error / onPrimary ... Typography Display / Headline Title / Body / Label 各分 Large/Medium/Small Shapes small (4dp) medium (8dp) large (16dp) 动态主题适配 跟随系统 / 手动切换 / CompositionLocal

12.8 实战建议

最后,分享几个我在项目里总结的经验:

  1. 先搭主题,再写页面。哪怕只是个小项目,也花10分钟把AppTheme搭好。后面你会感谢自己的。
  2. 颜色命名要语义化。不要用color_blue这种名字,用primarysecondary。这样换主题时,改一个值就行。
  3. 字体大小用sp,间距用dp。这是Android的基本规范,别搞混了。
  4. 测试深色模式。在开发者选项里强制开启深色模式,看看你的App有没有「亮瞎眼」的地方。
  5. 善用Material3的默认值。如果你不想自己配Typography,直接用Typography()空构造,系统会给你一套合理的默认值。
我的小秘密:我电脑上存了一个「主题模板」文件。每次开新项目,直接复制过去改几个颜色值就行。省时省力,还不会漏掉配置。

嗯,关于Material3主题,今天就聊这么多。记住一句话:主题不是装饰,是基础设施。把主题搭好了,后面的开发会顺畅很多。


公众号:蓝海资料掘金营,微信deep3321