# Jetpack Compose - Material 主题 **Published by:** [Taylor](https://paragraph.com/@taylor-4/) **Published on:** 2022-05-16 **URL:** https://paragraph.com/@taylor-4/jetpack-compose-material ## Content Compose 默认提供了 Material Design 的主题实现,基于这个主题可以实现 Google 亲儿子般的 UI:Material DesignMaterial 主题从 Compose 的架构分层来看,Material 的实现处于最上层,也就是说如果脱离 androidx.compose.material.*, Compose 也是一样可以用的。Compose LayersCompose 中 MaterialTheme 的定义:MaterialTheme( colors = …, typography = …, shapes = … ) { // app content } 颜色 - ColorGoogle 说强烈建议使用 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 ) 深色主题通过向 MaterialTheme 提供一组不同的 colors 来实现深浅两种主题:@Composable fun MyTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { MaterialTheme( colors = if (darkTheme) DarkColors else LightColors, /*...*/ content = content ) } isSystemInDarkTheme() 会查找设备当前是否为深色主题。字体排版 - TypographyMaterialTheme 采用 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 ) 形状 - ShapesShapesCompose 使用 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 中的 ColorSchemeM2 中的 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在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 中的 TypographyM3 中用了新的 Typography 和已有的 TextStyle, 新增了字体比例的定义,同时命名和分组简化为: 显示、大标题、标题、正文和标签,每个都有大号、中号和小号。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 扩展 MaterialCompositionLocal 可以在 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由此引入另一种方式: 封装 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 } ## Publication Information - [Taylor](https://paragraph.com/@taylor-4/): Publication homepage - [All Posts](https://paragraph.com/@taylor-4/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@taylor-4): Subscribe to updates