Sistemi di progettazione personalizzati in Compose

Material è il nostro sistema di progettazione consigliato e Jetpack Compose fornisce un'implementazione di Material, ma non è obbligatorio utilizzarlo. Il Material è basato interamente su API pubbliche, quindi è possibile creare il proprio sistema di progettazione allo stesso modo.

Puoi scegliere tra diversi approcci:

Puoi anche continuare a utilizzare i componenti Material con un sistema di progettazione personalizzato. È possibile farlo, ma ci sono alcuni aspetti da tenere presenti per adempiere all'approccio che hai adottato.

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

Estensione dei temi materiali

Compose Material modella da vicino Material Theming, per semplificare e rendere sicuro il rispetto delle linee guida di Material. Tuttavia, è possibile estendere gli insiemi di colori, tipografia e forma con valori aggiuntivi.

L'approccio più semplice consiste nell'aggiungere proprietà delle estensioni:

// Use with MaterialTheme.colors.snackbarAction
val Colors.snackbarAction: Color
    get() = if (isLight) 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 coerenza con le API di utilizzo di MaterialTheme. Un esempio di ciò definito stesso da Compose è primarySurface, che agisce come proxy tra primary e surface in base a Colors.isLight.

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

Supponiamo che tu voglia aggiungere altri due colori, tertiary e onTertiary, mantenendo i colori Material esistenti:

@Immutable
data class ExtendedColors(
    val tertiary: Color,
    val onTertiary: Color
)

val LocalExtendedColors = staticCompositionLocalOf {
    ExtendedColors(
        tertiary = Color.Unspecified,
        onTertiary = Color.Unspecified
    )
}

@Composable
fun ExtendedTheme(
    /* ... */
    content: @Composable () -> Unit
) {
    val extendedColors = ExtendedColors(
        tertiary = Color(0xFFA8EFF0),
        onTertiary = Color(0xFF002021)
    )
    CompositionLocalProvider(LocalExtendedColors provides extendedColors) {
        MaterialTheme(
            /* colors = ..., typography = ..., shapes = ... */
            content = content
        )
    }
}

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

È simile all'utilizzo di MaterialTheme API. Supporta inoltre più temi, poiché puoi nidificare i ExtendedTheme nello stesso modo di MaterialTheme.

Utilizzo dei componenti Material

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

Se vuoi utilizzare valori estesi nei componenti, aggregali nelle tue funzioni componibili, impostando direttamente i valori da modificare ed esponendo altri come parametri nell'elemento componibile contenitore:

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

Devi quindi sostituire gli utilizzi di Button con ExtendedButton, ove appropriato.

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

Sostituzione dei sistemi di materiali

Invece di estendere i temi materiali, potresti voler sostituire uno o più sistemi (Colors, Typography o Shapes) con un'implementazione personalizzata, mantenendo al contempo gli altri.

Supponiamo di voler sostituire i sistemi di tipo e forma mantenendo il sistema di colori:

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

Utilizzo dei componenti Material

Quando uno o più sistemi di MaterialTheme sono stati sostituiti, l'utilizzo dei componenti Material così come sono, potrebbe comportare valori indesiderati di colore, tipo o forma del materiale.

Se vuoi utilizzare valori sostitutivi nei componenti, aggregali nelle tue funzioni componibili, impostando direttamente i valori per il sistema pertinente ed esponendo altri come parametri nell'elemento componibile contenitore.

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

Devi quindi sostituire gli utilizzi di Button con ReplacementButton, ove appropriato.

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

Implementazione di un sistema di progettazione completamente personalizzato

Potresti voler sostituire i temi Material con un sistema di progettazione completamente personalizzato. Considera che MaterialTheme fornisce i seguenti sistemi:

  • Colors, Typography e Shapes: sistemi di applicazione dei temi Material
  • ContentAlpha: livelli di opacità per dare enfasi in Text e Icon
  • TextSelectionColors: colori utilizzati per la selezione del testo da Text e TextField
  • Ripple e RippleTheme: implementazione sostanziale di Indication

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

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

Nel codice seguente, creiamo un sistema di colori personalizzato che includa gradienti (List<Color>), sistemi di caratteri, introduciamo un nuovo sistema di elevazione ed escludiamo 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
}

Utilizzo dei componenti Material

Quando non è presente MaterialTheme, l'utilizzo dei componenti Material così come sono, comporta valori indesiderati di colore, tipo e forma di Material e comportamenti delle indicazioni.

Se vuoi utilizzare valori personalizzati nei componenti, aggregali nelle tue funzioni componibili, impostando direttamente i valori per il sistema pertinente ed esponendo gli altri come parametri all'elemento componibile contenitore.

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

@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 = ContentAlpha.disabled)
        ),
        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 classe, come List<Color> per rappresentare i gradienti, potrebbe essere preferibile implementare i componenti da zero anziché includerli a capo. Per un esempio, dai un'occhiata a JetsnackButton nell'esempio di Jetsnack.