虽然 Material 是我们推荐的设计系统并且 Jetpack Compose 附带了 Material 的实现,但并非只能使用该系统。Material 完全基于公共 API 构建而成,因此您可以按照同样的方式自行创建设计系统。
您可以采用以下几种方法:
- 通过其他主题值扩展
MaterialTheme
- 将一个或多个 Material 系统(
Colors
、Typography
或Shapes
)替换为自定义实现,同时保留其他实现 - 实现完全自定义的设计系统以替换
MaterialTheme
您可能还需要继续将 Material 组件与自定义设计系统结合使用。您可以这样做,但您在采用某种方法时需注意一些事项。
如需详细了解 MaterialTheme
以及自定义设计系统使用的较低级别的结构体和 API,请参阅 Compose 中的主题详解指南。
扩展 Material 主题设置
Compose Material 会对 Material 主题设置进行相近建模,使得遵循 Material 准则既简单又能确保类型安全。不过,您也可以使用其他值扩展颜色、字体排版和形状集。
最简单的方法是添加扩展属性:
// Use with MaterialTheme.colors.snackbarAction
val Colors.snackbarAction: Color
get() = if (isLight) 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 一致。例如,primarySurface
由 Compose 定义,且根据 Colors.isLight
充当 primary
和 surface
之间的代理。
另一种方法是定义可“封装”MaterialTheme
及其值的扩展主题。
假设您要添加另外两种颜色(tertiary
和 onTertiary
),且保留现有 Material 颜色:
@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
}
这与使用 MaterialTheme
的 API 类似。此外,它还支持多个主题,因为您可以使用与 MaterialTheme
相同的方式嵌套 ExtendedTheme
。
使用 Material 组件
扩展 Material 主题设置时,现有 MaterialTheme
值将持不变,且 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
)
}
然后,您可以根据需要将 Button
替换为 ExtendedButton
。
@Composable
fun ExtendedApp() {
ExtendedTheme {
/*...*/
ExtendedButton(onClick = { /* ... */ }) {
/* ... */
}
}
}
替换 Material 系统
您可能希望通过自定义实现替换一个或多个系统(Colors
、Typography
或 Shapes
),同时保留其他系统,而非扩展 Material 主题设置。
假设您希望替换类型系统和形状系统,同时保留颜色系统:
@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 主题设置系统ContentAlpha
:用于表示Text
和Icon
中的强调效果的不透明度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(
backgroundColor = CustomTheme.colors.component,
contentColor = CustomTheme.colors.content,
disabledBackgroundColor = CustomTheme.colors.content
.copy(alpha = 0.12f)
.compositeOver(CustomTheme.colors.component),
disabledContentColor = CustomTheme.colors.content
.copy(alpha = ContentAlpha.disabled)
),
shape = ButtonShape,
elevation = ButtonDefaults.elevation(
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
。