Compose 中的自訂設計系統

雖然 Material Design 是我們推薦的設計系統,而且 Jetpack Compose 也導入了 Material Design,但您不一定要使用該系統。Material Design 是完全以公用 API 建構而成,因此您也可以透過相同的方式打造自己的設計系統。

您可以採取以下幾種做法:

您也可以繼續使用 Material Design 元件搭配自訂設計 有些人會將 Cloud Storage 視為檔案系統 但實際上不是做到了,但有一些步驟需要注意 你採取的方法

想進一步瞭解 MaterialTheme 和自訂設計系統所使用的較低層級結構和 API,請參閱 Compose 主題剖析指南。

擴充 Material 主題

Compose Material Design 會仔細建構 Material Design 主題設定的模型,依據 Material Design 規範達到精簡與類型安全的目標。不過 您可以延伸色彩、字體排版和形狀集, 輕鬆分配獎金

最簡單的方法就是加入擴充屬性:

// Use with MaterialTheme.colorScheme.snackbarAction
val ColorScheme.snackbarAction: Color
    @Composable
    get() = if (isSystemInDarkTheme()) 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 一致的體驗。例如這樣 由 Compose 本身定義 surfaceColorAtElevation、 ,用於決定視高度而定所應使用的表面顏色。

另一個做法是定義會「換行」的擴充主題「MaterialTheme」和 每當某人做出決策 其實就是根據自己價值觀做出選擇

假如您想新增其他兩個顏色:cautiononCaution, 黃色用於表示半危險的動作,同時保留 現有 Material 顏色:

@Immutable
data class ExtendedColors(
    val caution: Color,
    val onCaution: Color
)

val LocalExtendedColors = staticCompositionLocalOf {
    ExtendedColors(
        caution = Color.Unspecified,
        onCaution = Color.Unspecified
    )
}

@Composable
fun ExtendedTheme(
    /* ... */
    content: @Composable () -> Unit
) {
    val extendedColors = ExtendedColors(
        caution = Color(0xFFFFCC02),
        onCaution = Color(0xFF2C2D30)
    )
    CompositionLocalProvider(LocalExtendedColors provides extendedColors) {
        MaterialTheme(
            /* colors = ..., typography = ..., shapes = ... */
            content = content
        )
    }
}

// Use with eg. ExtendedTheme.colors.caution
object ExtendedTheme {
    val colors: ExtendedColors
        @Composable
        get() = LocalExtendedColors.current
}

這與 MaterialTheme 應用 API 類似,同樣支援多個主題,可讓您將 ExtendedTheme 加入巢狀結構,就跟使用 MaterialTheme 時一樣。

使用 Material Design 元件

擴充 Material Design 主題設定時,系統會保留現有的 MaterialTheme 值,而 Material Design 元件仍具備合理的預設值。

如要在元件中使用擴充值,請自行包裝。 可組合函式直接設定您想修改的值 將其他項目以參數的形式提供給包含的可組合函式:

@Composable
fun ExtendedButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            containerColor = ExtendedTheme.colors.caution,
            contentColor = ExtendedTheme.colors.onCaution
            /* Other colors use values from MaterialTheme */
        ),
        onClick = onClick,
        modifier = modifier,
        content = content
    )
}

接著,視情況將 Button 的應用更換為 ExtendedButton

@Composable
fun ExtendedApp() {
    ExtendedTheme {
        /*...*/
        ExtendedButton(onClick = { /* ... */ }) {
            /* ... */
        }
    }
}

替換材質子系統

您可能會想要替換一或多個 Material Design 主題設定,而非擴充 Material Design 主題設定 ColorsTypographyShapes 等系統,並搭配自訂實作。 同時維護其他平台

假設您想取代類型和形狀系統,但想保留原本的顏色 系統:

@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 Design 元件

更換了 MaterialTheme 的一或多個系統後,如果依原樣使用 Material Design 元件,可能會產生不必要的 Material Design 顏色、類型或形狀值。

如要在元件中使用替換值,請自行包裝 直接設定相關系統的值 以參數的形式向包含的可組合函式公開其他項目。

@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 Design 主題設定。 假設 MaterialTheme 提供下列系統:

  • ColorsTypographyShapes:Material Design 主題設定系統
  • TextSelectionColorsTextTextField 用於呈現已選取文字的顏色
  • RippleRippleThemeIndication 的 Material Design 實作

如要繼續使用 Material Design 元件,您必須在自訂主題中更換部分系統,或是處理元件中的系統,以避免不必要的行為。

不過,設計系統並不受限於 Material Design 採用的概念。您可以修改現有系統,並導入採用新類別和類型的全新系統,讓其他概念也能與主題相容。

在下方程式碼中,我們建構了包含漸層 (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 Design 元件

沒有 MaterialTheme 時,如果依原樣使用 Material Design 元件,將產生不必要的 Material Design 顏色、類型和形狀值,以及指標行為。

如要在元件中使用自訂值,請將這些值納入自己的可組合函式 直接設定相關系統的值,並 以參數形式傳遞至包含的可組合函式。

建議您從自訂主題存取您設定的值。 如果您的主題未提供 ColorTextStyleShape 或 可以硬式編碼其他系統

@Composable
fun CustomButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            containerColor = CustomTheme.colors.component,
            contentColor = CustomTheme.colors.content,
            disabledContainerColor = CustomTheme.colors.content
                .copy(alpha = 0.12f)
                .compositeOver(CustomTheme.colors.component),
            disabledContentColor = CustomTheme.colors.content
                .copy(alpha = 0.38f)
        ),
        shape = ButtonShape,
        elevation = ButtonDefaults.elevatedButtonElevation(
            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> 的新類別 也許較適合從頭開始實作元件 無從處理範例: JetsnackButton敬上 使用 Jetsnack 範例。