尽管 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 中的主题构成