Ajouter des ombres dans Compose

Les ombres élèvent visuellement votre UI, indiquent l'interactivité aux utilisateurs et fournissent un retour immédiat sur les actions de l'utilisateur. Compose propose plusieurs façons d'intégrer des ombres dans votre application :

  • Modifier.shadow() : crée une ombre basée sur l'élévation derrière un composable conforme aux consignes Material Design.
  • Modifier.dropShadow() : crée une ombre personnalisable qui apparaît derrière un composable, le faisant apparaître comme surélevé.
  • Modifier.innerShadow() : crée une ombre à l'intérieur des bordures d'un composable, le faisant apparaître comme enfoncé dans la surface derrière lui.

Modifier.shadow() convient à la création d'ombres de base, tandis que les modificateurs dropShadow et innerShadow offrent un contrôle et une précision plus précis sur le rendu des ombres.

Cette page explique comment implémenter chacun de ces modificateurs, y compris comment animer les ombres lors de l'interaction de l'utilisateur et comment enchaîner les modificateurs innerShadow() et dropShadow() pour créer des ombres dégradées, des ombres neumorphiques et plus encore.

Créer des ombres de base

Modifier.shadow() crée une ombre de base suivant les consignes de Material Design qui simule une source de lumière provenant du dessus. La profondeur de l'ombre est basée sur une valeur elevation, et l'ombre portée est découpée selon la forme du composable.

@Composable
fun ElevationBasedShadow() {
    Box(
        modifier = Modifier.aspectRatio(1f).fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Box(
            Modifier
                .size(100.dp, 100.dp)
                .shadow(10.dp, RectangleShape)
                .background(Color.White)
        )
    }
}

Ombre grise projetée autour d'une forme rectangulaire blanche.
Figure 1. Ombre basée sur l'élévation créée avec Modifier.shadow.

Implémenter des ombres projetées

Utilisez le modificateur dropShadow() pour dessiner une ombre précise derrière votre contenu, ce qui donne l'impression que l'élément est surélevé.

Vous pouvez contrôler les aspects clés suivants grâce à son paramètre Shadow :

  • radius : définit la douceur et la diffusion de votre flou.
  • color : définit la couleur de la teinte.
  • offset : positionne la géométrie de l'ombre le long des axes x et y.
  • spread : contrôle l'expansion ou la contraction de la géométrie de l'ombre.

En outre, le paramètre shape définit la forme globale de l'ombre. Il peut utiliser n'importe quelle géométrie du package androidx.compose.foundation.shape, ainsi que les formes expressives Material.

Pour implémenter une ombre portée de base, ajoutez le modificateur dropShadow() à votre chaîne de composables, en fournissant le rayon, la couleur et l'étendue. Notez que l'arrière-plan purpleColor qui apparaît au-dessus de l'ombre est dessiné après le modificateur dropShadow() :

@Composable
fun SimpleDropShadowUsage() {
    Box(Modifier.fillMaxSize()) {
        Box(
            Modifier
                .width(300.dp)
                .height(300.dp)
                .dropShadow(
                    shape = RoundedCornerShape(20.dp),
                    shadow = Shadow(
                        radius = 10.dp,
                        spread = 6.dp,
                        color = Color(0x40000000),
                        offset = DpOffset(x = 4.dp, 4.dp)
                    )
                )
                .align(Alignment.Center)
                .background(
                    color = Color.White,
                    shape = RoundedCornerShape(20.dp)
                )
        ) {
            Text(
                "Drop Shadow",
                modifier = Modifier.align(Alignment.Center),
                fontSize = 32.sp
            )
        }
    }
}

Points clés concernant le code

  • Le modificateur dropShadow() est appliqué au Box interne. L'ombre présente les caractéristiques suivantes :
    • Forme rectangulaire arrondie (RoundedCornerShape(20.dp))
    • Rayon de flou de 10.dp, ce qui rend les bords doux et diffus
    • Une valeur d'étalement de 6.dp, qui augmente la taille de l'ombre et la rend plus grande que la boîte qui la projette
    • Un alpha de 0.5f, qui rend l'ombre semi-transparente
  • Une fois l'ombre définie, le .Le modificateur background() est appliqué.
    • Box est rempli en blanc.
    • L'arrière-plan est découpé en forme de rectangle arrondi comme l'ombre.

Résultat

Ombre portée grise autour d'une forme rectangulaire blanche.
Figure 2. Une ombre portée est dessinée autour de la forme.

Implémenter des ombres intérieures

Pour créer un effet inverse à dropShadow, utilisez Modifier.innerShadow(), qui donne l'illusion qu'un élément est en retrait ou enfoncé dans la surface sous-jacente.

L'ordre est important lors de la création d'ombres intérieures. L'ombre intérieure est dessinée au-dessus du contenu. Vous devez donc généralement procéder comme suit :

  1. Dessinez le contenu de votre arrière-plan.
  2. Appliquez le modificateur innerShadow() pour créer l'apparence concave.

Si innerShadow() est placé avant l'arrière-plan, celui-ci est dessiné au-dessus de l'ombre, la masquant complètement.

L'exemple suivant montre une application de innerShadow() sur un RoundedCornerShape :

@Composable
fun SimpleInnerShadowUsage() {
    Box(Modifier.fillMaxSize()) {
        Box(
            Modifier
                .width(300.dp)
                .height(200.dp)
                .align(Alignment.Center)
                // note that the background needs to be defined before defining the inner shadow
                .background(
                    color = Color.White,
                    shape = RoundedCornerShape(20.dp)
                )
                .innerShadow(
                    shape = RoundedCornerShape(20.dp),
                    shadow = Shadow(
                        radius = 10.dp,
                        spread = 2.dp,
                        color = Color(0x40000000),
                        offset = DpOffset(x = 6.dp, 7.dp)
                    )
                )

        ) {
            Text(
                "Inner Shadow",
                modifier = Modifier.align(Alignment.Center),
                fontSize = 32.sp
            )
        }
    }
}

Ombre intérieure grise à l'intérieur d'une forme rectangulaire blanche.
Figure 3. Application de Modifier.innerShadow() sur un rectangle à coins arrondis.

Animer les ombres lors d'une interaction utilisateur

Pour que vos ombres réagissent aux interactions des utilisateurs, vous pouvez intégrer des propriétés d'ombre aux API d'animation de Compose. Lorsqu'un utilisateur appuie sur un bouton, par exemple, l'ombre peut changer pour fournir un retour visuel instantané.

Le code suivant crée un effet "appuyé" avec une ombre (l'illusion que la surface est enfoncée dans l'écran) :

@Composable
fun AnimatedColoredShadows() {
    SnippetsTheme {
        Box(Modifier.fillMaxSize()) {
            val interactionSource = remember { MutableInteractionSource() }
            val isPressed by interactionSource.collectIsPressedAsState()

            // Create transition with pressed state
            val transition = updateTransition(
                targetState = isPressed,
                label = "button_press_transition"
            )

            fun <T> buttonPressAnimation() = tween<T>(
                durationMillis = 400,
                easing = EaseInOut
            )

            // Animate all properties using the transition
            val shadowAlpha by transition.animateFloat(
                label = "shadow_alpha",
                transitionSpec = { buttonPressAnimation() }
            ) { pressed ->
                if (pressed) 0f else 1f
            }
            // ...

            val blueDropShadow by transition.animateColor(
                label = "shadow_color",
                transitionSpec = { buttonPressAnimation() }
            ) { pressed ->
                if (pressed) Color.Transparent else blueDropShadowColor
            }

            // ...

            Box(
                Modifier
                    .clickable(
                        interactionSource, indication = null
                    ) {
                        // ** ...... **//
                    }
                    .width(300.dp)
                    .height(200.dp)
                    .align(Alignment.Center)
                    .dropShadow(
                        shape = RoundedCornerShape(70.dp),
                        shadow = Shadow(
                            radius = 10.dp,
                            spread = 0.dp,
                            color = blueDropShadow,
                            offset = DpOffset(x = 0.dp, -(2).dp),
                            alpha = shadowAlpha
                        )
                    )
                    .dropShadow(
                        shape = RoundedCornerShape(70.dp),
                        shadow = Shadow(
                            radius = 10.dp,
                            spread = 0.dp,
                            color = darkBlueDropShadow,
                            offset = DpOffset(x = 2.dp, 6.dp),
                            alpha = shadowAlpha
                        )
                    )
                    // note that the background needs to be defined before defining the inner shadow
                    .background(
                        color = Color(0xFFFFFFFF),
                        shape = RoundedCornerShape(70.dp)
                    )
                    .innerShadow(
                        shape = RoundedCornerShape(70.dp),
                        shadow = Shadow(
                            radius = 8.dp,
                            spread = 4.dp,
                            color = innerShadowColor2,
                            offset = DpOffset(x = 4.dp, 0.dp)
                        )
                    )
                    .innerShadow(
                        shape = RoundedCornerShape(70.dp),
                        shadow = Shadow(
                            radius = 20.dp,
                            spread = 4.dp,
                            color = innerShadowColor1,
                            offset = DpOffset(x = 4.dp, 0.dp),
                            alpha = innerShadowAlpha
                        )
                    )

            ) {
                Text(
                    "Animated Shadows",
                    // ...
                )
            }
        }
    }
}

Points clés concernant le code

  • Déclare les états de début et de fin des paramètres à animer lors de l'appui avec transition.animateColor et transition.animateFloat.
  • Utilise updateTransition et lui fournit le targetState (targetState = isPressed) choisi pour vérifier que toutes les animations sont synchronisées. Chaque fois que isPressed change, l'objet de transition gère automatiquement l'animation de toutes les propriétés enfants, de leurs valeurs actuelles aux nouvelles valeurs cibles.
  • Définit la spécification buttonPressAnimation, qui contrôle le timing et l'adoucissement de la transition. Il spécifie un tween (abréviation de "in-between") d'une durée de 400 millisecondes et une courbe EaseInOut, ce qui signifie que l'animation commence lentement, s'accélère au milieu et ralentit à la fin.
  • Définit un Box avec une chaîne de fonctions de modificateur qui appliquent toutes les propriétés animées pour créer l'élément visuel, y compris les suivantes :
    • .clickable() : modificateur qui rend Box interactif.
    • .dropShadow() : deux ombres projetées extérieures sont appliquées en premier. Leurs propriétés de couleur et alpha sont liées aux valeurs animées (blueDropShadow, etc.) et créent l'apparence surélevée initiale.
    • .innerShadow() : deux ombres intérieures sont dessinées au-dessus de l'arrière-plan. Leurs propriétés sont liées à l'autre ensemble de valeurs animées (innerShadowColor1, etc.) et créent l'apparence en retrait.

Résultat

Figure 4. Une ombre s'anime lorsque l'utilisateur appuie sur l'écran.

Créer des ombres en dégradé

Les ombres ne sont pas limitées aux couleurs unies. L'API Shadow accepte un Brush, qui vous permet de créer des ombres dégradées.

Box(
    modifier = Modifier
        .width(240.dp)
        .height(200.dp)
        .dropShadow(
            shape = RoundedCornerShape(70.dp),
            shadow = Shadow(
                radius = 10.dp,
                spread = animatedSpread.dp,
                brush = Brush.sweepGradient(
                    colors
                ),
                offset = DpOffset(x = 0.dp, y = 0.dp),
                alpha = animatedAlpha
            )
        )
        .clip(RoundedCornerShape(70.dp))
        .background(Color(0xEDFFFFFF)),
    contentAlignment = Alignment.Center
) {
    Text(
        text = breathingText,
        color = Color.Black,
        style = MaterialTheme.typography.bodyLarge
    )
}

Points clés concernant le code

  • dropShadow() ajoute une ombre derrière la boîte.
  • brush = Brush.sweepGradient(colors) colore l'ombre avec un dégradé qui parcourt une liste de colors prédéfinies, créant ainsi un effet arc-en-ciel.

Résultat

Vous pouvez utiliser un pinceau comme ombre pour créer un dégradé dropShadow() avec une animation "respirante" :

Figure 5. Ombre portée animée avec dégradé.

Combiner des ombres

Vous pouvez combiner et superposer les modificateurs dropShadow() et innerShadow() pour créer différents effets. Les sections suivantes vous expliquent comment produire des ombres néomorphiques, néobrutalistes et réalistes avec cette technique.

Créer des ombres neumorphiques

Les ombres neumorphiques se caractérisent par une apparence douce qui émerge naturellement de l'arrière-plan. Pour créer des ombres neumorphiques :

  1. Utilisez un élément dont les couleurs sont identiques à celles de son arrière-plan.
  2. Appliquez deux ombres portées opposées et légères : une ombre claire sur un angle et une ombre foncée sur l'angle opposé.

L'extrait suivant superpose deux modificateurs dropShadow() pour créer l'effet neumorphique :

@Composable
fun NeumorphicRaisedButton(
    shape: RoundedCornerShape = RoundedCornerShape(30.dp)
) {
    val bgColor = Color(0xFFe0e0e0)
    val lightShadow = Color(0xFFFFFFFF)
    val darkShadow = Color(0xFFb1b1b1)
    val upperOffset = -10.dp
    val lowerOffset = 10.dp
    val radius = 15.dp
    val spread = 0.dp
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(bgColor)
            .wrapContentSize(Alignment.Center)
            .size(240.dp)
            .dropShadow(
                shape,
                shadow = Shadow(
                    radius = radius,
                    color = lightShadow,
                    spread = spread,
                    offset = DpOffset(upperOffset, upperOffset)
                ),
            )
            .dropShadow(
                shape,
                shadow = Shadow(
                    radius = radius,
                    color = darkShadow,
                    spread = spread,
                    offset = DpOffset(lowerOffset, lowerOffset)
                ),

            )
            .background(bgColor, shape)
    )
}

Forme rectangulaire blanche avec un effet neumorphique sur fond blanc.
Figure 6 : Effet d'ombre neumorphique.

Créer des ombres néobrutalistes

Le style néobrutaliste se caractérise par des mises en page en blocs très contrastées, des couleurs vives et des bordures épaisses. Pour créer cet effet, utilisez un dropShadow() avec un flou nul et un décalage distinct, comme indiqué dans l'extrait suivant :

@Composable
fun NeoBrutalShadows() {
    SnippetsTheme {
        val dropShadowColor = Color(0xFF007AFF)
        val borderColor = Color(0xFFFF2D55)
        Box(Modifier.fillMaxSize()) {
            Box(
                Modifier
                    .width(300.dp)
                    .height(200.dp)
                    .align(Alignment.Center)
                    .dropShadow(
                        shape = RoundedCornerShape(0.dp),
                        shadow = Shadow(
                            radius = 0.dp,
                            spread = 0.dp,
                            color = dropShadowColor,
                            offset = DpOffset(x = 8.dp, 8.dp)
                        )
                    )
                    .border(
                        8.dp, borderColor
                    )
                    .background(
                        color = Color.White,
                        shape = RoundedCornerShape(0.dp)
                    )
            ) {
                Text(
                    "Neobrutal Shadows",
                    modifier = Modifier.align(Alignment.Center),
                    style = MaterialTheme.typography.bodyMedium
                )
            }
        }
    }
}

Bordure rouge autour d&#39;un rectangle blanc avec une ombre bleue sur un fond jaune.
Figure 7 : Un effet d'ombre néobrutaliste.

Créer des ombres réalistes

Les ombres réalistes imitent les ombres du monde physique. Elles semblent éclairées par une source de lumière principale, ce qui donne lieu à une ombre directe et à une ombre plus diffuse. Vous pouvez empiler plusieurs instances dropShadow() et innerShadow() avec différentes propriétés pour recréer des effets d'ombre réalistes, comme indiqué dans l'extrait suivant :

@Composable
fun RealisticShadows() {
    Box(Modifier.fillMaxSize()) {
        val dropShadowColor1 = Color(0xB3000000)
        val dropShadowColor2 = Color(0x66000000)

        val innerShadowColor1 = Color(0xCC000000)
        val innerShadowColor2 = Color(0xFF050505)
        val innerShadowColor3 = Color(0x40FFFFFF)
        val innerShadowColor4 = Color(0x1A050505)
        Box(
            Modifier
                .width(300.dp)
                .height(200.dp)
                .align(Alignment.Center)
                .dropShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 40.dp,
                        spread = 0.dp,
                        color = dropShadowColor1,
                        offset = DpOffset(x = 2.dp, 8.dp)
                    )
                )
                .dropShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 4.dp,
                        spread = 0.dp,
                        color = dropShadowColor2,
                        offset = DpOffset(x = 0.dp, 4.dp)
                    )
                )
                // note that the background needs to be defined before defining the inner shadow
                .background(
                    color = Color.Black,
                    shape = RoundedCornerShape(100.dp)
                )
// //
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 12.dp,
                        spread = 3.dp,
                        color = innerShadowColor1,
                        offset = DpOffset(x = 6.dp, 6.dp)
                    )
                )
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 4.dp,
                        spread = 1.dp,
                        color = Color.White,
                        offset = DpOffset(x = 5.dp, 5.dp)
                    )
                )
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 12.dp,
                        spread = 5.dp,
                        color = innerShadowColor2,
                        offset = DpOffset(x = (-3).dp, (-12).dp)
                    )
                )
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 3.dp,
                        spread = 10.dp,
                        color = innerShadowColor3,
                        offset = DpOffset(x = 0.dp, 0.dp)
                    )
                )
                .innerShadow(
                    shape = RoundedCornerShape(100.dp),
                    shadow = Shadow(
                        radius = 3.dp,
                        spread = 9.dp,
                        color = innerShadowColor4,
                        offset = DpOffset(x = 1.dp, 1.dp)
                    )
                )

        ) {
            Text(
                "Realistic Shadows",
                modifier = Modifier.align(Alignment.Center),
                fontSize = 24.sp,
                color = Color.White
            )
        }
    }
}

Points clés concernant le code

  • Deux modificateurs dropShadow() chaînés avec des propriétés distinctes sont appliqués, suivis d'un modificateur background.
  • Des modificateurs innerShadow() chaînés sont appliqués pour créer l'effet de bordure métallique autour du bord du composant.

Résultat

L'extrait de code précédent produit le résultat suivant :

Ombre blanche réaliste autour d&#39;une forme arrondie noire.
Figure 8 : Un effet d'ombre réaliste.