使用样式设置主题

您可以通过多种方式使用样式来构建应用。您选择哪种方式取决于您的应用在采用 Material Design 方面处于哪个阶段:

  1. 完全自定义的设计系统,不使用 Material Design
    • 建议:定义从主题中提取值的组件样式,并在设计系统组件上公开样式参数。
  2. 使用 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) { ... }
图表:显示了使用样式的主题设置,并引入了新图层
图 1. 一个组件示例以及该组件如何从主题访问样式。

原子样式与单体式样式

借助 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 逻辑的封装容器或自定义 BoxLayout 实现)直接使用这些样式,并为设计系统的使用者公开 Style 参数。
  • 样式层:样式是设计系统的主要定义。令牌是输入到这些样式的命名变量。这样可以实现深度自定义,例如为状态变化定义独特的动画(例如,在按压时为缩放和颜色添加动画效果)。

如果您在不使用 Material 的情况下构建自己的自定义主题背景,并且想要采用样式,请将样式列表添加到您的主题背景中。这样,您就可以从项目中的任何位置访问基本样式。

  1. 创建一个 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)
        }
    }

  2. 提供 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,
            )
        }
    }

  3. 在可组合项中访问 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。您应该优先选择在视觉定义内访问动态令牌,而不是在不同的样式对象之间切换。