Aggiungere ombre in Composizione

Le ombre migliorano visivamente l'interfaccia utente, indicano l'interattività agli utenti e forniscono un feedback immediato sulle azioni degli utenti. Compose offre diversi modi per incorporare le ombre nella tua app:

  • Modifier.shadow(): crea un'ombra basata sull'elevazione dietro un composable conforme alle linee guida di Material Design.
  • Modifier.dropShadow(): crea un'ombra personalizzabile che appare dietro un composable, facendolo sembrare in rilievo.
  • Modifier.innerShadow(): crea un'ombra all'interno dei bordi di un componibile, facendolo sembrare premuto sulla superficie dietro.

Modifier.shadow() è adatto per creare ombre di base, mentre i modificatori dropShadow() e innerShadow() offrono un controllo e una precisione più granulari sul rendering delle ombre.

Questa pagina descrive come implementare ciascuno di questi modificatori, incluso come animare le ombre in base all'interazione dell'utente e come concatenare i modificatori innerShadow() e dropShadow() per creare ombre con gradiente, ombre neumorfiche e altro ancora.

Crea ombre di base

Modifier.shadow() crea un'ombra di base seguendo le linee guida di Material Design che simula una sorgente di luce dall'alto. La profondità dell'ombra si basa su un valore elevation e l'ombra proiettata viene ritagliata in base alla forma del 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)
        )
    }
}

Un'ombra grigia proiettata intorno a una forma rettangolare bianca.
Figura 1. Un'ombra basata sull'elevazione creata con Modifier.shadow().

Implementa le ombre

Utilizza il modificatore dropShadow() per disegnare un'ombra precisa dietro i tuoi contenuti, in modo che l'elemento appaia sollevato.

Puoi controllare i seguenti aspetti chiave tramite il parametro Shadow:

  • radius: definisce la morbidezza e la diffusione della sfocatura.
  • color: definisce il colore della tinta.
  • offset: posiziona la geometria dell'ombra lungo gli assi X e Y.
  • spread: controlla l'espansione o la contrazione della geometria dell'ombra.

Inoltre, il parametro shape definisce la forma complessiva dell'ombra. Può utilizzare qualsiasi geometria del pacchetto androidx.compose.foundation.shape, nonché le forme Material Expressive.

Per implementare un'ombra di base, aggiungi il modificatore dropShadow() alla catena componibile, specificando il raggio, il colore e la diffusione. Tieni presente che lo sfondo purpleColor che appare sopra l'ombra viene disegnato dopo il modificatore 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
            )
        }
    }
}

Punti chiave sul codice

  • Il modificatore dropShadow() viene applicato all'elemento Box interno. L'ombra ha le seguenti caratteristiche:
    • Una forma rettangolare con angoli arrotondati (RoundedCornerShape(20.dp))
    • Un raggio di sfocatura di 10.dp, che rende i bordi morbidi e diffusi
    • Un valore di diffusione pari a 6.dp, che espande le dimensioni dell'ombra e la rende più grande della casella che la proietta
    • Un valore alfa di 0.5f, che rende l'ombra semitrasparente
  • Dopo aver definito l'ombra, ilIl modificatore background() è applicato.
    • La Box è riempita di colore bianco.
    • Lo sfondo viene ritagliato nella stessa forma rettangolare con angoli arrotondati dell'ombra.

Risultato

Un'ombra grigia proiettata intorno a una forma rettangolare bianca.
Figura 2. Un'ombra disegnata attorno alla forma.

Implementa le ombre interne

Per creare un effetto inverso a dropShadow(), utilizza Modifier.innerShadow(), che crea l'illusione che un elemento sia incassato o premuto nella superficie sottostante.

L'ordine è importante quando si creano ombre interne. Il modificatore innerShadow() disegna sopra i contenuti. Per assicurarti che l'ombreggiatura sia visibile, in genere devi eseguire i seguenti passaggi:

  1. Disegna i contenuti in background.
  2. Applica il modificatore innerShadow() per creare l'aspetto concavo.

Se l'elemento innerShadow() viene posizionato prima dello sfondo, lo sfondo viene disegnato sopra l'ombra, nascondendola completamente.

L'esempio seguente mostra un'applicazione di innerShadow() su 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
            )
        }
    }
}

Un'ombra interna grigia all'interno di una forma rettangolare bianca.
Figura 3. Un'applicazione di Modifier.innerShadow() su un rettangolo con angoli arrotondati.

Anima le ombre in base all'interazione dell'utente

Per fare in modo che le ombre rispondano alle interazioni degli utenti, puoi integrare le proprietà delle ombre con le API Animation di Compose. Ad esempio, quando un utente preme un pulsante, l'ombra può cambiare per fornire un feedback visivo istantaneo.

Il seguente codice crea un effetto "premuto" con un'ombra (l'illusione che la superficie venga spinta verso il basso sullo schermo):

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

Punti chiave sul codice

  • Dichiara gli stati iniziale e finale dei parametri da animare alla pressione con transition.animateColor e transition.animateFloat.
  • Utilizza updateTransition e gli fornisce il targetState (targetState = isPressed) scelto per verificare che tutte le animazioni siano sincronizzate. Ogni volta che isPressed cambia, l'oggetto di transizione gestisce automaticamente l'animazione di tutte le proprietà secondarie dai valori attuali ai nuovi valori target.
  • Definisce la specifica buttonPressAnimation, che controlla la tempistica e l'easing della transizione. Specifica un tween (abbreviazione di in-between) con una durata di 400 millisecondi e una curva EaseInOut, il che significa che l'animazione inizia lentamente, accelera nel mezzo e rallenta alla fine.
  • Definisce un Box con una catena di funzioni di modifica che applicano tutte le proprietà animate per creare l'elemento visivo, tra cui:
    • .clickable(): un modificatore che rende interattivo Box.
    • .dropShadow(): vengono applicate prima due ombre esterne. Le loro proprietà di colore e alfa sono collegate ai valori animati (blueDropShadow e così via) e creano l'aspetto iniziale in rilievo.
    • .innerShadow(): due ombre interne vengono disegnate sopra lo sfondo. Le loro proprietà sono collegate all'altro insieme di valori animati (innerShadowColor1 e così via) e creano l'aspetto rientrato.

Risultato

Figura 4. Un'ombra che si anima quando l'utente preme.

Crea ombre con sfumatura

Le ombre non sono limitate ai colori a tinta unita. L'API Shadow accetta un Brush, che consente di creare ombre con gradiente.

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

Punti chiave sul codice

  • dropShadow() aggiunge un'ombra dietro la casella.
  • brush = Brush.sweepGradient(colors) colora l'ombra con un gradiente che ruota attraverso un elenco di colors predefiniti, creando un effetto simile a un arcobaleno.

Risultato

Puoi utilizzare un pennello come ombra per creare un gradiente dropShadow() con un'animazione "pulsante":

Figura 5. Un'ombra con gradiente animato.

Combina le ombre

Puoi combinare e sovrapporre i modificatori dropShadow() e innerShadow() per creare una varietà di effetti. Le sezioni seguenti mostrano come produrre ombre neumorfiche, neobrutaliste e realistiche con questa tecnica.

Crea ombre neumorfiche

Le ombre neumorfiche sono caratterizzate da un aspetto morbido che emerge in modo organico dallo sfondo. Per creare ombre neumorfiche:

  1. Utilizza un elemento che condivide gli stessi colori dello sfondo.
  2. Applica due ombreggiature tenui e opposte: un'ombra chiara in un angolo e un'ombra scura nell'angolo opposto.

Il seguente snippet sovrappone due modificatori dropShadow() per creare l'effetto neumorfico:

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

Una forma rettangolare bianca con un effetto neumorfico su sfondo bianco.
Figura 6. Un effetto ombra neumorfico.

Crea ombre neobrutaliste

Lo stile neobrutalista presenta layout a blocchi ad alto contrasto, colori vivaci e bordi spessi. Per creare questo effetto, utilizza un dropShadow() con sfocatura pari a zero e un offset distinto, come mostrato nello snippet seguente:

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

Un bordo rosso attorno a un rettangolo bianco con un'ombra blu su sfondo giallo.
Figura 7. Un effetto ombra neobrutalista.

Crea ombre realistiche

Le ombre realistiche imitano le ombre del mondo fisico: appaiono illuminate da una fonte di luce principale, con un'ombra diretta e un'ombra più diffusa. Puoi impilare più istanze dropShadow() e innerShadow() con proprietà diverse per ricreare effetti di ombra realistici, come mostrato nello snippet seguente:

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

Punti chiave sul codice

  • Vengono applicati due modificatori dropShadow() concatenati con proprietà distinte, seguiti da un modificatore background().
  • I modificatori innerShadow() concatenati vengono applicati per creare l'effetto bordo metallico attorno al bordo del componente.

Risultato

Lo snippet di codice precedente produce quanto segue:

Un'ombra bianca realistica attorno a una forma arrotondata nera.
Figura 8. Un effetto ombra realistico.