Adicionar sombras no Compose

As sombras elevam visualmente a interface, indicam interatividade aos usuários e fornecem feedback imediato sobre as ações deles. O Compose oferece várias maneiras de incorporar sombras ao app:

  • Modifier.shadow(): cria uma sombra baseada em elevação atrás de um elemento combinável que segue as diretrizes do Material Design.
  • Modifier.dropShadow(): cria uma sombra personalizável que aparece atrás de um elemento combinável, fazendo com que ele pareça elevado.
  • Modifier.innerShadow(): cria uma sombra dentro das bordas de um elemento combinável, fazendo com que ele pareça pressionado contra a superfície atrás dele.

Modifier.shadow() é adequado para criar sombras básicas, enquanto os modificadores dropShadow() e innerShadow() oferecem controle e precisão mais refinados sobre a renderização de sombras.

Nesta página, descrevemos como implementar cada um desses modificadores, incluindo como animar sombras na interação do usuário e como encadear os modificadores innerShadow() e dropShadow() para criar sombras de gradiente, sombras neumórficas e muito mais.

Criar sombras básicas

Modifier.shadow() cria uma sombra básica seguindo as diretrizes do Material Design que simula uma fonte de luz superior. A profundidade da sombra é baseada em um valor elevation, e a sombra projetada é cortada de acordo com a forma do elemento combinável.

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

Uma sombra cinza projetada ao redor de uma forma retangular branca.
Figura 1. Uma sombra baseada em elevação criada com Modifier.shadow().

Implementar sombras projetadas

Use o modificador dropShadow() para desenhar uma sombra precisa atrás do conteúdo, o que faz com que o elemento pareça elevado.

É possível controlar os seguintes aspectos principais usando o parâmetro Shadow:

  • radius: define a suavidade e a difusão do desfoque.
  • color: define a cor da tonalidade.
  • offset: posiciona a geometria da sombra ao longo dos eixos x e y.
  • spread: controla a expansão ou contração da geometria da sombra.

Além disso, o parâmetro shape define a forma geral da sombra. Ele pode usar qualquer geometria do pacote androidx.compose.foundation.shape, além das formas expressivas do Material Design.

Para implementar uma sombra projetada básica, adicione o modificador dropShadow() à sua cadeia de combináveis, fornecendo o raio, a cor e a propagação. O plano de fundo purpleColor que aparece sobre a sombra é desenhado após o modificador 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
            )
        }
    }
}

Pontos principais sobre o código

  • O modificador dropShadow() é aplicado ao Box interno. A sombra tem as seguintes características:
    • Uma forma de retângulo arredondado (RoundedCornerShape(20.dp))
    • Um raio de desfoque de 10.dp, que suaviza e difunde as bordas.
    • Uma propagação de 6.dp, que expande o tamanho da sombra e a torna maior que a caixa que a projeta.
    • Um alfa de 0.5f, tornando a sombra semitransparente.
  • Depois que a sombra é definida, oO modificador background() é aplicado.
    • A Box é preenchida com uma cor branca.
    • O plano de fundo é cortado na mesma forma de retângulo arredondado que a sombra.

Resultado

Uma sombra projetada cinza ao redor de uma forma retangular branca.
Figura 2. Uma sombra projetada ao redor da forma.

Implementar sombras internas

Para criar um efeito inverso a dropShadow(), use Modifier.innerShadow(), que cria a ilusão de que um elemento está rebaixado ou pressionado na superfície abaixo.

A ordem é importante ao criar sombras internas. O modificador innerShadow() desenha na parte de cima do conteúdo. Para garantir que a sombra fique visível, geralmente você segue estas etapas:

  1. Desenhe o conteúdo em segundo plano.
  2. Aplique o modificador innerShadow() para criar a aparência côncava.

Se o innerShadow() for colocado antes do plano de fundo, o plano de fundo será desenhado sobre a sombra, ocultando-a completamente.

O exemplo a seguir mostra uma aplicação de innerShadow() em um 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
            )
        }
    }
}

Uma sombra interna cinza dentro de uma forma retangular branca.
Figura 3. Uma aplicação de Modifier.innerShadow() em um retângulo de canto arredondado.

Animar sombras na interação do usuário

Para que as sombras respondam às interações do usuário, integre as propriedades de sombra às APIs de animação do Compose. Quando um usuário pressiona um botão, por exemplo, a sombra pode mudar para fornecer feedback visual instantâneo.

O código a seguir cria um efeito de "pressionado" com uma sombra (a ilusão de que a superfície está sendo empurrada para baixo na tela):

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

Pontos principais sobre o código

  • Declara os estados inicial e final para os parâmetros serem animados ao pressionar com transition.animateColor e transition.animateFloat.
  • Usa updateTransition e fornece o targetState (targetState = isPressed) escolhido para verificar se todas as animações estão sincronizadas. Sempre que isPressed muda, o objeto de transição gerencia automaticamente a animação de todas as propriedades filhas, dos valores atuais para os novos valores de destino.
  • Define a especificação buttonPressAnimation, que controla o tempo e a easing da transição. Ele especifica um tween (abreviação de "in-between", ou seja, "intermediário") com uma duração de 400 milissegundos e uma curva EaseInOut, o que significa que a animação começa devagar, acelera no meio e desacelera no final.
  • Define um Box com uma cadeia de funções modificadoras que aplicam todas as propriedades animadas para criar o elemento visual, incluindo:
    • .clickable(): um modificador que torna o Box interativo.
    • .dropShadow(): duas sombras projetadas externas são aplicadas primeiro. As propriedades de cor e alfa estão vinculadas aos valores animados (blueDropShadow etc.) e criam a aparência elevada inicial.
    • .innerShadow(): duas sombras internas são desenhadas sobre o plano de fundo. As propriedades deles estão vinculadas ao outro conjunto de valores animados (innerShadowColor1 etc.) e criam a aparência recuada.

Resultado

Figura 4. Uma sombra que se anima quando o usuário pressiona o botão.

Criar sombras em gradiente

As sombras não se limitam a cores sólidas. A API de sombra aceita um Brush, que permite criar sombras de 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
    )
}

Pontos principais sobre o código

  • dropShadow() adiciona uma sombra atrás da caixa.
  • brush = Brush.sweepGradient(colors) colore a sombra com um gradiente que gira por uma lista de colors predefinidos, criando um efeito de arco-íris.

Resultado

É possível usar um pincel como sombra para criar um gradiente dropShadow() com uma animação de "respiração":

Figura 5. Uma sombra projetada com gradiente animado.

Combinar sombras

É possível combinar e sobrepor os modificadores dropShadow() e innerShadow() para criar vários efeitos. As seções a seguir mostram como produzir sombras neumórficas, neobrutalistas e realistas com essa técnica.

Criar sombras neumórficas

As sombras neumórficas são caracterizadas por uma aparência suave que surge organicamente do plano de fundo. Para criar sombras neumórficas, faça o seguinte:

  1. Use um elemento que tenha as mesmas cores do plano de fundo.
  2. Aplique duas sombras projetadas leves e opostas: uma sombra clara em um canto e uma sombra escura no canto oposto.

O snippet a seguir sobrepõe dois modificadores dropShadow() para criar o efeito neumórfico:

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

Uma forma retangular branca com um efeito neumórfico em um fundo branco.
Figura 6. Um efeito de sombra neumórfico.

Criar sombras neobrutalistas

O estilo neobrutalista apresenta layouts em blocos de alto contraste, cores vivas e bordas grossas. Para criar esse efeito, use um dropShadow() com desfoque zero e um deslocamento distinto, conforme mostrado no snippet abaixo:

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

Uma borda vermelha ao redor de um retângulo branco com uma sombra azul em um fundo amarelo.
Figura 7. Um efeito de sombra neobrutalista.

Criar sombras realistas

As sombras realistas imitam as sombras do mundo físico: elas parecem iluminadas por uma fonte de luz principal, resultando em uma sombra direta e outra mais difusa. É possível empilhar várias instâncias de dropShadow() e innerShadow() com propriedades diferentes para recriar efeitos de sombra realistas, como mostrado no snippet a seguir:

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

Pontos principais sobre o código

  • Dois modificadores dropShadow() encadeados com propriedades distintas são aplicados, seguidos por um modificador background().
  • Os modificadores innerShadow() encadeados são aplicados para criar o efeito de borda metálica ao redor do componente.

Resultado

O snippet de código anterior produz o seguinte:

Uma sombra branca realista ao redor de uma forma preta arredondada.
Figura 8. Um efeito de sombra realista.