Material Design 2 in Compose

Jetpack Compose offre un'implementazione di Material Design, un sistema di progettazione completo per la creazione di interfacce digitali. I componenti di Material Design (pulsanti, schede, opzioni e così via) sono basati sui temi di Material, un modo sistematico per personalizzare Material Design in modo da riflettere meglio il brand del tuo prodotto. Un tema di Material contiene attributi di colore, tipografia e forma. Quando personalizzi questi attributi, le modifiche vengono applicate automaticamente ai componenti che utilizzi per creare l'app.

Jetpack Compose implementa questi concetti con il componibile MaterialTheme:

MaterialTheme(
    colors = // ...
    typography = // ...
    shapes = // ...
) {
    // app content
}

Configura i parametri che passi a MaterialTheme per applicare un tema alla tua applicazione.

Due screenshot a contrasto. La prima utilizza lo stile MaterialTheme predefinito, mentre il secondo screenshot utilizza uno stile modificato.
Figura 1. Il primo screenshot mostra un'app che non configura `MaterialTheme` e quindi utilizza lo stile predefinito. Il secondo screenshot mostra un'app che passa i parametri a "MaterialTheme" per personalizzare lo stile.

Colore

I colori sono modellati in Compose con la classe Color, una classe di conservazione dei dati.

val Red = Color(0xffff0000)
val Blue = Color(red = 0f, green = 0f, blue = 1f)

Sebbene tu possa organizzarli come preferisci (come costanti di primo livello, all'interno di un singleton o definiti in linea), ti consigliamo vivamente di specificare i colori nel tema e di recuperarli da lì. Questo approccio consente di supportare il tema scuro e i temi nidificati.

Esempio di tavolozza dei colori del tema
Figura 2. Il sistema di colori Material.

Compose fornisce la classe Colors per modellare il sistema di colori di Material. Colors fornisce funzioni di creazione per creare set di colori chiari o scuri:

private val Yellow200 = Color(0xffffeb46)
private val Blue200 = Color(0xff91a4fc)
// ...

private val DarkColors = darkColors(
    primary = Yellow200,
    secondary = Blue200,
    // ...
)
private val LightColors = lightColors(
    primary = Yellow500,
    primaryVariant = Yellow400,
    secondary = Blue700,
    // ...
)

Dopo aver definito i Colors, puoi passarli a un MaterialTheme:

MaterialTheme(
    colors = if (darkTheme) DarkColors else LightColors
) {
    // app content
}

Usa i colori del tema

Puoi recuperare il Colors fornito all'elemento composable MaterialTheme utilizzando MaterialTheme.colors.

Text(
    text = "Hello theming",
    color = MaterialTheme.colors.primary
)

Colore della superficie e dei contenuti

Molti componenti accettano una coppia di colori e colori dei contenuti:

Surface(
    color = MaterialTheme.colors.surface,
    contentColor = contentColorFor(color),
    // ...
) { /* ... */ }

TopAppBar(
    backgroundColor = MaterialTheme.colors.primarySurface,
    contentColor = contentColorFor(backgroundColor),
    // ...
) { /* ... */ }

In questo modo, non solo puoi impostare il colore di un composable, ma anche fornire un colore predefinito per i contenuti, ovvero i composable contenuti al suo interno. Molti composabili utilizzano questo colore dei contenuti per impostazione predefinita. Ad esempio, Text basa il suo colore sul colore dei contenuti del componente principale e Icon utilizza quel colore per impostare la sua tinta.

Due esempi dello stesso banner, con colori diversi
Figura 3. L'impostazione di colori di sfondo diversi produce colori di testo e icone diversi.

Il metodo contentColorFor() recupera il colore "on" appropriato per qualsiasi colore del tema. Ad esempio, se imposti un colore di sfondo primary su Surface, utilizza questa funzione per impostare onPrimary come colore dei contenuti. Se imposti un colore di sfondo non del tema, devi anche specificare un colore dei contenuti appropriato. Utilizza LocalContentColor per recuperare il colore dei contenuti preferito per lo sfondo corrente, in una determinata posizione della gerarchia.

Alpha dei contenuti

Spesso è necessario variare l'enfasi dei contenuti per comunicare l'importanza e fornire una gerarchia visiva. I consigli sulla leggibilità del testo di Material Design suggeriscono di utilizzare diversi livelli di opacità per comunicare diversi livelli di importanza.

Jetpack Compose implementa questa funzionalità utilizzando LocalContentAlpha. Puoi specificare un valore alfa dei contenuti per una gerarchia fornendo un valore per questo CompositionLocal. I componibili nidificati possono utilizzare questo valore per applicare il trattamento alfa ai propri contenuti. Ad esempio, Text e Icon per impostazione predefinita utilizzano la combinazione di LocalContentColor modificata per utilizzare LocalContentAlpha. Material specifica alcuni valori alfa standard (high, medium, disabled) che vengono modellati dall'oggetto ContentAlpha.

// By default, both Icon & Text use the combination of LocalContentColor &
// LocalContentAlpha. De-emphasize content by setting content alpha
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
    Text(
        // ...
    )
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
    Icon(
        // ...
    )
    Text(
        // ...
    )
}

Per scoprire di più su CompositionLocal, consulta Dati con ambito locale con CompositionLocal.

Screenshot del titolo di un articolo che mostra diversi livelli di enfasi del testo
Figura 4. Applica diversi livelli di enfasi al testo per comunicare visivamente la gerarchia delle informazioni. La prima riga di testo è il titolo e contiene le informazioni più importanti, quindi utilizza ContentAlpha.high. La seconda riga contiene metadati meno importanti, quindi utilizza ContentAlpha.medium.

Tema scuro

In Compose, implementi i temi chiaro e scuro fornendo diversi set di Colors al composable MaterialTheme:

@Composable
fun MyTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    MaterialTheme(
        colors = if (darkTheme) DarkColors else LightColors,
        /*...*/
        content = content
    )
}

In questo esempio, MaterialTheme è aggregato nella propria funzione componibile, che accetta un parametro che specifica se utilizzare o meno un tema scuro. In questo caso, la funzione ottiene il valore predefinito per darkTheme eseguendo una query sull'impostazione del tema del dispositivo.

Puoi utilizzare un codice come questo per verificare se i Colors correnti sono chiari o scuri:

val isLightTheme = MaterialTheme.colors.isLight
Icon(
    painterResource(
        id = if (isLightTheme) {
            R.drawable.ic_sun_24
        } else {
            R.drawable.ic_moon_24
        }
    ),
    contentDescription = "Theme"
)

Overlay di altitudine

In Material, le superfici nei temi scuri con elevazioni più elevate ricevono overlay di elevazione, che ne schiariscono lo sfondo. Più alta è l'elevazione di una superficie (che la avvicina a una fonte di luce implicita), più chiara diventa la superficie.

Il componibile Surface applica automaticamente questi overlay quando si utilizzano colori scuri, così come qualsiasi altro componibile Material che utilizza una superficie:

Surface(
    elevation = 2.dp,
    color = MaterialTheme.colors.surface, // color will be adjusted for elevation
    /*...*/
) { /*...*/ }

Screenshot di un'app che mostra i colori leggermente diversi utilizzati per gli elementi a livelli di elevazione diversi
Figura 5. Le schede e la barra di navigazione in basso utilizzano entrambe il colore surface come sfondo. Poiché le schede e la barra di navigazione in basso si trovano a livelli di elevazione diversi rispetto allo sfondo, hanno colori leggermente diversi: le schede sono più chiare dello sfondo e la barra di navigazione in basso è più chiara delle schede.

Per gli scenari personalizzati che non coinvolgono un Surface, utilizza LocalElevationOverlay, un CompositionLocal contenente ElevationOverlay utilizzato dai componenti Surface:

// Elevation overlays
// Implemented in Surface (and any components that use it)
val color = MaterialTheme.colors.surface
val elevation = 4.dp
val overlaidColor = LocalElevationOverlay.current?.apply(
    color, elevation
)

Per disattivare gli overlay di elevazione, fornisci null nel punto scelto di una gerarchia componibile:

MyTheme {
    CompositionLocalProvider(LocalElevationOverlay provides null) {
        // Content without elevation overlays
    }
}

Accenti di colore limitati

Material consiglia di applicare accenti di colore limitati per i temi scuri, preferendo l'uso del colore surface rispetto al colore primary nella maggior parte dei casi. I componibili Material come TopAppBar e BottomNavigation implementano questo comportamento per impostazione predefinita.

Screenshot di un tema scuro di Material, che mostra la barra delle app in alto che utilizza il colore della superficie anziché il colore principale per accenti di colore limitati
Figura 6. Tema scuro Material con accenti di colore limitati. La barra dell'app in alto utilizza il colore principale nel tema chiaro e il colore della superficie nel tema scuro.

Per gli scenari personalizzati, utilizza la proprietà di estensione primarySurface:

Surface(
    // Switches between primary in light theme and surface in dark theme
    color = MaterialTheme.colors.primarySurface,
    /*...*/
) { /*...*/ }

Tipografia

Material definisce un sistema di tipi, incoraggiandoti a utilizzare un numero ridotto di stili con nomi semantici.

Esempio di diversi caratteri tipografici in vari stili
Figura 7. Il sistema di caratteri di Material.

Compose implementa il sistema di tipi con le classi Typography, TextStyle e relative ai caratteri. Il costruttore Typography offre valori predefiniti per ogni stile, in modo da poter omettere quelli che non vuoi personalizzare:

val raleway = FontFamily(
    Font(R.font.raleway_regular),
    Font(R.font.raleway_medium, FontWeight.W500),
    Font(R.font.raleway_semibold, FontWeight.SemiBold)
)

val myTypography = Typography(
    h1 = TextStyle(
        fontFamily = raleway,
        fontWeight = FontWeight.W300,
        fontSize = 96.sp
    ),
    body1 = TextStyle(
        fontFamily = raleway,
        fontWeight = FontWeight.W600,
        fontSize = 16.sp
    )
    /*...*/
)
MaterialTheme(typography = myTypography, /*...*/) {
    /*...*/
}

Se vuoi utilizzare lo stesso carattere tipografico ovunque, specifica il parametro defaultFontFamily e ometti fontFamily di tutti gli elementi TextStyle:

val typography = Typography(defaultFontFamily = raleway)
MaterialTheme(typography = typography, /*...*/) {
    /*...*/
}

Utilizza gli stili di testo

Gli elementi TextStyle sono accessibili tramite MaterialTheme.typography. Recupera gli elementi TextStyle come segue:

Text(
    text = "Subtitle2 styled",
    style = MaterialTheme.typography.subtitle2
)

Screenshot che mostra un mix di caratteri tipografici diversi per scopi diversi
Figura 8. Utilizza una selezione di caratteri tipografici e stili per esprimere il tuo brand.

Forma

Material definisce un sistema di forme che ti consente di definire le forme per i componenti grandi, medi e piccoli.

Mostra una varietà di forme di Material Design
Figura 9. Il sistema di forme di Material.

Compose implementa il sistema di forme con la classe Shapes, che ti consente di specificare un CornerBasedShape per ogni categoria di dimensioni:

val shapes = Shapes(
    small = RoundedCornerShape(percent = 50),
    medium = RoundedCornerShape(0f),
    large = CutCornerShape(
        topStart = 16.dp,
        topEnd = 0.dp,
        bottomEnd = 0.dp,
        bottomStart = 16.dp
    )
)

MaterialTheme(shapes = shapes, /*...*/) {
    /*...*/
}

Molti componenti utilizzano queste forme per impostazione predefinita. Ad esempio, Button, TextField e FloatingActionButton sono impostati per impostazione predefinita su small, AlertDialog su medium e ModalDrawer su large. Per la mappatura completa, consulta il riferimento allo schema delle forme.

Utilizza le forme

Gli elementi Shape sono accessibili tramite MaterialTheme.shapes. Recupera gli elementi Shape con un codice simile al seguente:

Surface(
    shape = MaterialTheme.shapes.medium, /*...*/
) {
    /*...*/
}

Screenshot di un'app che utilizza le forme di Material per indicare lo stato di un elemento
Figura 10. Usa le forme per esprimere il brand o lo stato.

Stili predefiniti

In Compose non esiste un concetto equivalente agli stili predefiniti delle visualizzazioni Android. Puoi fornire una funzionalità simile creando le tue funzioni componibili overload che aggregano i componenti Material. Ad esempio, per creare uno stile di pulsante, inserisci un pulsante nella tua funzione componibile, impostando direttamente i parametri che vuoi o devi modificare ed esponendo gli altri come parametri al componibile contenitore.

@Composable
fun MyButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            backgroundColor = MaterialTheme.colors.secondary
        ),
        onClick = onClick,
        modifier = modifier,
        content = content
    )
}

Overlay dei temi

Puoi ottenere l'equivalente degli overlay dei temi dalle visualizzazioni Android in Compose nidificando i componibili MaterialTheme. Poiché MaterialTheme imposta i colori, la tipografia e le forme sui valori del tema corrente, tutti gli altri parametri mantengono i valori predefiniti quando un tema imposta solo uno di questi parametri.

Inoltre, durante la migrazione delle schermate basate su View a Compose, fai attenzione agli utilizzi dell'attributo android:theme. È probabile che tu abbia bisogno di un nuovo MaterialTheme in quella parte dell'albero della UI di Compose.

In questo esempio, la schermata dei dettagli utilizza un PinkTheme per la maggior parte della schermata e un BlueTheme per la sezione correlata. Lo screenshot e il codice seguenti illustrano questo concetto:

Screenshot di un'app che mostra temi nidificati, con un tema rosa per la schermata principale e un tema blu per una sezione correlata
Figura 11. Temi nidificati.

@Composable
fun DetailsScreen(/* ... */) {
    PinkTheme {
        // other content
        RelatedSection()
    }
}

@Composable
fun RelatedSection(/* ... */) {
    BlueTheme {
        // content
    }
}

Stati dei componenti

I componenti Material con cui è possibile interagire (clic, attivazione/disattivazione e così via) possono trovarsi in stati visivi diversi. Gli stati includono attivato, disattivato, premuto e così via.

I componibili spesso hanno un parametro enabled. Se lo imposti su false, l'interazione viene impedita e vengono modificate proprietà come il colore e l'elevazione per comunicare visivamente lo stato del componente.

Screenshot di due pulsanti: uno abilitato, uno disabilitato, che mostrano i loro diversi stati visivi
Figura 12. Pulsante con enabled = true (sinistra) e enabled = false (destra).

Nella maggior parte dei casi, puoi fare affidamento sui valori predefiniti per elementi come colore e rilievo. Se devi configurare i valori utilizzati in stati diversi, sono disponibili classi e funzioni di convenienza. Considera il seguente esempio di pulsante:

Button(
    onClick = { /* ... */ },
    enabled = true,
    // Custom colors for different states
    colors = ButtonDefaults.buttonColors(
        backgroundColor = MaterialTheme.colors.secondary,
        disabledBackgroundColor = MaterialTheme.colors.onBackground
            .copy(alpha = 0.2f)
            .compositeOver(MaterialTheme.colors.background)
        // Also contentColor and disabledContentColor
    ),
    // Custom elevation for different states
    elevation = ButtonDefaults.elevation(
        defaultElevation = 8.dp,
        disabledElevation = 2.dp,
        // Also pressedElevation
    )
) { /* ... */ }

Screenshot di due pulsanti con colore e rilievo regolati per gli stati abilitato e disabilitato
Figura 13. Pulsante con enabled = true (a sinistra) e enabled = false (a destra), con valori di colore e altezza modificati.

Increspature

I componenti Material utilizzano gli effetti ripple per indicare che sono in corso interazioni. Se utilizzi MaterialTheme nella tua gerarchia, viene utilizzato un Ripple come Indication predefinito all'interno di modificatori come clickable e indication.

Nella maggior parte dei casi puoi fare affidamento sul valore predefinito Ripple. Se devi configurare il loro aspetto, puoi utilizzare RippleTheme per modificare proprietà come il colore e l'alpha.

Puoi estendere RippleTheme e utilizzare le funzioni di utilità defaultRippleColor e defaultRippleAlpha. Puoi quindi fornire il tuo tema di ripple personalizzato nella gerarchia utilizzando LocalRippleTheme:

@Composable
fun MyApp() {
    MaterialTheme {
        CompositionLocalProvider(
            LocalRippleTheme provides SecondaryRippleTheme
        ) {
            // App content
        }
    }
}

@Immutable
private object SecondaryRippleTheme : RippleTheme {
    @Composable
    override fun defaultColor() = RippleTheme.defaultRippleColor(
        contentColor = MaterialTheme.colors.secondary,
        lightTheme = MaterialTheme.colors.isLight
    )

    @Composable
    override fun rippleAlpha() = RippleTheme.defaultRippleAlpha(
        contentColor = MaterialTheme.colors.secondary,
        lightTheme = MaterialTheme.colors.isLight
    )
}

GIF animata che mostra pulsanti con diversi effetti di ripple quando vengono toccati
Figura 14. Pulsanti con valori di ripple diversi forniti utilizzando RippleConfiguration.

Scopri di più

Per scoprire di più sui temi di Material in Compose, consulta le seguenti risorse aggiuntive.

Codelab

Video