Dodawanie cieni w Compose

Cienie wizualnie uatrakcyjniają interfejs, wskazują użytkownikom interaktywność i zapewniają natychmiastowe informacje zwrotne o działaniach użytkownika. Compose udostępnia kilka sposobów na dodanie cieni do aplikacji:

  • Modifier.shadow(): tworzy cień oparty na wysokości za elementem kompozycyjnym, który jest zgodny ze wskazówkami Material Design.
  • Modifier.dropShadow(): tworzy cień, który można dostosować i który pojawia się za elementem kompozycyjnym, dzięki czemu wydaje się on uniesiony.
  • Modifier.innerShadow(): tworzy cień wewnątrz obramowania elementu kompozycyjnego, dzięki czemu wydaje się on wciśnięty w powierzchnię za nim.

Modyfikator Modifier.shadow() nadaje się do tworzenia podstawowych cieni, a modyfikatory dropShadow() i innerShadow() zapewniają bardziej szczegółową kontrolę i precyzję renderowania cieni.

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

Tworzenie podstawowych cieni

Modifier.shadow() tworzy podstawowy cień zgodnie ze wskazówkami Material Design, który symuluje źródło światła nad obiektem. Głębokość cienia zależy od wartości elevation, a rzucany cień jest przycinany do kształtu elementu kompozycyjnego.

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

Zastosuj cienie.

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

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: określa rozszerzanie lub zmniejszanie 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 kształtów Material Expressive.

Aby zaimplementować podstawowy cień, dodaj modyfikator dropShadow() do łańcucha funkcji kompozycyjnych, podając promień, kolor i rozprzestrzenianie. Zwróć uwagę, że tło purpleColor, które pojawia się nad cieniem, 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;
    • rozprzestrzenianie 6.dp, które zwiększa rozmiar cienia i sprawia, że jest on większy niż pole, które go rzuca;
    • wartość alfa 0.5f, dzięki czemu cień jest półprzezroczysty;
  • Po zdefiniowaniu cienia,Zastosowano modyfikator background().
    • Box jest wypełniony białym kolorem.
    • Tło jest przycięte do tego samego kształtu zaokrąglonego prostokąta co cień.

Wynik

Szary cień rzucany wokół białego prostokątnego kształtu.
Rysunek 2. Z cieniem wokół kształtu.

Implementowanie cieni wewnętrznych

Aby utworzyć efekt odwrotny do dropShadow(), użyj Modifier.innerShadow(), który tworzy iluzję, że element jest zagłębiony lub wciśnięty w powierzchnię pod nim.

Kolejność jest ważna podczas tworzenia wewnętrznych cieni. Modyfikator innerShadow() jest rysowany na treści. Aby upewnić się, że cień jest widoczny, zwykle wykonujesz te czynności:

  1. Narysuj zawartość tła.
  2. Aby uzyskać efekt wklęsłości, zastosuj modyfikator innerShadow().

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

Poniższy przykład pokazuje zastosowanie elementu 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ątnym kształcie.
Rysunek 3. Zastosowanie Modifier.innerShadow() na prostokącie z zaokrąglonymi rogami.

Animuj cienie podczas interakcji użytkownika

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

Ten kod tworzy efekt „naciśnięcia” z cieniem (złudzenie, ż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ątkowe i końcowe parametrów, które mają być animowane po naciśnięciu, za pomocą transition.animateColor i transition.animateFloat.
  • Używa updateTransition i udostępnia mu wybraną wartość targetState (targetState = isPressed), aby sprawdzić, czy wszystkie animacje są zsynchronizowane. Za każdym razem, gdy zmienia się isPressed, 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 płynność przejścia. Określa tween (skrót od „in-between”, czyli „pomiędzy”) o czasie trwania 400 milisekund i krzywą EaseInOut, co oznacza, że animacja zaczyna się powoli, przyspiesza w środku i zwalnia na końcu.
  • Definiuje Box z łańcuchem funkcji modyfikujących, które stosują wszystkie animowane właściwości, aby utworzyć element wizualny, 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 alfa są powiązane z animowanymi wartościami (blueDropShadow itd.) i tworzą początkowy efekt podniesienia.
    • .innerShadow(): na tle są rysowane 2 cienie wewnętrzne. Ich właściwości są połączone z innym zestawem animowanych wartości (innerShadowColor1 itd.) i tworzą wygląd z wcięciem.

Wynik

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

Tworzenie cieni gradientowych

Cienie nie ograniczają się do jednolitych kolorów. Interfejs API cieni akceptuje Brush, co 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) nadaje cieniowi kolor gradientu, który przechodzi przez listę wstępnie zdefiniowanych colors, tworząc efekt tęczy.

Wynik

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

Rysunek 5. Animowany cień z gradientem.

Łączenie cieni

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

Tworzenie neumorficznych cieni

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

  1. używać elementu, który ma takie same kolory jak tło;
  2. Zastosuj 2 delikatne, przeciwstawne cienie: jasny w jednym rogu i ciemny w przeciwnym.

Poniższy fragment kodu nakłada na siebie 2 modyfikatory dropShadow(), aby utworzyć efekt neumorficzny:

@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ątny kształt z efektem neumorficznym na białym tle.
Rysunek 6. Efekt cienia neumorficznego.

Tworzenie neobrutalistycznych cieni

Styl neobrutalistyczny charakteryzuje się wysokim kontrastem, blokowymi układami, ż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 kodu:

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

Tworzenie realistycznych cieni

Realistyczne cienie naśladują cienie w świecie fizycznym – wyglądają, jakby były oświetlone przez główne źródło światła, co daje zarówno bezpośredni, jak i bardziej rozproszony cień. Możesz ułożyć kilka instancji dropShadow() i innerShadow() z różnymi właściwościami, 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

  • Zastosowano 2 połączone modyfikatory dropShadow() z różnymi właściwościami, a następnie modyfikator background().
  • Aby uzyskać efekt metalicznej krawędzi wokół komponentu, zastosowano połączone modyfikatory innerShadow().

Wynik

Poprzedni fragment kodu generuje te dane:

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