虽然 Material 是我们推荐的设计系统,Jetpack Compose 附带了 Material 的实现,但您不必使用它。Material 完全基于公共 API 构建,因此您可以以相同的方式创建自己的设计系统。
您可以采用几种方法
- 使用其他主题值扩展
MaterialTheme
- 替换一个或多个 Material 系统 —
Colors
、Typography
或Shapes
— 使用自定义实现,同时保留其他系统 - 实现完全自定义的设计系统 以替换
MaterialTheme
您可能还想在自定义设计系统中继续使用 Material 组件。这样做是可行的,但需要注意一些事项以适应您采用的方法。
要了解有关 MaterialTheme
和自定义设计系统使用的低级别结构和 API 的更多信息,请查看 Compose 中主题的解剖 指南。
扩展 Material 主题
Compose Material 紧密地模仿了 Material 主题,以便简单且类型安全地遵循 Material 指南。但是,您可以使用其他值扩展颜色、排版和形状集。
最简单的方法是添加扩展属性
// Use with MaterialTheme.colorScheme.snackbarAction val ColorScheme.snackbarAction: Color @Composable get() = if (isSystemInDarkTheme()) Red300 else Red700 // 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)
这为 MaterialTheme
使用 API 提供了一致性。Compose 本身定义的一个示例是 surfaceColorAtElevation
,它根据高度确定应使用的表面颜色。
另一种方法是定义一个扩展主题,它“包装”MaterialTheme
及其值。
假设您要添加两种额外的颜色 — caution
和 onCaution
,一种用于半危险操作的黄色 — 同时保留现有的 Material 颜色
@Immutable data class ExtendedColors( val caution: Color, val onCaution: Color ) val LocalExtendedColors = staticCompositionLocalOf { ExtendedColors( caution = Color.Unspecified, onCaution = Color.Unspecified ) } @Composable fun ExtendedTheme( /* ... */ content: @Composable () -> Unit ) { val extendedColors = ExtendedColors( caution = Color(0xFFFFCC02), onCaution = Color(0xFF2C2D30) ) CompositionLocalProvider(LocalExtendedColors provides extendedColors) { MaterialTheme( /* colors = ..., typography = ..., shapes = ... */ content = content ) } } // Use with eg. ExtendedTheme.colors.caution object ExtendedTheme { val colors: ExtendedColors @Composable get() = LocalExtendedColors.current }
这与 MaterialTheme
使用 API 类似。它还支持多个主题,因为您可以像使用 MaterialTheme
一样嵌套 ExtendedTheme
。
使用 Material 组件
扩展 Material 主题时,会保留现有的 MaterialTheme
值,Material 组件仍然具有合理的默认值。
如果您要在组件中使用扩展值,请将它们包装在您自己的可组合函数中,直接设置要更改的值,并将其他值作为参数公开给包含的可组合函数
@Composable fun ExtendedButton( onClick: () -> Unit, modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit ) { Button( colors = ButtonDefaults.buttonColors( containerColor = ExtendedTheme.colors.caution, contentColor = ExtendedTheme.colors.onCaution /* Other colors use values from MaterialTheme */ ), onClick = onClick, modifier = modifier, content = content ) }
然后,您可以在适当的位置将 Button
的使用替换为 ExtendedButton
。
@Composable fun ExtendedApp() { ExtendedTheme { /*...*/ ExtendedButton(onClick = { /* ... */ }) { /* ... */ } } }
替换 Material 子系统
您可能不想扩展 Material 主题,而是要替换一个或多个系统 — 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 组件
当替换 MaterialTheme
的一个或多个系统时,直接使用 Material 组件可能会导致出现意外的 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() } } ) }
然后,您可以在适当的位置将 Button
的使用替换为 ReplacementButton
。
@Composable fun ReplacementApp() { ReplacementTheme { /*...*/ ReplacementButton(onClick = { /* ... */ }) { /* ... */ } } }
实现完全自定义的设计系统
您可能希望使用完全自定义的设计系统替换 Material 主题。请注意 MaterialTheme
提供以下系统
Colors
、Typography
和Shapes
:Material 主题系统TextSelectionColors
:Text
和TextField
用于文本选择的颜色Ripple
和RippleTheme
:Indication
的 Material 实现
如果您想继续使用 Material 组件,则需要在自定义主题中替换其中一些系统,或者在组件中处理这些系统,以避免出现意外行为。
但是,设计系统并不局限于 Material 所依赖的概念。您可以修改现有系统并引入全新的系统 — 使用新类和类型 — 使其他概念与主题兼容。
在以下代码中,我们模拟了一个自定义颜色系统,该系统包括渐变 (List<Color>
),包含一个类型系统,引入了一个新的高度系统,并排除了 MaterialTheme
提供的其他系统
@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 }
使用 Material 组件
当不存在 MaterialTheme
时,直接使用 Material 组件会导致出现意外的 Material 颜色、类型和形状值以及指示行为。
如果您要在组件中使用自定义值,请将它们包装在您自己的可组合函数中,直接为相关系统设置值,并将其他值作为参数公开给包含的可组合函数。
我们建议您从自定义主题中访问您设置的值。或者,如果您的主题没有提供 Color
、TextStyle
、Shape
或其他系统,您可以将它们硬编码。
@Composable fun CustomButton( onClick: () -> Unit, modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit ) { Button( colors = ButtonDefaults.buttonColors( containerColor = CustomTheme.colors.component, contentColor = CustomTheme.colors.content, disabledContainerColor = CustomTheme.colors.content .copy(alpha = 0.12f) .compositeOver(CustomTheme.colors.component), disabledContentColor = CustomTheme.colors.content .copy(alpha = 0.38f) ), shape = ButtonShape, elevation = ButtonDefaults.elevatedButtonElevation( defaultElevation = CustomTheme.elevation.default, pressedElevation = CustomTheme.elevation.pressed /* disabledElevation = 0.dp */ ), onClick = onClick, modifier = modifier, content = { ProvideTextStyle( value = CustomTheme.typography.body ) { content() } } ) } val ButtonShape = RoundedCornerShape(percent = 50)
如果您引入了新的类类型 — 例如 List<Color>
用于表示渐变 — 那么最好从头开始实现组件,而不是包装它们。例如,请查看 Jetsnack 示例中的 JetsnackButton
。
推荐给你
- 注意:当 JavaScript 关闭时,链接文本将显示
- Compose 中的 Material Design 3
- 将 Compose 中的 Material 2 迁移到 Material 3
- Compose 中主题的解剖