Material Design 2 dans Compose

Jetpack Compose propose une implémentation de Material Design, un système de conception complet permettant de créer des interfaces numériques. Les composants Material Design (boutons, cartes, interrupteurs, etc.) sont basés sur la thématisation Material, un moyen systématique de personnaliser Material Design pour mieux refléter la marque de votre produit. Un thème Material inclut les attributs couleur, typographie et forme. Lorsque vous personnalisez ces attributs, vos modifications sont automatiquement répercutées dans les composants que vous utilisez pour créer votre application.

Jetpack Compose implémente ces concepts avec le composable MaterialTheme :

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

Configurez les paramètres que vous transmettez à MaterialTheme pour appliquer un thème à votre application.

Deux captures d'écran aux contrastes différents. La première utilise le style MaterialTheme par défaut, et la seconde un style modifié.

Figure 1. La première capture d'écran montre une application qui ne configure pas MaterialTheme. Elle utilise donc le style par défaut. La deuxième capture d'écran montre une application qui transmet des paramètres à MaterialTheme pour personnaliser le style.

Couleur

Les couleurs sont modélisées dans Compose avec la classe Color, qui est une classe simple de stockage de données.

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

Bien que vous puissiez les organiser comme vous le souhaitez (en tant que constantes de premier niveau, dans un singleton ou définies de manière intégrée), nous vous recommandons vivement de spécifier des couleurs dans votre thème et de les récupérer à partir de cet endroit. Cela facilite la prise en charge des thèmes sombres et des thèmes imbriqués.

Exemple de palette de couleurs de thème

Figure 2. Système de couleurs Material.

Compose fournit la classe Colors pour modéliser le système de couleurs Material. Colors fournit des fonctions de compilateur pour créer des ensembles de couleurs claires ou sombres :

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

Après avoir défini vos Colors, vous pouvez les transmettre à un MaterialTheme :

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

Utiliser les couleurs du thème

Vous pouvez récupérer les Colors fournies au composable MaterialTheme à l'aide de MaterialTheme.colors.

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

Couleur de la surface et du contenu

De nombreux composants acceptent une paire de couleurs et des couleurs de contenu :

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

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

Cela vous permet non seulement de définir la couleur d'un composable, mais aussi d'utiliser une couleur par défaut pour le contenu, c'est-à-dire les composables qu'il contient. De nombreux composables utilisent cette couleur de contenu par défaut. Par exemple, Text base sa couleur sur celle du contenu de son parent, et Icon utilise cette couleur pour définir sa teinte.

Deux exemples de la même bannière, avec des couleurs différentes

Figure 3. Définir différentes couleurs d'arrière-plan génère différentes couleurs de texte et d'icône.

La méthode contentColorFor() récupère la couleur "on" appropriée pour toutes les couleurs du thème. Par exemple, si vous définissez un arrière-plan de couleur primary pour la Surface, cette fonction est utilisée pour définir onPrimary comme couleur de contenu. Si vous choisissez une couleur d'arrière-plan différente de celle du thème, vous devez également spécifier une couleur de contenu appropriée. Utilisez LocalContentColor pour récupérer la couleur de contenu préférée de l'arrière-plan actuel, à une position donnée dans la hiérarchie.

Valeurs alpha du contenu

Vous souhaitez souvent varier la mise en valeur du contenu pour insister sur l'importance et fournir une hiérarchie visuelle. Les recommandations de lisibilité du texte Material Design conseillent d'employer différents niveaux d'opacité pour indiquer différents niveaux d'importance.

Jetpack Compose met cela en œuvre via LocalContentAlpha. Vous pouvez spécifier une valeur alpha du contenu pour une hiérarchie en fournissant une valeur pour ce CompositionLocal. Les composables imbriqués peuvent utiliser cette valeur pour appliquer le traitement alpha à leur contenu. Par exemple, Text et Icon utilisent par défaut la combinaison de LocalContentColor ajustée pour utiliser LocalContentAlpha. Material spécifie des valeurs alpha standards (high, medium, disabled), qui sont modélisées par l'objet 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(
        // ...
    )
}

Pour en savoir plus sur CompositionLocal, consultez le guide Données à champ d'application local avec CompositionLocal.

Capture d'écran du titre d'un article, montrant différents niveaux d'accentuation du texte

Figure 4. Appliquez différents niveaux d'accentuation du texte pour communiquer visuellement la hiérarchie des informations. La première ligne du texte correspond au titre et comporte les informations les plus importantes. Elle utilise donc ContentAlpha.high. La deuxième contient des métadonnées moins importantes et utilise donc ContentAlpha.medium.

Thème sombre

Dans Compose, vous implémentez des thèmes clair et sombre en fournissant différents ensembles de Colors pour le composable MaterialTheme :

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

Dans cet exemple, MaterialTheme est encapsulé dans sa propre fonction modulable, qui accepte un paramètre spécifiant s'il faut ou non utiliser un thème sombre. Dans ce cas, la fonction obtient la valeur par défaut pour darkTheme en interrogeant le paramètre de thème de l'appareil.

Vous pouvez utiliser ce code pour vérifier si les Colors actuelles sont claires ou sombres :

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

Superpositions d'élévation

Dans Material, les surfaces des thèmes sombres d'élévation plus élevée reçoivent des superpositions d'élévation, ce qui éclaircit leur arrière-plan. Plus l'élévation d'une surface est élevée (c'est-à-dire plus proche d'une source de lumière implicite), plus cette surface est claire.

Ces superpositions sont appliquées automatiquement par le composable Surface lors de l'utilisation de couleurs sombres, et pour tout autre composable Material qui utilise une surface :

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

Capture d'écran d'une application montrant les couleurs légèrement différentes utilisées pour des éléments à différentes élévations

Figure 5. Les cartes et la barre de navigation inférieure utilisent toutes deux la couleur surface comme arrière-plan. Étant donné que les cartes et la barre de navigation inférieure sont situées à des élévations différentes au-dessus de l'arrière-plan, les couleurs sont légèrement différentes. Les cartes sont plus claires que l'arrière-plan et la barre de navigation inférieure est plus claire que les cartes.

Pour les scénarios personnalisés qui n'impliquent pas de Surface, utilisez LocalElevationOverlay, un CompositionLocal contenant l'ElevationOverlay utilisée par les composants 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
)

Pour désactiver les superpositions d'élévation, indiquez null au point souhaité dans une hiérarchie composable :

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

Touches de couleurs limitées

Dans la plupart des cas, Material Design recommande d'appliquer des touches de couleurs limitées pour les thèmes sombres en utilisant plutôt la couleur surface que la couleur primary. Les composables Material tels que TopAppBar et BottomNavigation implémentent ce comportement par défaut.

Figure 6. Thème sombre Material avec des touches de couleur limitées. La barre d'application supérieure utilise la couleur principale du thème clair et la couleur de surface du thème sombre.

Pour les scénarios personnalisés, utilisez la propriété d'extension primarySurface :

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

Typographie

Material définit un système type, qui vous encourage à utiliser un petit nombre de styles nommés sémantiquement.

Exemples de polices différentes dans différents styles

Figure 7. Le système de type Material.

Compose implémente le système de type avec les classes Typography, TextStyle et liées à la police. Le constructeur Typography propose des valeurs par défaut pour chaque style afin que vous puissiez omettre ceux que vous ne souhaitez pas personnaliser :

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, /*...*/) {
    /*...*/
}

Si vous souhaitez utiliser la même police de caractères partout, spécifiez la defaultFontFamily parameter et omettez la fontFamily des éléments TextStyle :

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

Utiliser des styles de texte

Les TextStyle sont accessibles via MaterialTheme.typography. Récupérez les TextStyle comme suit :

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

Capture d'écran montrant différentes polices de caractères pour plusieurs objectifs

Figure 8. Utilisez une sélection de polices et de styles pour exprimer votre marque.

Forme

Material définit un système de formes qui vous permet de définir des formes pour les composants volumineux, moyens et petits.

Montre différentes formes Material Design

Figure 9. Le système de formes Material.

Compose implémente le système de formes avec la classe Shapes, qui vous permet de spécifier une CornerBasedShape pour chaque catégorie de taille :

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, /*...*/) {
    /*...*/
}

De nombreux composants utilisent ces formes par défaut. Par exemple : Button, TextField et FloatingActionButton sont définis par défaut sur "petit", AlertDialog sur "moyen", et ModalDrawer sur "grand". Consultez la documentation de référence sur les schémas de forme pour le mappage complet.

Utiliser des formes

Les Shapes sont accessibles via MaterialTheme.shapes. Récupérez les Shapes avec le code suivant :

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

Capture d'écran d'une application utilisant les formes Material pour indiquer l'état d'un élément

Figure 10. Utilisez des formes pour exprimer votre marque ou votre état.

Styles par défaut

Les styles par défaut pour les vues Android n'ont pas d'équivalent dans Compose. Vous pouvez fournir une fonctionnalité similaire en créant vos propres fonctions modulables de surcharge qui encapsulent les composants Material. Par exemple, pour créer un style de bouton, encapsulez un bouton dans votre fonction modulable, en définissant directement les paramètres que vous souhaitez modifier et en exposant les autres en tant que paramètres au composable associé.

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

Superpositions de thèmes

Vous pouvez obtenir l'équivalent des superpositions de thèmes d'Android Views dans Compose en imbriquant des composables MaterialTheme. Comme MaterialTheme utilise par défaut la valeur actuelle du thème pour les couleurs, la typographie et les formes, si un thème ne définit qu'un seul de ces paramètres, les autres conservent leurs valeurs par défaut.

De plus, lorsque vous migrez des écrans basés sur les vues Compose, faites attention aux utilisations de l'attribut android:theme. Vous aurez probablement besoin d'un nouveau MaterialTheme dans cette partie de l'arborescence de l'UI Compose.

Dans l'exemple Owl, l'écran des détails utilise un thème rose (PinkTheme) pour la majeure partie de l'écran, puis un thème bleu (BlueTheme) pour la section associée. Voir la capture d'écran et le code ci-dessous.

Figure 11. Thèmes imbriqués dans l'exemple Owl.

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

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

États des composants

Les composants Material avec lesquels vous pouvez interagir (cliqué, activé/désactivé, etc.) peuvent se trouver dans différents états visuels. Les états incluent : activé, désactivé, appuyé, etc.

Les composables comportent souvent un paramètre enabled. Définir sa valeur sur false empêche toute interaction et modifie des propriétés telles que la couleur et l'élévation pour transmettre visuellement l'état du composant.

Figure 12. Bouton avec enabled = true (à gauche) et enabled = false (à droite).

Dans la plupart des cas, vous pouvez conserver les valeurs par défaut pour les valeurs telles que la couleur et l'élévation. Si vous souhaitez configurer des valeurs utilisées dans différents états, des classes et des fonctions pratiques sont disponibles. Consultez l'exemple de bouton ci-dessous :

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
    )
) { /* ... */ }

Figure 13. Bouton avec enabled = true (à gauche) et enabled = false (à droite), avec des valeurs de couleur et d'élévation ajustées.

Ondulations

Les composants Material utilisent des ondulations pour indiquer qu'ils font l'objet d'une interaction. Si vous utilisez MaterialTheme dans votre hiérarchie, une ondulation (Ripple) sera utilisée comme Indication par défaut dans des modificateurs tels que clickable et indication.

Dans la plupart des cas, vous pouvez conserver la Ripple par défaut. Si vous souhaitez configurer leur apparence, vous pouvez utiliser RippleTheme pour modifier des propriétés telles que la couleur et la l'alpha.

Vous pouvez étendre RippleTheme et utiliser les fonctions utilitaires defaultRippleColor et defaultRippleAlpha. Vous pouvez ensuite fournir votre thème d'ondulation personnalisé dans votre hiérarchie à l'aide de 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
    )
}

alt_text

Figure 14. Boutons avec différentes valeurs d'ondulation fournies par RippleTheme.

En savoir plus

Pour en savoir plus sur la thématisation Material dans Compose, consultez les ressources supplémentaires suivantes.

Ateliers de programmation

Vidéos