Dodawanie cieni w Compose

Cienie wizualnie podnoszą jakość interfejsu, wskazują użytkownikom interaktywność i zapewniają natychmiastową informację zwrotną o działaniach użytkownika. Compose udostępnia kilka sposobów na dodanie cieni do aplikacji:

  • Modifier.shadow(): tworzy cień za elementem kompozycyjnym na podstawie wysokości, zgodnie z wytycznymi Material Design.
  • Modifier.dropShadow(): tworzy konfigurowalny cień, który pojawia się za elementem kompozycyjnym, dzięki czemu wygląda on na podniesiony.
  • Modifier.innerShadow(): tworzy cień wewnątrz obramowania elementu kompozycyjnego, dzięki czemu wygląda on na wciśnięty w powierzchnię za nim.

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

Na tej stronie opisujemy, jak zaimplementować każdy z tych modyfikatorów, w tym jak animować cienie w reakcji na interakcję użytkownika oraz jak łączyć modyfikatory innerShadow() i dropShadow() w celu tworzenia cieni gradientowych, cieni neumorficznych i innych.

Tworzenie podstawowych cieni

Modifier.shadow() tworzy podstawowy cień zgodny z wytycznymi Material Design, który symuluje źródło światła z góry. Głębokość cienia zależy od wartości elevation, a cień rzucany 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ń na podstawie wysokości utworzony za pomocą Modifier.shadow().

Implementowanie cieni rzucanych

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 się geometrii cienia.

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

Aby zaimplementować podstawowy cień rzucany, dodaj modyfikator dropShadow() do łańcucha elementów kompozycyjnych, podając promień, kolor i rozproszenie. Pamiętaj, że tło purpleColor widoczne nad cieniem jest rysowane po zastosowaniu modyfikatora 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:
    • Kształt zaokrąglonego prostokąta (RoundedCornerShape(20.dp))
    • Promień rozmycia 10.dp, dzięki czemu krawędzie są miękkie i rozproszone
    • Rozproszenie 6.dp, które powiększa cień i sprawia, że jest on większy niż rzucający go element Box
    • Alfa 0.5f, dzięki czemu cień jest półprzezroczysty
  • Po zdefiniowaniu cienia stosowany jest modyfikatorbackground() modyfikator jest zastosowany.
    • Element Box jest wypełniony białym kolorem.
    • Tło jest przycinane do tego samego kształtu zaokrąglonego prostokąta co cień.

Wynik

Szary cień padający na biały prostokąt.
Rysunek 2. Cień rzucany narysowany wokół kształtu.

Implementowanie cieni wewnętrznych

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

Podczas tworzenia cieni wewnętrznych kolejność ma znaczenie. Modyfikator innerShadow() rysuje się nad treścią. Aby cień był widoczny, zwykle wykonujesz te czynności:

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

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

Ten przykład pokazuje zastosowanie innerShadow() na 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 w reakcji na interakcję 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 np. przycisk, cień może się zmienić, aby zapewnić natychmiastową informację zwrotną.

Ten kod tworzy efekt „naciśnięcia” z cieniem (iluzja, że powierzchnia jest wciśnięta 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.animateColor i transition.animateFloat.
  • Używa updateTransition i podaje wybrany 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 wygładzanie 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 element Box z łańcuchem funkcji modyfikatora, 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 rzucane. Ich właściwości koloru i alfa są powiązane z animowanymi wartościami (blueDropShadow itp.) i tworzą początkowy podniesiony wygląd.
    • .innerShadow(): 2 cienie wewnętrzne są rysowane nad tłem. Ich właściwości są powiązane z innym zestawem animowanych wartości (innerShadowColor1 itp.) i tworzą wcięty wygląd.

Wynik

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

Tworzenie cieni gradientowych

Cienie nie muszą być jednolite. Interfejs API cieni akceptuje Brush, który 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 elementem Box.
  • brush = Brush.sweepGradient(colors) koloruje cień gradientem, który obraca się wokół 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 gradientowy cień rzucany.

Łączenie cieni

Możesz łączyć i nakładać na siebie modyfikatory dropShadow() i 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 neumorficznych

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

  1. Użyj elementu, który ma te same kolory co tło.
  2. Zastosuj 2 słabe, przeciwstawne cienie rzucane: jasny cień w jednym rogu i ciemny cień w rogu przeciwnym.

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

Tworzenie cieni neobrutalistycznych

Styl neobrutalistyczny charakteryzuje się układami blokowymi o wysokim kontraście, żywymi kolorami i grubymi obramowaniami. Aby utworzyć 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 neobrutalistycznego.

Tworzenie cieni realistycznych

Cienie realistyczne naśladują 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 cienia bezpośredniego, jak i bardziej rozproszonego. Aby odtworzyć realistyczne efekty cienia, możesz ułożyć w stos kilka instancji dropShadow() i innerShadow() z różnymi właściwościami, 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

  • Stosowane są 2 połączone modyfikatory dropShadow() z różnymi właściwościami, a następnie modyfikator background().
  • Stosowane są połączone modyfikatory innerShadow(), aby utworzyć efekt metalowej krawędzi wokół krawędzi komponentu.

Wynik

Poprzedni fragment kodu daje ten efekt:

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