Menambahkan bayangan di Compose

Bayangan meningkatkan UI Anda secara visual, menunjukkan interaktivitas kepada pengguna, dan memberikan masukan langsung tentang tindakan pengguna. Compose menyediakan beberapa cara untuk menggabungkan bayangan ke dalam aplikasi Anda:

  • Modifier.shadow(): Membuat bayangan berbasis elevasi di belakang composable yang sesuai dengan panduan Desain Material.
  • Modifier.dropShadow(): Membuat bayangan yang dapat disesuaikan yang muncul di belakang composable, sehingga membuatnya tampak lebih tinggi.
  • Modifier.innerShadow(): Membuat bayangan di dalam batas composable, sehingga tampak seperti ditekan ke permukaan di belakangnya.

Modifier.shadow() cocok untuk membuat bayangan dasar, sedangkan pengubah dropShadow() dan innerShadow() menawarkan kontrol yang lebih terperinci dan presisi atas rendering bayangan.

Halaman ini menjelaskan cara menerapkan setiap pengubah ini, termasuk cara menganimasikan bayangan saat interaksi pengguna dan cara merangkai pengubah innerShadow() dan dropShadow() untuk membuat bayangan gradien, bayangan neumorfik, dan lainnya.

Membuat bayangan dasar

Modifier.shadow() membuat bayangan dasar mengikuti panduan Desain Material yang menyimulasikan sumber cahaya di atas kepala. Kedalaman bayangan didasarkan pada nilai elevation, dan bayangan yang dihasilkan dipangkas sesuai bentuk composable.

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

Bayangan abu-abu di sekitar bentuk persegi panjang putih.
Gambar 1. Bayangan berbasis elevasi yang dibuat dengan Modifier.shadow().

Menerapkan drop shadow

Gunakan pengubah dropShadow() untuk menggambar bayangan yang akurat di belakang konten, yang membuat elemen tampak ditinggikan.

Anda dapat mengontrol aspek utama berikut melalui parameter Shadow:

  • radius: Menentukan kelembutan dan difusi blur Anda.
  • color: Menentukan warna tint.
  • offset: Memosisikan geometri bayangan di sepanjang sumbu x dan y.
  • spread: Mengontrol perluasan atau penyusutan geometri bayangan.

Selain itu, parameter shape menentukan bentuk keseluruhan bayangan. Fungsi ini dapat menggunakan geometri apa pun dari paket androidx.compose.foundation.shape, serta bentuk Material Ekspresif.

Untuk menerapkan Drop Shadow dasar, tambahkan pengubah dropShadow() ke rantai composable, dengan memberikan radius, warna, dan penyebaran. Perhatikan bahwa latar belakang purpleColor yang muncul di atas bayangan digambar setelah pengubah 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
            )
        }
    }
}

Poin penting tentang kode

  • Pengubah dropShadow() diterapkan ke Box dalam. Bayangan memiliki karakteristik berikut:
    • Bentuk persegi panjang bersudut bulat (RoundedCornerShape(20.dp))
    • Radius blur 10.dp, membuat tepi menjadi lembut dan tersebar
    • Penyebaran 6.dp, yang memperluas ukuran bayangan dan membuatnya lebih besar dari kotak yang memproyeksikannya
    • Alfa 0.5f, yang membuat bayangan menjadi semi-transparan
  • Setelah bayangan ditentukan, .Pengubah background() diterapkan.
    • Box diisi dengan warna putih.
    • Latar belakang dipotong ke bentuk persegi panjang bersudut lengkung yang sama dengan bayangan.

Hasil

Drop shadow abu-abu di sekitar bentuk persegi panjang putih.
Gambar 2. Bayangan jatuh yang digambar di sekeliling bentuk.

Menerapkan bayangan dalam

Untuk membuat efek terbalik pada dropShadow(), gunakan Modifier.innerShadow(), yang menciptakan ilusi bahwa elemen tersembunyi atau ditekan ke permukaan di bawahnya.

Urutan sangat penting saat membuat bayangan dalam. Pengubah innerShadow() digambar di atas konten. Untuk memastikan bayangan terlihat, Anda biasanya melakukan langkah-langkah berikut:

  1. Gambar konten latar belakang Anda.
  2. Terapkan pengubah innerShadow() untuk membuat tampilan cekung.

Jika innerShadow() ditempatkan sebelum latar belakang, latar belakang akan digambar di atas bayangan, sehingga menyembunyikannya sepenuhnya.

Contoh berikut menunjukkan penerapan innerShadow() pada 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
            )
        }
    }
}

Bayangan dalam berwarna abu-abu di dalam bentuk persegi panjang putih.
Gambar 3. Penerapan Modifier.innerShadow() pada persegi panjang sudut membulat.

Menganimasikan bayangan pada interaksi pengguna

Agar bayangan merespons interaksi pengguna, Anda dapat mengintegrasikan properti bayangan dengan API animasi Compose. Misalnya, saat pengguna menekan tombol, bayangan dapat berubah untuk memberikan masukan visual instan.

Kode berikut membuat efek "ditekan" dengan bayangan (ilusi bahwa permukaan ditekan ke dalam layar):

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

Poin penting tentang kode

  • Mendeklarasikan status awal dan akhir untuk parameter yang akan dianimasikan saat ditekan dengan transition.animateColor dan transition.animateFloat.
  • Menggunakan updateTransition dan menyediakannya dengan targetState (targetState = isPressed) yang dipilih untuk memverifikasi bahwa semua animasi disinkronkan. Setiap kali isPressed berubah, objek transisi akan otomatis mengelola animasi semua properti turunan dari nilai saat ini ke nilai target baru.
  • Menentukan spesifikasi buttonPressAnimation, yang mengontrol pengaturan waktu dan easing transisi. Kode ini menentukan tween (singkatan dari in-between) dengan durasi 400 milidetik dan kurva EaseInOut, yang berarti animasi dimulai dengan lambat, dipercepat di tengah, dan diperlambat di akhir.
  • Menentukan Box dengan rantai fungsi pengubah yang menerapkan semua properti animasi untuk membuat elemen visual, termasuk hal berikut:
    • .clickable(): Pengubah yang membuat Box menjadi interaktif.
    • .dropShadow(): Dua drop shadow luar diterapkan terlebih dahulu. Properti warna dan alfanya ditautkan ke nilai animasi (blueDropShadow, dll.) dan membuat tampilan awal yang ditinggikan.
    • .innerShadow(): Dua bayangan dalam digambar di atas latar belakang. Propertinya ditautkan ke kumpulan nilai animasi lainnya (innerShadowColor1, dll.) dan membuat tampilan yang menjorok ke dalam.

Hasil

Gambar 4. Bayangan yang dianimasikan saat pengguna menekan.

Membuat bayangan gradien

Bayangan tidak terbatas pada warna solid. API bayangan menerima Brush, yang memungkinkan Anda membuat bayangan gradien.

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

Poin penting tentang kode

  • dropShadow() menambahkan bayangan di belakang kotak.
  • brush = Brush.sweepGradient(colors) mewarnai bayangan dengan gradien yang berputar melalui daftar colors yang telah ditentukan, sehingga menciptakan efek seperti pelangi.

Hasil

Anda dapat menggunakan kuas sebagai bayangan untuk membuat gradien dropShadow() dengan animasi "bernafas":

Gambar 5. Drop Shadow gradien animasi.

Menggabungkan bayangan

Anda dapat menggabungkan dan melapisi pengubah dropShadow() dan innerShadow() untuk membuat berbagai efek. Bagian berikut menunjukkan cara menghasilkan bayangan neumorphic, neobrutalist, dan realistis dengan teknik ini.

Membuat bayangan neumorfik

Bayangan neumorfik ditandai dengan tampilan lembut yang muncul secara organik dari latar belakang. Untuk membuat bayangan neumorphic, lakukan hal berikut:

  1. Gunakan elemen yang memiliki warna yang sama dengan latar belakangnya.
  2. Terapkan dua drop shadow yang samar dan berlawanan: bayangan terang ke satu sudut, dan bayangan gelap ke sudut yang berlawanan.

Cuplikan berikut melapisi dua pengubah dropShadow() untuk membuat efek neumorfik:

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

Bentuk persegi panjang putih dengan efek neumorfik pada latar belakang putih.
Gambar 6. Efek bayangan neumorfik.

Membuat bayangan neobrutalis

Gaya neobrutalis menampilkan tata letak kotak-kotak dengan kontras tinggi, warna-warna cerah, dan batas tebal. Untuk membuat efek ini, gunakan dropShadow() dengan blur nol dan offset yang berbeda, seperti yang ditunjukkan dalam cuplikan berikut:

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

Bingkai merah di sekeliling persegi panjang putih dengan bayangan biru di atas latar belakang kuning.
Gambar 7. Efek bayangan neobrutalis.

Membuat bayangan realistis

Bayangan realistis meniru bayangan di dunia fisik—bayangan tersebut tampak diterangi oleh sumber cahaya utama, sehingga menghasilkan bayangan langsung dan bayangan yang lebih menyebar. Anda dapat menumpuk beberapa instance dropShadow() dan innerShadow() dengan properti yang berbeda untuk membuat ulang efek bayangan yang realistis, seperti yang ditunjukkan dalam cuplikan berikut:

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

Poin penting tentang kode

  • Dua pengubah dropShadow() berantai dengan properti yang berbeda diterapkan, diikuti dengan pengubah background().
  • Pengubah innerShadow() berantai diterapkan untuk membuat efek tepi metalik di sekitar tepi komponen.

Hasil

Cuplikan kode sebelumnya menghasilkan hal berikut:

Bayangan putih realistis di sekitar bentuk bulat hitam.
Gambar 8. Efek bayangan yang realistis.