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 com base em elevação atrás de um elemento combinável que está de acordo com 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 na superfície atrás dele.

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

Esta página descreve 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 gradientes, sombras neomórficas e muito mais.

Criar sombras básicas

Modifier.shadow() cria uma sombra básica seguindo as diretrizes do Material Design (link em inglês) que simula uma fonte de luz de cima. A profundidade da sombra é baseada em um valor elevation, e a sombra projetada é cortada na 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 em torno 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 com 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, bem como as formas expressivas do Material.

Para implementar uma sombra projetada básica, adicione o modificador dropShadow() à sua cadeia de elementos combináveis, fornecendo o raio, a cor e a extensão. O plano de fundo purpleColor que aparece acima da sombra é desenhado depois do 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, deixando as bordas suaves e difusas
    • Uma propagação de 6.dp, que aumenta o tamanho da sombra e a torna maior do que a caixa que a projeta
    • Um alfa de 0.5f, tornando a sombra semitransparente
  • Depois que a sombra é definida, o .O modificador background() é aplicado.
    • O Box é preenchido com uma cor branca.
    • O plano de fundo é cortado no mesmo formato de retângulo arredondado da sombra.

Resultado

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

Implementar sombras internas

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

A ordem é importante ao criar sombras internas. A sombra interna é desenhada na parte de cima do conteúdo. Portanto, geralmente é necessário fazer o seguinte:

  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, ele 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 fazer com que as sombras respondam às interações do usuário, integre propriedades de sombra com as 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 "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 dos parâmetros a 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 facilidade da transição. Ele especifica um tween (abreviação de in-between) 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 diminui no final.
  • Define um Box com uma cadeia de funções modificadoras que aplicam todas as propriedades animadas para criar o elemento visual, incluindo o seguinte:
    • .clickable(): um modificador que torna o Box interativo.
    • .dropShadow(): duas sombras projetadas externas são aplicadas primeiro. As propriedades de cor e alfa sã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 animada quando o usuário pressiona.

Criar sombras gradientes

As sombras não estão limitadas a cores sólidas. A API Shadow aceita um Brush, que permite criar sombras gradientes.

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 semelhante a um arco-íris.

Resultado

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

Figura 5. Uma sombra projetada de gradiente animada.

Combinar sombras

Você pode combinar e criar camadas com os modificadores dropShadow() e innerShadow() para criar vários efeitos. As seções a seguir mostram como produzir sombras neomórficas, neobrutalistas e realistas com essa técnica.

Criar sombras neomórficas

As sombras neomórficas têm uma aparência suave que surge organicamente do plano de fundo. Para criar sombras neomó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 clara em um canto e uma escura no canto oposto.

O snippet a seguir sobrepõe dois modificadores dropShadow() para criar o efeito neomó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 neomórfico em um fundo branco.
Figura 6. Um efeito de sombra neomorfista.

Criar sombras neobrutalistas

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

@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 primária, resultando em uma sombra direta e outra mais difusa. É possível empilhar várias instâncias dropShadow() e innerShadow() com propriedades diferentes para recriar efeitos de sombra realistas, conforme 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.
  • Modificadores innerShadow() encadeados são aplicados para criar o efeito de borda metálica ao redor da borda do componente.

Resultado

O snippet de código anterior produz o seguinte:

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