Aggiungere ombre in Composizione

Le ombre migliorano visivamente la tua UI, 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 elemento componibile che rispetta le linee guida di Material Design.
  • Modifier.dropShadow(): crea un'ombra personalizzabile che appare dietro a un componente, facendolo sembrare sollevato.
  • Modifier.innerShadow(): crea un'ombra all'interno dei bordi di un componente componibile, facendolo sembrare premuto sulla superficie dietro.

Modifier.shadow() è adatto per creare ombre di base, mentre i modificatori dropShadow e innerShadow offrono un controllo più granulare e precisione nel 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 sfumate, ombre neumorfiche e altro ancora.

Creare ombre di base

Modifier.shadow() crea un'ombra di base che segue le linee guida di Material Design che simula una sorgente luminosa dall'alto. La profondità dell'ombra si basa su un valore elevation e l'ombra proiettata viene ritagliata in base alla forma del componente componibile.

@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.

Implementare 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 dal pacchetto androidx.compose.foundation.shape, nonché le forme espressive di Material.

Per implementare un'ombra di base, aggiungi il modificatore dropShadow() alla catena di componenti, fornendo raggio, colore e 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 del codice

  • Il modificatore dropShadow() viene applicato al Box interno. L'ombra ha le seguenti caratteristiche:
    • Una forma rettangolare arrotondata (RoundedCornerShape(20.dp))
    • Un raggio sfocatura di 10.dp, che rende i bordi morbidi e diffusi
    • Una diffusione di 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, il .Viene applicato il modificatore background().
    • Il Box è riempito di bianco.
    • Lo sfondo viene ritagliato nella stessa forma rettangolare arrotondata dell'ombra.

Risultato

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

Implementare 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. L'ombra interna viene disegnata sopra i contenuti, quindi in genere devi:

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

Se innerShadow() viene posizionato prima dello sfondo, quest'ultimo 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.

Animare le ombre in base all'interazione dell'utente

Per fare in modo che le ombre rispondano alle interazioni degli utenti, puoi integrare le proprietà dell'ombra con le API di animazione di Compose. Quando un utente preme un pulsante, ad esempio, l'ombra può cambiare per fornire un feedback visivo immediato.

Il seguente codice crea un effetto "premuto" con un'ombra (l'illusione che la superficie venga spinta verso il basso dello 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 del codice

  • Dichiara gli stati iniziale e finale dei parametri da animare alla pressione con transition.animateColor e transition.animateFloat.
  • Utilizza updateTransition e 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 correnti ai nuovi valori di destinazione.
  • Definisce la specifica buttonPressAnimation, che controlla la tempistica e l'accelerazione 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 a metà e rallenta alla fine.
  • Definisce un Box con una catena di funzioni modificatrici che applicano tutte le proprietà animate per creare l'elemento visivo, tra cui:
    • .clickable(): un modificatore che rende Box interattivo.
    • .dropShadow(): vengono applicate prima due ombre esterne. Le 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.

Creare ombre sfumate

Le ombre non sono limitate ai colori pieni. L'API Shadow accetta un Brush, che consente di creare ombre sfumate.

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 del codice

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

Risultato

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

Figura 5. Un'ombra animata con gradiente.

Combina ombre

Puoi combinare e sovrapporre i modificatori dropShadow() e innerShadow() per creare una serie 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 condivida gli stessi colori dello sfondo.
  2. Applica due ombre sfumate e opposte: un'ombra chiara a un angolo e un'ombra scura all'angolo opposto.

Lo snippet seguente 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.

Creare ombre neobrutaliste

Lo stile neobrutalista mette in mostra 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 nel seguente snippet:

@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 intorno a un rettangolo bianco con un&#39;ombra blu su uno sfondo giallo.
Figura 7. Un effetto ombra neobrutalista.

Creare ombre realistiche

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

@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 del 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 di bordo metallico attorno al bordo del componente.

Risultato

Lo snippet di codice precedente produce quanto segue:

Un&#39;ombra bianca realistica intorno a una forma arrotondata nera.
Figura 8. Un effetto ombra realistico.