您可以通过多种方式使用样式来构建应用。您选择哪种方式取决于您的应用在采用 Material Design 方面处于哪个阶段:
- 完全自定义的设计系统,不使用 Material Design
- 建议:定义从主题中提取值的组件样式,并在设计系统组件上公开样式参数。
- 使用 Material Design
- 建议:等待 Material 采用,以便与样式集成。 尽可能在您自己的组件上使用样式。
样式层
在传统的 Compose 模型中,自定义通常在很大程度上依赖于替换 MaterialTheme 提供的全局令牌(颜色和排版),或者尽可能封装和替换设计系统可组合函数的属性。有时,Material 层中存在一些未通过子系统或参数公开的属性,但这些属性是组件本身上的硬编码默认值。
借助 Styles API,可以在子系统和组件之间添加一个抽象层:样式。
| 层 | 责任 | 示例 |
|---|---|---|
| 子系统值 | 命名值 | val Primary = Color(0xFF34A85E) |
| 原子样式 | 仅更改一个属性的样式 | val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then largeSize then interactiveShadowAtomic |
| 组件样式 | 特定于组件的配置 | 具有主要背景和 16dp 内边距的按钮。val buttonStyle = Style { contentPadding(16.dp) shape(RoundedCornerShape(8.dp)) background(Color.Blue) } |
| 组件 | 使用样式的功能性界面元素。 | Button(style = buttonStyle) { ... } |
原子样式与单体式样式
借助 Styles API,您可以将样式分解为单独的原子样式。除了定义复杂的组件专用样式(如 baseButtonStyle)之外,您还可以创建小型的单用途实用程序样式。这些是您的“原子”。
// Define single-purpose "atomic" styles val paddingAtomic = Style { contentPadding(16.dp) } val roundedCornerShapeAtomic = Style { shape(RoundedCornerShape(8.dp)) } val primaryBackgroundAtomic = Style { background(Color.Blue) } val largeSizeAtomic = Style { size(100.dp, 40.dp) } val interactiveShadowAtomic = Style { hovered { animate { dropShadow( Shadow( offset = DpOffset( 0.dp, 0.dp ), radius = 2.dp, spread = 0.dp, color = Color.Blue, ) ) } } }
使用“then”的组合
新的样式 API 的一项强大功能是 then 运算符,可让您合并多个 Style 对象。这样,您就可以使用原子实用程序类构建组件。
传统(非原子):
// One large monolithic style val buttonStyle = Style { contentPadding(16.dp) shape(RoundedCornerShape(8.dp)) background(Color.Blue) }
原子重构:
// Combine atoms to create the final appearance val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then interactiveShadowAtomic
在设计体系中采用样式
在设计系统中采用样式时,请考虑以下选项,具体取决于设计系统在频谱中的位置。
使用样式实现的自定义设计系统
考虑使用时机:您收到了一份不基于 Material Design 的详尽品牌指南,并且不打算使用 Material Design。
策略:实现完全自定义的设计系统,并将样式作为主题的一部分公开。
如果您不使用 Material 作为主要设计系统语言,则此选项为自定义路径。您完全绕过了 MaterialTheme 来定义视觉效果,并且已经创建了自己的自定义主题。您构建一个 CompanyTheme,用作样式的容器。
- 运作方式:创建一个
CompanyTheme对象,其中包含系统中每个组件的Style对象。您的组件(Material 逻辑的封装容器或自定义Box或Layout实现)直接使用这些样式,并为设计系统的使用者公开Style参数。 - 样式层:样式是设计系统的主要定义。令牌是输入到这些样式的命名变量。这样可以实现深度自定义,例如为状态变化定义独特的动画(例如,在按压时为缩放和颜色添加动画效果)。
如果您在不使用 Material 的情况下构建自己的自定义主题背景,并且想要采用样式,请将样式列表添加到您的主题背景中。这样,您就可以从项目中的任何位置访问基本样式。
创建一个
Styles类,用于存储应用中的各种样式并创建默认值。例如,在 Jetsnack 应用中,该类的名称为JetsnackStyles:object JetsnackStyles{ val buttonStyle: Style = Style { shape(shapes.medium) background(colors.brand) contentColor(colors.textPrimary) contentPaddingVertical(8.dp) contentPaddingHorizontal(24.dp) textStyle(typography.labelLarge) disabled { animate { background(colors.brandSecondary) } } } val cardStyle: Style = Style { shape(shapes.medium) background(colors.uiBackground) contentColor(colors.textPrimary) } }
提供
Styles作为整体主题的一部分,并在StyleScope上公开辅助扩展函数以访问子系统:@Immutable class JetsnackTheme( val colors: JetsnackColors = LightJetsnackColors, val typography: androidx.compose.material3.Typography = androidx.compose.material3.Typography(), val shapes: Shapes = Shapes() ) { companion object { val colors: JetsnackColors @Composable @ReadOnlyComposable get() = LocalJetsnackTheme.current.colors val typography: androidx.compose.material3.Typography @Composable @ReadOnlyComposable get() = LocalJetsnackTheme.current.typography val shapes: Shapes @Composable @ReadOnlyComposable get() = LocalJetsnackTheme.current.shapes val styles: JetsnackStyles = JetsnackStyles val LocalJetsnackTheme: ProvidableCompositionLocal<JetsnackTheme> get() = LocalJetsnackThemeInstance } } val StyleScope.colors: JetsnackColors get() = LocalJetsnackTheme.currentValue.colors val StyleScope.typography: androidx.compose.material3.Typography get() = LocalJetsnackTheme.currentValue.typography val StyleScope.shapes: Shapes get() = LocalJetsnackTheme.currentValue.shapes internal val LocalJetsnackThemeInstance = staticCompositionLocalOf { JetsnackTheme() } @Composable fun JetsnackTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { val colors = if (darkTheme) DarkJetsnackColors else LightJetsnackColors val theme = JetsnackTheme(colors = colors) CompositionLocalProvider( LocalJetsnackTheme provides theme, ) { MaterialTheme( typography = LocalJetsnackTheme.current.typography, shapes = LocalJetsnackTheme.current.shapes, content = content, ) } }
在可组合项中访问
JetsnackStyles:@Composable fun CustomButton(modifier: Modifier, style: Style = Style, text: String) { val interactionSource = remember { MutableInteractionSource() } val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } // Apply style to top level container in combination with incoming style from parameter. Box(modifier = modifier .clickable( interactionSource = interactionSource, indication = null, enabled = true, role = Role.Button, onClick = { }, ) .styleable(styleState, JetsnackTheme.styles.buttonStyle, style)) { Text(text) } }
除了采用全局主题之外,还有其他将 Styles 纳入应用中的替代策略。您可以针对特定通话网站利用 Styles 内嵌,也可以在不需要完整主题功能时使用静态定义。除非整个样式从根本上不同,否则不应有条件地交换 Styles。您应该优先选择在视觉定义内访问动态令牌,而不是在不同的样式对象之间切换。