Sistemi di progettazione personalizzati in Compose

Material è il nostro sistema di progettazione consigliato e Jetpack Compose offre implementazione Material, non sei obbligato a utilizzarlo. Material è basato interamente su API pubbliche, quindi è possibile creare il tuo sistema di design nello stesso modo.

Puoi adottare diversi approcci:

Puoi anche continuare a usare i componenti Material con un design personalizzato di un sistema operativo completo. È possibile farlo, ma ci sono alcuni aspetti da tenere presenti l'approccio adottato.

Per scoprire di più sui costrutti e sulle API di livello inferiore utilizzati da MaterialTheme e dai sistemi di progettazione personalizzati, consulta la guida Anatomia di un tema in Compose.

Estensione del tema Material

Creare modelli Material con la massima attenzione Temi Material per semplificare il rispetto delle linee guida sul materiale e garantire la sicurezza di digitazione. Tuttavia, è possibile estendere i set di colori, tipografia e forme con altre e i relativi valori.

L'approccio più semplice è aggiungere proprietà di estensione:

// 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)

Ciò garantisce la coerenza con le API di utilizzo di MaterialTheme. Un esempio di questo valore definito da Compose stesso è surfaceColorAtElevation, che determina il colore della superficie da utilizzare in base all'elevazione.

Un altro approccio è definire un tema esteso che "avvolge" MaterialTheme e i suoi valori.

Supponiamo di voler aggiungere altri due colori: caution e onCaution, un colore giallo utilizzato per azioni semipericolose, pur mantenendo il colori Material esistenti:

@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
}

Questo è simile alle API di utilizzo di MaterialTheme. Supporta anche più temi puoi nidificare i ExtendedTheme nello stesso modo dei MaterialTheme.

Utilizzare i componenti Material

Quando estendi i temi Material, i valori MaterialTheme esistenti vengono mantenuti e i componenti Material hanno ancora valori predefiniti ragionevoli.

Se vuoi utilizzare valori estesi nei componenti, aggregali funzioni componibili, impostando direttamente i valori da modificare e l'esposizione di altri come parametri all'elemento componibile contenitore:

@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
    )
}

Devi quindi sostituire gli utilizzi di Button con ExtendedButton in cui appropriato.

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

Sostituire i sottosistemi di Material

Anziché estendere Material Theming, ti consigliamo di sostituire uno o più sistemi (Colors, Typography o Shapes) con un'implementazione personalizzata, mantenendo gli altri.

Supponiamo che tu voglia sostituire i sistemi di tipo e forma mantenendo il sistema di colore:

@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
}

Utilizzare i componenti Material

Quando uno o più sistemi di MaterialTheme sono stati sostituiti, utilizzando Material componenti così come sono potrebbero causare valori indesiderati per il colore, il tipo o la forma del materiale.

Se vuoi utilizzare valori di sostituzione nei componenti, inseriscili nelle tue funzioni composable, impostando direttamente i valori per il sistema pertinente ed esponendo gli altri come parametri al composable contenente.

@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()
            }
        }
    )
}

Dovresti quindi sostituire gli utilizzi di Button con ReplacementButton ove opportuno.

@Composable
fun ReplacementApp() {
    ReplacementTheme {
        /*...*/
        ReplacementButton(onClick = { /* ... */ }) {
            /* ... */
        }
    }
}

Implementare un sistema di design completamente personalizzato

Ti consigliamo di sostituire Material Theming con un sistema di design completamente personalizzato. Tieni presente che MaterialTheme fornisce i seguenti sistemi:

  • Colors, Typography e Shapes: sistemi di temi di Material
  • TextSelectionColors: colori utilizzati per la selezione del testo tramite Text e TextField
  • Ripple e RippleTheme: implementazione materiale di Indication

Se vuoi continuare a utilizzare i componenti Material, dovrai sostituire alcuni di questi sistemi nel tuo tema o temi personalizzati oppure gestire i sistemi nel tuo componenti, per evitare comportamenti indesiderati.

Tuttavia, i sistemi di progettazione non si limitano ai concetti su cui si basa Material. Tu modificare i sistemi esistenti e introdurne di nuovi, con nuove classi e tipi diversi, per rendere altri concetti compatibili con i temi.

Nel codice seguente, modelliamo un sistema di colori personalizzato che include gradienti (List<Color>), un sistema di tipi, un nuovo sistema di elevazione e esclude altri sistemi forniti da 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
}

Utilizzare i componenti Material

Se non è presente MaterialTheme, l'utilizzo dei componenti Material così come sono comporterà valori e comportamenti indesiderati per colore, tipo e forma del materiale.

Per utilizzare valori personalizzati nei componenti, includili in un di funzioni, impostando direttamente i valori per il sistema pertinente ed esponendo altri come parametri per il componibile contenitore.

Ti consigliamo di accedere ai valori impostati dal tema personalizzato. In alternativa, se il tema non fornisce Color, TextStyle, Shape o altri sistemi, puoi codificarli come hardcoded.

@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)

Se hai introdotto nuovi tipi di classi, ad esempio List<Color> per rappresentare i gradienti, potrebbe essere meglio implementare i componenti da zero anziché incapsularli. Ad esempio, dai un'occhiata a JetsnackButton dell'esempio di Jetsnack.