使用樣式設定主題

您可以使用樣式,透過多種方式建構應用程式。選擇哪種做法取決於應用程式採用 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
元件樣式 元件專屬設定 按鈕,背景為 Primary,邊框間距為 16dp。val buttonStyle = Style { contentPadding(16.dp) shape(RoundedCornerShape(8.dp)) background(Color.Blue) }
元件 使用樣式的實用 UI 元素。 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」的組合

新版 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 邏輯的包裝函式,或自訂 BoxLayout 實作項目) 會直接使用這些樣式,並為設計系統的消費者公開 Style 參數。
  • 樣式層:樣式是設計系統的主要定義。權杖是輸入這些樣式的命名變數。這可實現深度自訂,例如為狀態變化定義專屬動畫 (例如在按下時動畫化比例和顏色)。

如果您未使用 Material Design 建立自訂主題,但想採用樣式,請將樣式清單新增至主題。這樣一來,您就能在專案的任何位置存取基本樣式。

  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。您應該優先在視覺定義中存取動態符記,而不是在不同的樣式物件之間切換。