# Jetpack Compose - Material 主题

By [Taylor](https://paragraph.com/@taylor-4) · 2022-05-16

---

`Compose` 默认提供了 `Material Design` 的主题实现，基于这个主题可以实现 Google 亲儿子般的 UI：

![Material Design](https://storage.googleapis.com/papyrus_images/a6ad828d654de60afc104c9eb72a62842968e839f0828cff1ca3188c5132e193.png)

Material Design

Material 主题
-----------

从 `Compose` 的架构分层来看，`Material` 的实现处于最上层，也就是说如果脱离 `androidx.compose.material.*`, `Compose` 也是一样可以用的。

![Compose Layers](https://storage.googleapis.com/papyrus_images/9c249670c08507e645464b4a088866bdca43bfd7343ba01787832f60a425277a.png)

Compose Layers

`Compose` 中 `MaterialTheme` 的定义：

    MaterialTheme(
        colors = …,
        typography = …,
        shapes = …
    ) {
        // app content
    }
    

### 颜色 - Color

Google 说强烈建议使用 `Compose` 中的 `Color` 类来定义和管理颜色，这样可以轻松支持主题的定制，甚至还能把主题嵌套起来用。

    val Red = Color(0xffff0000)
    val Blue = Color(red = 0f, green = 0f, blue = 1f)
    

例如：定义一个支持深色和浅色的主题：

    private val Yellow200 = Color(0xffffeb46)
    private val Blue200 = Color(0xff91a4fc)
    // ...
    
    private val DarkColors = darkColors(
        primary = Yellow200,
        secondary = Blue200,
        // ...
    )
    private val LightColors = lightColors(
        primary = Yellow500,
        primaryVariant = Yellow400,
        secondary = Blue700,
        // ...
    )
    

应用到 `MaterialTheme`：

    MaterialTheme(
        colors = if (darkTheme) DarkColors else LightColors
    ) {
        // app content
    }
    

使用：

    Text(
        text = "Hello theming",
        color = MaterialTheme.colors.primary
    )	
    

#### 深色主题

通过向 `MaterialThem`e 提供一组不同的 `colors` 来实现深浅两种主题：

    @Composable
    fun MyTheme(
        darkTheme: Boolean = isSystemInDarkTheme(),
        content: @Composable () -> Unit
    ) {
        MaterialTheme(
            colors = if (darkTheme) DarkColors else LightColors,
            /*...*/
            content = content
        )
    }
    

`isSystemInDarkTheme()` 会查找设备当前是否为深色主题。

### 字体排版 - Typography

![](https://storage.googleapis.com/papyrus_images/d78526fda49eb00385a4d093b214177c1e401ab43abc629a80e321deb745d941.png)

`MaterialTheme` 采用 `Typography`、`TextStyle` 类实现默认的字体字型定义，构造`MaterialTheme`时传入：

    val Rubik = FontFamily(
        Font(R.font.rubik_regular),
        Font(R.font.rubik_medium, FontWeight.W500),
        Font(R.font.rubik_bold, FontWeight.Bold)
    )
    
    val MyTypography = Typography(
        h1 = TextStyle(
            fontFamily = Rubik,
            fontWeight = FontWeight.W300,
            fontSize = 96.sp
        ),
        body1 = TextStyle(
            fontFamily = Rubik,
            fontWeight = FontWeight.W600,
            fontSize = 16.sp
        )
        /*...*/
    )
    MaterialTheme(typography = MyTypography, /*...*/)
    

然后通过 `MaterialTheme.typrography` 访问对应 `TextStyle`即可：

    Text(
        text = "Subtitle2 styled",
        style = MaterialTheme.typography.subtitle2
    )
    

### 形状 - Shapes

![Shapes](https://storage.googleapis.com/papyrus_images/023c596599339b2b178d3322e5ebc61aa8c357163c20abc7b45345dc1444a435.png)

Shapes

`Compose` 使用 `Shapes` 实现形状系统，可以设置每个大小类别的形状默认状态：

    val Shapes = Shapes(
        small = RoundedCornerShape(percent = 50),
        medium = RoundedCornerShape(0f),
        large = CutCornerShape(
            topStart = 16.dp,
            topEnd = 0.dp,
            bottomEnd = 0.dp,
            bottomStart = 16.dp
        )
    )
    
    MaterialTheme(shapes = Shapes, /*...*/)
    

`MaterialTheme` 认为默认情况下组件使用的形状基本上符合这几个大小状态，比如`Button`、`Text` 等默认为 “小”，`AlertDialog` 默认为“中”， `Drawer` 为 “大”

使用：

    Surface(
        shape = MaterialTheme.shapes.medium, /*...*/
    ) {
        /*...*/
    }
    

Material Design 3（下一代 Material Design）
--------------------------------------

为了适配下一代`Material Design`，`Compose` 已经开始实现 `Material Design 3`， 包含更新的主题和组件，以及对Material You的个性风格的支持。

> **注意**：
> 
> *   `androidx.compose.material3` 目前为 Alpha 版。此 API 随时可能发生变化，因此不应视为最终版本。
>     
> *   “Material Design 3”、“Material 3”和“M3”这三个术语可以互换。现有的 Material Design 规范和相应的 `androidx.compose.material` 库称为“Material Design 2”、“Material 2”或“M2”。
>     

M3 主题定义了新的配置方案和文字排版单元，目前还没有对形状的支持。但是官方说 **Updates to shapes coming soon**

    @Composable
    fun MaterialTheme(
        colorScheme: ColorScheme? = MaterialTheme.colorScheme,
        typography: Typography? = MaterialTheme.typography,
        content: (@Composable () -> Unit)?
    ): Unit	
    

### M3 中的 ColorScheme

`M2` 中的 `Color` 变成了 `ColorScheme`，同样以深色浅色举例：

    private val Blue40 = Color(0xff1e40ff)
    private val DarkBlue40 = Color(0xff3e41f4)
    private val Yellow40 = Color(0xff7d5700)
    // Remaining colors from tonal palettes
    
    private val LightColorScheme = lightColorScheme(
        primary = Blue40,
        secondary = DarkBlue40,
        tertiary = Yellow40,
        // error, primaryContainer, onSecondary, etc.
    )
    private val DarkColorScheme = darkColorScheme(
        primary = Blue80,
        secondary = DarkBlue80,
        tertiary = Yellow80,
        // error, primaryContainer, onSecondary, etc.
    )
    // 使用：
    val darkTheme = isSystemInDarkTheme()
    MaterialTheme(
        colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
    ) {
        // M3 app content
    }
    

M3 中使用的“**primary**”、“**background**”和“**error**”等等这些值有了”插槽“的概念，他们组合在一起形成一个配色方案，每个槽位的颜色获取来自”\\色调模板)“，

![material-m3-baseline-color-schemes](https://storage.googleapis.com/papyrus_images/b026392fcefcc92e4499721886dc2be64a11c8b8d30d005d9ee9c725a2de95b6.png)

material-m3-baseline-color-schemes

#### 在Material You中使用动态配色方案

`Material You` 是在 `Android 12` 中引入的概念，以用户壁纸为原型派生自定义颜色，并将其应用到系统。M3中的动态配色方案以此为起点：

    // Dynamic color is available on Android 12+
    val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
    val colorScheme = when {
        dynamicColor && darkTheme -> dynamicDarkColorScheme(LocalContext.current)
        dynamicColor && !darkTheme -> dynamicLightColorScheme(LocalContext.current)
        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }
    // 使用：
    Text(
        text = "Hello M3 theming",
        color = MaterialTheme.colorScheme.tertiary
    )
    

### M3 中的 Typography

M3 中用了新的 `Typography` 和已有的 `TextStyle`， 新增了**字体比例**的定义，同时命名和分组简化为: 显示、大标题、标题、正文和标签，每个都有大号、中号和小号。

![](https://storage.googleapis.com/papyrus_images/b919753223907347e15b6217e6a5bd65f56dcd37c52e9b8d1ae2204da31f3be9.png)

    val KarlaFontFamily = FontFamily(
        Font(R.font.karla_regular),
        Font(R.font.karla_bold, FontWeight.Bold)
    )
    
    val AppTypography = Typography(
        bodyLarge = TextStyle(
            fontFamily = KarlaFontFamily,
            fontWeight = FontWeight.Normal,
            fontSize = 16.sp,
            lineHeight = 24.sp,
            letterSpacing = 0.15.sp
        ),
        // titleMedium, labelSmall, etc.
    )
    
    MaterialTheme(
        typography = AppTypography
    ) {
        // M3 app content
    }
    
    // 使用：
    Text(
        text = "Hello M3 theming",
        style = MaterialTheme.typography.bodyLarge
    )
    

自定义主题
-----

`MaterialTheme` 的设计看上去在 UI 层面考虑已经比较全面了，但是回到现实世界问题依然很明显：很多情况下依然无法满足设计上的需求与多样化。我们需要比现成的`Material Theme`更复杂的主题。 在实际开发中更方便的做法是基于 `Material Design` 之上对主题进行自定义。

### 扩展 Material

一种比较常见的情况是尽管主题中已经提供了很多颜色，但是依然无法满足我们的需求，如果我们的使用需求与`MaterialTheme` 的 `API`是一致的，只是不够丰富，那么我们对已有的`Colors`进行扩展即可：

    // Use with MaterialTheme.colors.snackbarAction
    val Colors.snackbarAction: Color
        get() = if (isLight) Red300 else Red700
    

同样的情况也适用于 `Typography` `Shapes`：

    // Use with MaterialTheme.typography.textFieldInput
    val Typography.textFieldInput: TextStyle
        get() = TextStyle(/* ... */)
    
    // Use with MaterialTheme.shapes.card
    val Shapes.card: Shape
        get() = RoundedCornerShape(size = 20.dp)
    

### 基于 CompositionLocal 扩展 Material

`CompositionLocal` 可以在 `Composable` 中以某个节点开始将数据“向下”传递到每一个子节点。用 Google 官方的话说就是**将数据的作用域限定在局部**。 为什么要这样用呢？

对于广泛使用的常用数据，在`Composable` 中显示的传递是一个非常麻烦的过程，特别是这种主题相关的数据：

    @Composable
    fun MyApp() {
        // Theme information tends to be defined near the root of the application
        val colors = …
    }
    
    // Some composable deep in the hierarchy
    @Composable
    fun SomeTextLabel(labelText: String) {
        Text(
            text = labelText,
            color = // ← need to access colors here
        )
    }
    

为了让 `colors` 无需显式传递给大多数子节点`Composable`， `Compose` 提供 `CompositionLocal` 来创建以树为作用域的对象，它通常在界面树的某个节点以值的形式提供，这个值可以在子节点中使用，而无需将这个对象声明为参数。

`MaterialTheme`的 `Color`、`Typography`、`Shapes` 就是基于这个组件实现的，对应 `LocalColors`、`LocalShapes` 和 `LocalTypography` 属性。

`CompositionLocal` 实例的作用域限定在`Composable`中，因此可以在树的不同级别提供不同的值。如果需要更新`CompositionLocal` 提供新值，需使用 `CompositionLocalProvider` 的 `infix` 函数 `provider`。`Composable` 作为 `CompositionLocalProvider` 的 `content lambda`，会获取 `CompositionLocal` 的 `current` 以达到在任意节点读取新值的目的。

例如： `LocalContentAlpha` `CompositionLocal` 的值是给作用域内的文本和图标提供当前主题定义的 `Aplha`。强调或弱化界面不同部分，在其内部的`Composable` 在获取`LocalContentAlpha`时，就会获取到最近一次提供的新的值，如下示例 `CompositionLocalProvider` 用于为 `Composable` 的不同部分提供不同的值：

    @Composable
    fun CompositionLocalExample() {
        MaterialTheme { // MaterialTheme sets ContentAlpha.high as default
            Column {
                Text("Uses MaterialTheme's provided alpha")
                CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                    Text("Medium value provided for LocalContentAlpha")
                    Text("This Text also uses the medium value")
                    CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
                        DescendantExample()
                    }
                }
            }
        }
    }
    
    @Composable
    fun DescendantExample() {
        // CompositionLocalProviders also work across composable functions
        Text("This Text uses the disabled alpha now")
    }
    
    // Text Composable 的内部实现：
    @Composable
    fun Text(
        text: String,
        modifier: Modifier = Modifier,
        color: Color = Color.Unspecified,
        ...
    ) {
    
        val textColor = color.takeOrElse {
            style.color.takeOrElse {
              // 读取 colorAlpha
              LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
            }
        }
        
        ...
    }
    

![compositionlocal-alpha](https://storage.googleapis.com/papyrus_images/87dba068ec00b4dc554ac132b43f086facb72ad9bb313b03ff6d6755a0335b1c.png)

compositionlocal-alpha

由此引入另一种方式： 封装 `MaterialTheme`， 保留其原本的设定的情况下，扩展出其它值。

例如颜色：

    @Immutable
    data class ExtendedColors(
        val tertiary: Color,
        val onTertiary: Color
    )
    
    val LocalExtendedColors = staticCompositionLocalOf {
        ExtendedColors(
            tertiary = Color.Unspecified,
            onTertiary = Color.Unspecified
        )
    }
    
    @Composable
    fun ExtendedTheme(
        /* ... */
        content: @Composable () -> Unit
    ) {
        val extendedColors = ExtendedColors(
            tertiary = Color(0xFFA8EFF0),
            onTertiary = Color(0xFF002021)
        )
        CompositionLocalProvider(LocalExtendedColors provides extendedColors) {
            MaterialTheme(
                /* colors = ..., typography = ..., shapes = ... */
                content = content
            )
        }
    }
    
    // Use with eg. ExtendedTheme.colors.tertiary
    object ExtendedTheme {
        val colors: ExtendedColors
            @Composable
            get() = LocalExtendedColors.current
    }
    

应用到 `Material` 组件：

    @Composable
    fun ExtendedButton(
        onClick: () -> Unit,
        modifier: Modifier = Modifier,
        content: @Composable RowScope.() -> Unit
    ) {
        Button(
            colors = ButtonDefaults.buttonColors(
                backgroundColor = ExtendedTheme.colors.tertiary,
                contentColor = ExtendedTheme.colors.onTertiary
                /* Other colors use values from MaterialTheme */
            ),
            onClick = onClick,
            modifier = modifier,
            content = content
        )
    }
    

### 替换 Material 系统

对 `MaterialTheme` 中的一个或多个单元（`Colors`、`Typography` 或 `Shapes`）进行替换，同时保留其它单元：

    @Immutable
    data class ReplacementTypography(
        val body: TextStyle,
        val title: TextStyle
    )
    
    @Immutable
    data class ReplacementShapes(
        val component: Shape,
        val surface: Shape
    )
    
    val LocalReplacementTypography = staticCompositionLocalOf {
        ReplacementTypography(
            body = TextStyle.Default,
            title = TextStyle.Default
        )
    }
    val LocalReplacementShapes = staticCompositionLocalOf {
        ReplacementShapes(
            component = RoundedCornerShape(ZeroCornerSize),
            surface = RoundedCornerShape(ZeroCornerSize)
        )
    }
    
    @Composable
    fun ReplacementTheme(
        /* ... */
        content: @Composable () -> Unit
    ) {
        val replacementTypography = ReplacementTypography(
            body = TextStyle(fontSize = 16.sp),
            title = TextStyle(fontSize = 32.sp)
        )
        val replacementShapes = ReplacementShapes(
            component = RoundedCornerShape(percent = 50),
            surface = RoundedCornerShape(size = 40.dp)
        )
        CompositionLocalProvider(
            LocalReplacementTypography provides replacementTypography,
            LocalReplacementShapes provides replacementShapes
        ) {
            MaterialTheme(
                /* colors = ... */
                content = content
            )
        }
    }
    
    // Use with eg. ReplacementTheme.typography.body
    object ReplacementTheme {
        val typography: ReplacementTypography
            @Composable
            get() = LocalReplacementTypography.current
        val shapes: ReplacementShapes
            @Composable
            get() = LocalReplacementShapes.current
    }
    

应用到`Material`组件：

    @Composable
    fun ReplacementButton(
        onClick: () -> Unit,
        modifier: Modifier = Modifier,
        content: @Composable RowScope.() -> Unit
    ) {
        Button(
            shape = ReplacementTheme.shapes.component,
            onClick = onClick,
            modifier = modifier,
            content = {
                ProvideTextStyle(
                    value = ReplacementTheme.typography.body
                ) {
                    content()
                }
            }
        )
    }
    

### 实现完全自定义主题系统

设计并非仅限于 `Material Design`，完全自主定义一套设计语言也是可行的。 正如最开始所说，如果整个系统完全不依赖`Material`层，也是完全可行的。

如下示例就实现了一套跟`Material Design` 一丝儿关系都没有的主题。

    @Immutable
    data class CustomColors(
        val content: Color,
        val component: Color,
        val background: List<Color>
    )
    
    @Immutable
    data class CustomTypography(
        val body: TextStyle,
        val title: TextStyle
    )
    
    @Immutable
    data class CustomElevation(
        val default: Dp,
        val pressed: Dp
    )
    
    val LocalCustomColors = staticCompositionLocalOf {
        CustomColors(
            content = Color.Unspecified,
            component = Color.Unspecified,
            background = emptyList()
        )
    }
    val LocalCustomTypography = staticCompositionLocalOf {
        CustomTypography(
            body = TextStyle.Default,
            title = TextStyle.Default
        )
    }
    val LocalCustomElevation = staticCompositionLocalOf {
        CustomElevation(
            default = Dp.Unspecified,
            pressed = Dp.Unspecified
        )
    }
    
    @Composable
    fun CustomTheme(
        /* ... */
        content: @Composable () -> Unit
    ) {
        val customColors = CustomColors(
            content = Color(0xFFDD0D3C),
            component = Color(0xFFC20029),
            background = listOf(Color.White, Color(0xFFF8BBD0))
        )
        val customTypography = CustomTypography(
            body = TextStyle(fontSize = 16.sp),
            title = TextStyle(fontSize = 32.sp)
        )
        val customElevation = CustomElevation(
            default = 4.dp,
            pressed = 8.dp
        )
        CompositionLocalProvider(
            LocalCustomColors provides customColors,
            LocalCustomTypography provides customTypography,
            LocalCustomElevation provides customElevation,
            content = content
        )
    }
    
    // Use with eg. CustomTheme.elevation.small
    object CustomTheme {
        val colors: CustomColors
            @Composable
            get() = LocalCustomColors.current
        val typography: CustomTypography
            @Composable
            get() = LocalCustomTypography.current
        val elevation: CustomElevation
            @Composable
            get() = LocalCustomElevation.current
    }

---

*Originally published on [Taylor](https://paragraph.com/@taylor-4/jetpack-compose-material)*
