Thêm hiệu ứng đổ bóng trong Compose

Hiệu ứng đổ bóng giúp nâng cao giao diện người dùng một cách trực quan, cho người dùng biết khả năng tương tác và cung cấp phản hồi tức thì về hành động của người dùng. Compose cung cấp một số cách để kết hợp bóng đổ vào ứng dụng:

  • Modifier.shadow(): Tạo bóng dựa trên độ cao phía sau một composable tuân thủ các nguyên tắc của Material Design.
  • Modifier.dropShadow(): Tạo bóng có thể tuỳ chỉnh xuất hiện phía sau một composable, làm cho thành phần đó xuất hiện cao hơn.
  • Modifier.innerShadow(): Tạo bóng bên trong đường viền của một composable, làm cho thành phần đó có vẻ như được nhấn vào bề mặt phía sau.

Modifier.shadow() phù hợp để tạo bóng cơ bản, trong khi đối tượng sửa đổi dropShadow() và innerShadow() giúp bạn kiểm soát chi tiết và chính xác hơn đối với việc kết xuất bóng.

Trang này mô tả cách triển khai từng đối tượng sửa đổi này, bao gồm cả cách tạo hiệu ứng động cho bóng đổ khi người dùng tương tác và cách liên kết các đối tượng sửa đổi innerShadow() và dropShadow() để tạo bóng đổ chuyển màu, bóng đổ tân cổ điển, v.v.

Tạo bóng cơ bản

Modifier.shadow() tạo bóng cơ bản theo nguyên tắc Material Design mô phỏng nguồn sáng trên cao. Độ sâu của bóng dựa trên giá trị elevation và bóng đổ được cắt theo hình dạng của thành phần kết hợp.

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

Bóng màu xám đổ xung quanh hình chữ nhật màu trắng.
Hình 1. Bóng dựa trên độ cao được tạo bằng Modifier.shadow().

Triển khai hiệu ứng bóng đổ

Sử dụng đối tượng sửa đổi dropShadow() để vẽ bóng chính xác phía sau nội dung, giúp phần tử có vẻ như được nâng lên.

Bạn có thể kiểm soát các khía cạnh chính sau đây thông qua tham số Shadow:

  • radius: Xác định độ mềm và độ khuếch tán của hiệu ứng làm mờ.
  • color: Xác định màu của sắc thái.
  • offset: Đặt hình dạng của bóng dọc theo trục x và y.
  • spread: Kiểm soát việc mở rộng hoặc thu hẹp hình dạng của bóng.

Ngoài ra, tham số shape xác định hình dạng tổng thể của bóng. Thành phần này có thể sử dụng mọi hình học trong gói androidx.compose.foundation.shape, cũng như Hình dạng biểu cảm của Material.

Để triển khai hiệu ứng bóng đổ cơ bản, hãy thêm modifier dropShadow() vào chuỗi composable, cung cấp bán kính, màu sắc và độ lan. Lưu ý rằng nền purpleColor xuất hiện ở đầu bóng được vẽ sau đối tượng sửa đổi 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
            )
        }
    }
}

Các điểm chính về mã

  • Đối tượng sửa đổi dropShadow() được áp dụng cho Box bên trong. Bóng có các đặc điểm sau:
    • Hình chữ nhật góc tròn (RoundedCornerShape(20.dp))
    • Bán kính làm mờ là 10.dp, giúp các cạnh mềm mại và khuếch tán
    • Độ lan rộng 6.dp, giúp mở rộng kích thước của bóng và làm cho bóng lớn hơn hộp tạo bóng
    • Giá trị alpha là 0.5f, làm cho bóng đổ trở nên bán trong suốt
  • Sau khi xác định bóng đổ, .Đã áp dụng đối tượng sửa đổi background().
    • Box được tô màu trắng.
    • Nền được cắt theo cùng hình chữ nhật bo tròn như bóng.

Kết quả

Hiệu ứng bóng đổ màu xám xung quanh hình chữ nhật màu trắng.
Hình 2. Bóng đổ được vẽ xung quanh hình dạng.

Triển khai bóng bên trong

Để tạo hiệu ứng ngược với dropShadow(), hãy sử dụng Modifier.innerShadow() để tạo ảo giác rằng một phần tử bị lõm hoặc được nhấn vào bề mặt bên dưới.

Thứ tự là rất quan trọng khi tạo bóng bên trong. Đối tượng sửa đổi innerShadow() vẽ trên đầu nội dung. Để đảm bảo bóng đổ hiển thị, bạn thường thực hiện các bước sau:

  1. Vẽ nội dung nền.
  2. Áp dụng đối tượng sửa đổi innerShadow() để tạo giao diện lõm.

Nếu innerShadow() được đặt trước nền, thì nền sẽ được vẽ đè lên bóng, ẩn hoàn toàn bóng.

Ví dụ sau đây cho thấy cách áp dụng innerShadow() trên 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
            )
        }
    }
}

Bóng trong màu xám bên trong hình chữ nhật màu trắng.
Hình 3. Ứng dụng Modifier.innerShadow() trên hình chữ nhật có góc bo tròn.

Tạo hiệu ứng động cho bóng khi người dùng tương tác

Để bóng phản hồi với hoạt động tương tác của người dùng, bạn có thể tích hợp các thuộc tính bóng với API Ảnh động của Compose. Ví dụ: khi người dùng nhấn vào một nút, bóng đổ có thể thay đổi để cung cấp phản hồi tức thì bằng hình ảnh.

Mã sau đây tạo hiệu ứng "đã nhấn" bằng bóng đổ (tạo ảo giác rằng bề mặt đang được đẩy xuống màn hình):

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

Các điểm chính về mã

  • Khai báo trạng thái bắt đầu và kết thúc cho các tham số để tạo ảnh động khi nhấn bằng transition.animateColor và transition.animateFloat.
  • Sử dụng updateTransition và cung cấp targetState (targetState = isPressed) đã chọn để xác minh rằng tất cả ảnh động đều được đồng bộ hoá. Bất cứ khi nào isPressed thay đổi, đối tượng chuyển đổi sẽ tự động quản lý ảnh động của tất cả thuộc tính con từ giá trị hiện tại đến giá trị mục tiêu mới.
  • Xác định thông số kỹ thuật buttonPressAnimation, giúp kiểm soát thời gian và tốc độ chuyển đổi. Nó chỉ định một tween (viết tắt của in-between) với thời lượng 400 mili giây và một đường cong EaseInOut, tức là ảnh động bắt đầu chậm, tăng tốc ở giữa và chậm lại ở cuối.
  • Xác định Box bằng một chuỗi các hàm đối tượng sửa đổi áp dụng tất cả các thuộc tính ảnh động để tạo phần tử hình ảnh, bao gồm:
    • .clickable(): Một đối tượng sửa đổi giúp Box có thể tương tác.
    • .dropShadow(): Hai bóng đổ bên ngoài được áp dụng trước. Thuộc tính màu sắc và alpha của các thành phần này được liên kết với các giá trị ảnh động (blueDropShadow, v.v.) và tạo ra giao diện nổi lên ban đầu.
    • .innerShadow(): Hai bóng bên trong được vẽ lên trên nền. Các thuộc tính của chúng được liên kết với một tập hợp giá trị ảnh động khác (innerShadowColor1, v.v.) và tạo ra giao diện thụt lề.

Kết quả

Hình 4. Bóng đổ động khi người dùng nhấn.

Tạo bóng chuyển màu

Đổ bóng không chỉ giới hạn ở màu đồng nhất. API bóng chấp nhận Brush, cho phép bạn tạo bóng chuyển màu.

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

Các điểm chính về mã

  • dropShadow() thêm bóng phía sau hộp.
  • brush = Brush.sweepGradient(colors) tô màu bóng bằng hiệu ứng chuyển màu xoay vòng qua danh sách colors được xác định trước, tạo ra hiệu ứng giống như cầu vồng.

Kết quả

Bạn có thể sử dụng bút vẽ làm bóng để tạo hiệu ứng chuyển màu dropShadow() bằng ảnh động "thở":

Hình 5. Bóng đổ chuyển màu động.

Kết hợp bóng

Bạn có thể kết hợp và phân lớp các đối tượng sửa đổi dropShadow() và innerShadow() để tạo ra nhiều hiệu ứng. Các phần sau đây cho bạn biết cách tạo bóng đổ theo phong cách tân tối giản, tân thô mộc và chân thực bằng kỹ thuật này.

Tạo bóng neumorphic

Bóng đổ theo phong cách Neumorphic có đặc điểm là vẻ ngoài mềm mại, nổi lên một cách tự nhiên từ nền. Để tạo bóng neumorphic, hãy làm như sau:

  1. Sử dụng một phần tử có cùng màu với nền.
  2. Áp dụng hai bóng đổ mờ, đối lập: một bóng sáng ở một góc và một bóng tối ở góc đối diện.

Đoạn mã sau đây phân lớp hai đối tượng sửa đổi dropShadow() để tạo hiệu ứng tân cổ điển:

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

Hình chữ nhật màu trắng có hiệu ứng neumorphic trên nền trắng.
Hình 6. Hiệu ứng đổ bóng tân cổ điển.

Tạo bóng theo phong cách tân thô mộc

Phong cách tân thô mộc thể hiện bố cục khối, độ tương phản cao, màu sắc sống động và đường viền dày. Để tạo hiệu ứng này, hãy sử dụng dropShadow() không có độ mờ và có độ lệch riêng biệt, như trong đoạn mã sau:

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

Đường viền màu đỏ xung quanh hình chữ nhật màu trắng có bóng màu xanh dương trên nền màu vàng.
Hình 7. Hiệu ứng bóng theo phong cách tân thô mộc.

Tạo bóng chân thực

Bóng đổ chân thực mô phỏng bóng đổ trong thế giới thực – chúng xuất hiện dưới ánh sáng của một nguồn sáng chính, tạo ra cả bóng đổ trực tiếp và bóng đổ khuếch tán hơn. Bạn có thể xếp chồng nhiều thực thể dropShadow() và innerShadow() có các thuộc tính khác nhau để tạo lại hiệu ứng đổ bóng chân thực, như trong đoạn mã sau:

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

Các điểm chính về mã

  • Hai đối tượng sửa đổi dropShadow() được liên kết với các thuộc tính riêng biệt được áp dụng, theo sau là đối tượng sửa đổi background().
  • Các đối tượng sửa đổi innerShadow() theo chuỗi được áp dụng để tạo hiệu ứng viền kim loại xung quanh cạnh của thành phần.

Kết quả

Đoạn mã trước đó tạo ra kết quả sau:

Bóng đổ chân thực màu trắng xung quanh một hình dạng bo tròn màu đen.
Hình 8. Hiệu ứng đổ bóng chân thực.