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, interruttori e così via) sono basati su Material Theming, un modo sistematico per personalizzare Material Design in modo da riflettere meglio il brand del tuo prodotto. Un tema Material contiene gli attributi colore, tipografia e forma. Quando personalizzi questi attributi, le modifiche vengono automaticamente riportate nei componenti che utilizzi per creare l'app.

Jetpack Compose implementa questi concetti con il composable MaterialTheme:

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

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

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

Colore

I colori vengono modellati in Compose con la classe Color, una classe di contenimento dei dati.

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

Anche se puoi organizzarli come preferisci (come costanti di primo livello, all'interno di un singleton o definiti inline), 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 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
}

Utilizzare i colori del tema

Puoi recuperare Colors fornito al componente componibile 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, puoi non solo impostare il colore di un componente componibile,ma anche fornire un colore predefinito per i contenuti e i componenti componibili contenuti al suo interno. Molti composable utilizzano questo colore dei contenuti per impostazione predefinita. Ad esempio, Text basa il suo colore su quello dei contenuti del relativo elemento principale e Icon utilizza questo colore per impostare la sua tonalità.

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

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 diverso da quello del tema, devi specificare anche un colore appropriato per i contenuti. Utilizza LocalContentColor per recuperare il colore dei contenuti preferito per lo sfondo corrente, in una determinata posizione della gerarchia.

Alpha dei contenuti

Spesso vuoi variare l'enfasi sui contenuti per comunicarne l'importanza e fornire una gerarchia visiva. I consigli di 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 alpha dei contenuti per una gerarchia fornendo un valore per questo CompositionLocal. I composable 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. Materiale specifica alcuni valori alfa standard (high, medium, disabled) 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, pertanto utilizza ContentAlpha.high. La seconda riga contiene metadati meno importanti e utilizza quindi 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 è racchiuso 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 le Colors attuali sono chiare o scure:

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 elevazione

In Material, le superfici dei temi scuri con elevazioni più alte ricevono overlay di elevazione, che schiariscono lo sfondo. Più alta è l'elevazione di una superficie (avvicinandola a una sorgente luminosa implicita), più chiara diventa la superficie.

Il composable Surface applica automaticamente queste sovrapposizioni quando si utilizzano colori scuri, così come qualsiasi altro composable 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 diversi livelli di elevazione
Figura 5. Le schede e la navigazione in basso utilizzano il colore surface come sfondo. Poiché le schede e la 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 navigazione in basso è più chiara delle schede.

Per gli scenari personalizzati che non prevedono 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 le sovrapposizioni 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'utilizzo del colore surface rispetto al colore primary nella maggior parte dei casi. I composable Material come TopAppBar e BottomNavigation implementano questo comportamento per impostazione predefinita.

Screenshot di un tema scuro Material che mostra la barra delle app superiore 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 delle app superiore 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 in vari stili
Figura 7. Il sistema di tipo di materiale.

Compose implementa il sistema di tipi con le classi Typography, TextStyle e relative ai caratteri. Il costruttore Typography offre valori predefiniti per ogni stile, quindi puoi 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 tipo di carattere ovunque, specifica il parametro defaultFontFamily e ometti fontFamily di qualsiasi elemento TextStyle:

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

Utilizzare gli stili di testo

Agli elementi TextStyle si accede utilizzando MaterialTheme.typography. Recupera gli elementi TextStyle nel seguente modo:

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

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

Forma

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

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

Compose implementa il sistema di forme con la classe Shapes, che consente di specificare una 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 è impostato per impostazione predefinita su medium e ModalDrawer è impostato per impostazione predefinita su large. Consulta il riferimento allo schema delle forme per la mappatura completa.

Utilizzare le forme

Agli elementi Shape si accede utilizzando MaterialTheme.shapes. Recupera gli elementi Shape con un codice simile a questo:

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

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

Stili predefiniti

In Compose non esiste un concetto equivalente di stili predefiniti delle View di Android. Puoi fornire funzionalità simili creando overload funzioni componibili che racchiudono i componenti Material. Ad esempio, per creare uno stile di pulsante, racchiudi un pulsante nella tua funzione componibile, impostando direttamente i parametri che vuoi o devi modificare ed esponendo gli altri come parametri al componente 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 delle sovrapposizioni di temi dalle visualizzazioni di Android in Compose nidificando i composable MaterialTheme. Poiché MaterialTheme imposta i colori, la tipografia e le forme sul valore del tema corrente, tutti gli altri parametri mantengono i valori predefiniti quando un tema imposta solo uno di questi parametri.

Inoltre, quando esegui la migrazione delle schermate basate su View a Compose, fai attenzione agli utilizzi dell'attributo android:theme. Probabilmente ti serve 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 dello schermo e poi un BlueTheme per la sezione correlata. Lo screenshot e il codice riportati di seguito 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 (cliccare, attivare/disattivare e così via) possono trovarsi in stati visivi diversi. Gli stati includono abilitato, disabilitato, premuto e così via.

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

Screenshot di due pulsanti: uno attivato, uno disattivato, 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 colore ed elevazione. Se devi configurare i valori utilizzati in stati diversi, sono disponibili classi e funzioni di utilità. 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 ed elevazione modificati per gli stati abilitato e disabilitato
Figura 13. Pulsante con enabled = true (a sinistra) e enabled = false (a destra), con valori di colore ed elevazione regolati.

Increspature

I componenti Material utilizzano gli effetti ripple per indicare che si sta interagendo con loro. Se utilizzi MaterialTheme nella gerarchia, Ripple viene utilizzato come Indication predefinito all'interno di modificatori come clickable e indication.

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

Puoi estendere RippleTheme e utilizzare le funzioni di utilità defaultRippleColor e defaultRippleAlpha. Puoi quindi fornire il tema 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 ripple quando vengono toccati
Figura 14. Pulsanti con valori di increspatura diversi forniti utilizzando RippleTheme.

Scopri di più

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

Codelab

Video