您可以通过多种方式使用样式来构建应用。具体选择哪种方式取决于您的应用在采用 Material Design 方面所处的阶段:
- 完全自定义的设计系统,不使用 Material Design
- 建议:定义使用 主题值的组件样式,并在设计系统组件上公开样式参数。
- 使用 Material Design
- 建议:等待 Material 采用与样式集成。 尽可能在自己的组件上使用样式。
样式层
在传统的 Compose 模型中,自定义通常严重依赖于替换 MaterialTheme 提供的全局令牌(颜色和排版),或者尽可能封装和替换设计系统可组合项的属性。
有时,Material 层中存在一些属性,这些属性不会通过子系统或参数公开,而是组件本身的硬编码默认值。
借助 Styles API,子系统和组件之间新增了一个抽象层:样式 。
| 层 | 责任 | 示例 |
|---|---|---|
| 子系统值 | 命名值 | val Primary = Color(0xFF34A85E) |
| 原子样式 | 仅更改一个属性的样式 | val largeSizeAtomic = Style { size(100.dp, 40.dp) } |
| 组件样式 | 组件专用配置 | 具有 Primary 背景和 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”进行组合
新的 Styles 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。您应该首选在视觉定义中访问动态令牌,而不是在不同的样式对象之间切换。