17、主题与样式:MaterialTheme、Colors、Typography、Shapes 的自定义

说实话,做 Android 开发这么多年,我见过太多「五彩斑斓的灰」—— 就是那种每个页面颜色都不一样、字体忽大忽小的 App。用户嘴上不说,但心里肯定觉得这产品不靠谱。

在 Compose 里,主题系统是咱们的救星。它把颜色、字体、形状这些视觉元素统一管理起来。你改一个地方,全局都跟着变。今天我就带你把这套东西玩明白。

核心思想: 主题就是设计系统的「宪法」。所有组件都得遵守它。

17.1 MaterialTheme 是什么?

说白了,MaterialTheme 就是一个 Composable 函数。它把三个核心对象 —— ColorsTypographyShapes —— 通过 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 定义了从 displayLargelabelSmall 共 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
    )
)
注意: 如果你用了自定义字体(比如从 assets 加载的),一定要在 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() 或者 Surfaceshape 参数单独覆盖。但全局统一用这三个,能保证视觉一致性。

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 知识体系总览

下面这张图帮你理清整个主题系统的结构:

MaterialTheme 主题系统 MaterialTheme Colors Typography Shapes primary / secondary background / surface onPrimary / onSecondary display / headline title / body label(共15种) small(按钮) medium(卡片) large(弹窗) 通过 CompositionLocal 向下传递,全局生效

17.7 避坑指南

最后,分享几个我踩过的坑:

  • 颜色不要硬编码:我曾经在十几个文件里写了 Color(0xFF1976D2),后来改品牌色时改到崩溃。一定要用 MaterialTheme.colorScheme.primary
  • 暗色主题不是「亮色取反」:很多人直接把亮色的颜色取反当暗色用,结果对比度一塌糊涂。暗色主题需要单独设计。
  • 字体文件要提前加载:如果你用了自定义字体,记得在 setContent 之前用 FontFamily.Load 加载,否则会有闪白。
  • Shape 不要滥用:每个组件都单独设 shape,最后就是「四不像」。能用主题默认的就用默认的。

好了,主题系统就讲到这里。你只要把 ColorsTypographyShapes 这三个东西管好,整个 App 的视觉一致性就有了保障。下次设计师改需求时,你只需要改一个文件,而不是翻遍整个项目。

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