Dodawanie cieni w Compose

Cienie wizualnie podnoszą poziom interfejsu, wskazują użytkownikom interaktywność i zapewniają natychmiastową reakcję na ich działania. Compose udostępnia kilka sposobów dodawania cieni do aplikacji:

  • Modifier.shadow(): tworzy cień na podstawie wysokości za elementem kompozycyjnym, który jest zgodny z wytycznymi Material Design.
  • Modifier.dropShadow(): tworzy cień z możliwością dostosowania, który pojawia się za elementem kompozycyjnym, dzięki czemu wygląda on na podniesiony.
  • Modifier.innerShadow(): tworzy cień wewnątrz obramowania komponentu kompozycyjnego, dzięki czemu wygląda on, jakby był wciśnięty w powierzchnię za nim.

Modifier.shadow() nadaje się do tworzenia podstawowych cieni, a modyfikatory dropShadowinnerShadow zapewniają większą kontrolę i precyzję renderowania cieni.

Na tej stronie opisujemy, jak wdrożyć każdy z tych modyfikatorów, w tym jak animować cienie w reakcji na interakcję użytkownika oraz jak łączyć modyfikatory innerShadow()dropShadow(), aby tworzyć cienie gradientowe, cienie neumorficzne i inne.

Tworzenie podstawowych cieni

Modifier.shadow() tworzy podstawowy cień zgodnie z wytycznymi Material Design, który symuluje źródło światła z góry. Głębokość cienia jest określana na podstawie wartości elevation, a cień rzucany jest przycinany do kształtu komponentu.

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

Szary cień wokół białego prostokąta.
Rysunek 1. Cień oparty na wysokości utworzony za pomocą Modifier.shadow.

Implementacja cieni

Użyj modyfikatora dropShadow(), aby narysować dokładny cień za treścią, dzięki czemu element będzie wyglądał na podniesiony.

Za pomocą parametru Shadow możesz kontrolować te kluczowe aspekty:

  • radius: określa miękkość i rozproszenie rozmycia.
  • color: określa kolor odcienia.
  • offset: określa położenie geometrii cienia wzdłuż osi x i y.
  • spread: kontroluje rozszerzanie lub kurczenie geometrii cienia.

Dodatkowo parametr shape określa ogólny kształt cienia. Może używać dowolnej geometrii z pakietu androidx.compose.foundation.shape, a także wyrazistych kształtów Material.

Aby zaimplementować podstawowy cień, dodaj modyfikator dropShadow() do łańcucha funkcji kompozycyjnych, podając promień, kolor i rozmiar. Pamiętaj, że tło purpleColor, które pojawia się na cieniu, jest rysowane po modyfikatorze 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
            )
        }
    }
}

Najważniejsze informacje o kodzie

  • Modyfikator dropShadow() jest stosowany do wewnętrznego elementu Box. Cień ma te cechy:
    • Zaokrąglony prostokąt (RoundedCornerShape(20.dp))
    • Promień rozmycia 10.dp, dzięki czemu krawędzie są miękkie i rozproszone.
    • Rozmiar 6.dp, który powiększa cień i sprawia, że jest on większy niż element, który go rzuca.
    • wartość alfa 0.5f, dzięki czemu cień jest półprzezroczysty;
  • Po zdefiniowaniu cienia .Zastosowano modyfikator background().
    • Ikona Box jest wypełniona białym kolorem.
    • Tło jest przycięte do tego samego zaokrąglonego prostokąta co cień.

Wynik

Szary cień wokół białego prostokąta.
Rysunek 2. Cień narysowany wokół kształtu.

Implementowanie cieni wewnętrznych

Aby uzyskać efekt odwrotny do dropShadow, użyj Modifier.innerShadow(), który sprawia, że element wydaje się wklęsły lub wciśnięty w powierzchnię.

Kolejność ma znaczenie podczas tworzenia wewnętrznych cieni. Cień wewnętrzny jest rysowany nad treścią, więc zwykle należy wykonać te czynności:

  1. Narysuj zawartość tła.
  2. Zastosuj modyfikator innerShadow(), aby uzyskać wklęsły wygląd.

Jeśli symbol innerShadow() zostanie umieszczony przed tłem, tło zostanie narysowane nad cieniem, całkowicie go zasłaniając.

Poniższy przykład pokazuje zastosowanie innerShadow() w przypadku 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
            )
        }
    }
}

Szary cień wewnętrzny w białym prostokącie.
Rysunek 3. Zastosowanie Modifier.innerShadow() na prostokącie z zaokrąglonymi rogami.

Animowanie cieni podczas interakcji użytkownika

Aby cienie reagowały na interakcje użytkownika, możesz zintegrować właściwości cienia z interfejsami API animacji Compose. Gdy użytkownik naciśnie przycisk, cień może się zmienić, aby zapewnić natychmiastową informację wizualną.

Poniższy kod tworzy efekt „naciśnięcia” z cieniem (iluzja, że powierzchnia jest wciskana w ekran):

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

Najważniejsze informacje o kodzie

  • Deklaruje stany początkowy i końcowy parametrów, które mają być animowane po naciśnięciu, za pomocą transition.animateColortransition.animateFloat.
  • Używa updateTransition i przekazuje do niego wybrane targetState (targetState = isPressed), aby sprawdzić, czy wszystkie animacje są zsynchronizowane. Gdy isPressed się zmieni, obiekt przejścia automatycznie zarządza animacją wszystkich właściwości podrzędnych od ich bieżących wartości do nowych wartości docelowych.
  • Definiuje specyfikację buttonPressAnimation, która kontroluje czas i przebieg przejścia. Określa tween (skrót od „in-between”) o czasie trwania 400 milisekund i krzywej EaseInOut, co oznacza, że animacja zaczyna się powoli, przyspiesza w środku i zwalnia na końcu.
  • Definiuje Box z ciągiem funkcji modyfikujących, które stosują wszystkie animowane właściwości do utworzenia elementu wizualnego, w tym:
    • .clickable(): modyfikator, który sprawia, że element Box jest interaktywny.
    • .dropShadow(): najpierw stosowane są 2 zewnętrzne cienie. Ich właściwości koloru i wartości alfa są połączone z wartościami animowanymi (blueDropShadow itp.) i tworzą początkowy efekt wypukłości.
    • .innerShadow(): na tle są rysowane 2 cienie wewnętrzne. Ich właściwości są połączone z innym zestawem animowanych wartości (innerShadowColor1 itp.) i tworzą wcięcie.

Wynik

Rysunek 4. Cień animowany po naciśnięciu przez użytkownika.

Tworzenie cieni gradientowych

Cienie nie muszą być jednolite. Interfejs Shadow API akceptuje wartość Brush, która umożliwia tworzenie cieni gradientowych.

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

Najważniejsze informacje o kodzie

  • dropShadow() dodaje cień za polem.
  • brush = Brush.sweepGradient(colors) koloruje cień gradientem, który obraca się w obrębie listy predefiniowanych colors, tworząc efekt tęczy.

Wynik

Możesz użyć pędzla jako cienia, aby utworzyć gradient dropShadow() z animacją „oddechu”:

Rysunek 5. Animowany cień z gradientem.

Łączenie cieni

Możesz łączyć i nakładać na siebie modyfikatory dropShadow()innerShadow(), aby tworzyć różne efekty. Z sekcji poniżej dowiesz się, jak za pomocą tej techniki tworzyć cienie neumorficzne, neobrutalistyczne i realistyczne.

Tworzenie cieni w stylu neomorficznym

Cienie neumorficzne charakteryzują się miękkim wyglądem, który naturalnie wyłania się z tła. Aby utworzyć cienie neumorficzne:

  1. Używaj elementu, który ma takie same kolory jak tło.
  2. Zastosuj dwa słabe, przeciwstawne cienie: jasny w jednym rogu i ciemny w przeciwnym.

Poniższy fragment kodu zawiera 2 modyfikatory dropShadow(), które tworzą efekt neomorficzny:

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

Biały prostokąt z efektem neumorficznym na białym tle.
Rysunek 6. Efekt cienia w stylu neumorficznym.

Tworzenie cieni w stylu neobrutalizmu

Styl neobrutalistyczny charakteryzuje się układami blokowymi o wysokim kontraście, żywymi kolorami i grubymi obramowaniami. Aby uzyskać ten efekt, użyj dropShadow() z zerowym rozmyciem i wyraźnym przesunięciem, jak pokazano w tym fragmencie:

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

Czerwona ramka wokół białego prostokąta z niebieskim cieniem na żółtym tle.
Rysunek 7. Efekt cienia w stylu neobrutalizmu.

Tworzenie realistycznych cieni

Realistyczne cienie imitują cienie w świecie fizycznym – wyglądają tak, jakby były oświetlone przez główne źródło światła, co powoduje powstanie zarówno bezpośredniego, jak i bardziej rozproszonego cienia. Możesz łączyć ze sobą kilka instancji dropShadow()innerShadow() o różnych właściwościach, aby odtworzyć realistyczne efekty cienia, jak pokazano w tym fragmencie kodu:

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

Najważniejsze informacje o kodzie

  • Zastosowane są 2 połączone modyfikatory dropShadow() o różnych właściwościach, a po nich modyfikator background.
  • Połączone modyfikatory innerShadow() są stosowane w celu uzyskania efektu metalowej obręczy wokół krawędzi komponentu.

Wynik

Poprzedni fragment kodu generuje te dane:

Biały realistyczny cień wokół czarnego zaokrąglonego kształtu.
Rysunek 8. realistyczny efekt cienia;