เพิ่มเงาในฟีเจอร์เขียน

เงาช่วยยกระดับ UI ของคุณในเชิงภาพ บ่งบอกถึงการโต้ตอบกับผู้ใช้ และให้การตอบสนองต่อการดำเนินการของผู้ใช้ในทันที Compose มีวิธีต่างๆ ในการรวมเงาเข้ากับแอป ดังนี้

  • Modifier.shadow(): สร้างเงาตามระดับความสูงด้านหลัง Composable ที่เป็นไปตามหลักเกณฑ์ของ Material Design
  • Modifier.dropShadow(): สร้างเงาที่ปรับแต่งได้ซึ่งปรากฏด้านหลัง Composable เพื่อให้ดูเหมือนยกขึ้น
  • Modifier.innerShadow(): สร้างเงาภายในขอบเขตของ Composable ทำให้ดูเหมือนถูกกดลงไปในพื้นผิวด้านหลัง

Modifier.shadow() เหมาะสำหรับการสร้างเงาพื้นฐาน ส่วนตัวแก้ไข dropShadow() และ innerShadow() จะช่วยให้ควบคุมและแสดงผลเงาได้อย่างละเอียดยิ่งขึ้น

หน้านี้อธิบายวิธีใช้ตัวแก้ไขแต่ละรายการ รวมถึงวิธีทำให้เงาเคลื่อนไหวเมื่อผู้ใช้โต้ตอบ และวิธีเชื่อมโยงตัวแก้ไข innerShadow() และ dropShadow() เพื่อสร้างเงาไล่ระดับสี เงาแบบนิวอมอร์ฟิก และอื่นๆ

สร้างเงาพื้นฐาน

Modifier.shadow() จะสร้างเงาพื้นฐานตามหลักเกณฑ์ของ Material Design ที่จำลองแหล่งกำเนิดแสงเหนือศีรษะ ความลึกของเงาจะอิงตามค่า elevation และเงาที่ทอดจะถูกตัดให้เป็นรูปร่างของ 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)
        )
    }
}

เงาสีเทาที่ทอดรอบรูปทรงสี่เหลี่ยมผืนผ้าสีขาว
รูปที่ 1 เงาตามระดับความสูงที่สร้างด้วย Modifier.shadow()

ใช้เงาตกกระทบ

ใช้ตัวแก้ไข dropShadow() เพื่อวาดเงาที่แม่นยำด้านหลังเนื้อหา ซึ่งจะทำให้องค์ประกอบดูนูนขึ้นมา

คุณควบคุมลักษณะสำคัญต่อไปนี้ได้ผ่านพารามิเตอร์ Shadow

  • radius: กำหนดความนุ่มนวลและการกระจายของภาพเบลอ
  • color: กำหนดสีของโทนสี
  • offset: จัดตำแหน่งเรขาคณิตของเงาตามแกน x และ y
  • spread: ควบคุมการขยายหรือหดตัวของเรขาคณิตของเงา

นอกจากนี้ พารามิเตอร์ shape จะกำหนดรูปร่างโดยรวมของเงา โดยสามารถใช้รูปทรงเรขาคณิตใดก็ได้จากแพ็กเกจ androidx.compose.foundation.shape รวมถึงรูปทรง Material Expressive

หากต้องการใช้เงาตกกระทบพื้นฐาน ให้เพิ่มตัวแก้ไข dropShadow() ลงในเชน Composable โดยระบุรัศมี สี และการกระจาย โปรดทราบว่าระบบจะวาดพื้นหลัง purpleColor ที่ปรากฏด้านบนของเงาหลังจากตัวปรับแต่ง 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
            )
        }
    }
}

ประเด็นสำคัญเกี่ยวกับโค้ด

  • ระบบจะใช้ตัวแก้ไข dropShadow() กับ Box ด้านใน เงาจะมีลักษณะดังต่อไปนี้
    • รูปทรงสี่เหลี่ยมผืนผ้ามุมมน (RoundedCornerShape(20.dp))
    • รัศมีการเบลอ 10.dp ทำให้ขอบนุ่มนวลและกระจายตัว
    • การกระจาย 6.dp ซึ่งจะขยายขนาดของเงาและทำให้เงาใหญ่กว่ากล่องที่ทอดเงา
    • อัลฟา 0.5f ทำให้เงากึ่งโปร่งใส
  • หลังจากกำหนดเงาแล้ว ให้ทำดังนี้ใช้ตัวปรับแต่ง background() แล้ว
    • Boxจะเต็มไปด้วยสีขาว
    • พื้นหลังจะถูกตัดให้เป็นรูปสี่เหลี่ยมผืนผ้าขอบมนแบบเดียวกับเงา

ผลลัพธ์

เงาตกกระทบสีเทารอบรูปทรงสี่เหลี่ยมผืนผ้าสีขาว
รูปที่ 2 เงาตกกระทบที่วาดรอบรูปร่าง

ใช้เงาด้านใน

หากต้องการสร้างเอฟเฟกต์ตรงกันข้ามกับ dropShadow() ให้ใช้ Modifier.innerShadow() ซึ่งจะสร้างภาพลวงตาว่าองค์ประกอบถูกกดหรือฝังลงในพื้นผิวด้านล่าง

ลำดับมีความสำคัญเมื่อสร้างเงาด้านใน ตัวแก้ไข innerShadow() จะวาดด้านบนของเนื้อหา โดยปกติแล้ว คุณจะต้องทำตามขั้นตอนต่อไปนี้เพื่อให้แน่ใจว่าเงาจะปรากฏขึ้น

  1. วาดเนื้อหาพื้นหลัง
  2. ใช้ตัวปรับแต่ง innerShadow() เพื่อสร้างลักษณะเว้า

หากวาง innerShadow() ไว้หน้าพื้นหลัง ระบบจะวาดพื้นหลังทับเงาและซ่อนเงาไว้ทั้งหมด

ตัวอย่างต่อไปนี้แสดงการใช้ innerShadow() ใน 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
            )
        }
    }
}

เงาด้านในสีเทาภายในรูปทรงสี่เหลี่ยมผืนผ้าสีขาว
รูปที่ 3 การใช้ Modifier.innerShadow() กับสี่เหลี่ยมผืนผ้ามุมมน

แสดงภาพเคลื่อนไหวของเงาเมื่อผู้ใช้โต้ตอบ

หากต้องการให้เงาตอบสนองต่อการโต้ตอบของผู้ใช้ คุณสามารถผสานรวมพร็อพเพอร์ตี้เงากับ API ภาพเคลื่อนไหวของ Compose เช่น เมื่อผู้ใช้กดปุ่ม เงาจะเปลี่ยนไปเพื่อให้การตอบสนองด้วยภาพทันที

โค้ดต่อไปนี้จะสร้างเอฟเฟกต์ "กด" พร้อมเงา (ภาพลวงตาว่าพื้นผิวกำลังถูกกดลงในหน้าจอ)

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

ประเด็นสำคัญเกี่ยวกับโค้ด

  • ประกาศสถานะเริ่มต้นและสิ้นสุดสำหรับพารามิเตอร์ที่จะเคลื่อนไหวเมื่อกดด้วย transition.animateColor และ transition.animateFloat
  • ใช้ updateTransition และระบุ targetState (targetState = isPressed) ที่เลือกเพื่อยืนยันว่าภาพเคลื่อนไหวทั้งหมดซิงค์กัน เมื่อใดก็ตามที่ isPressed เปลี่ยนแปลง ออบเจ็กต์การเปลี่ยนภาพจะจัดการภาพเคลื่อนไหวของพร็อพเพอร์ตี้ย่อยทั้งหมดจากค่าปัจจุบันเป็นค่าเป้าหมายใหม่โดยอัตโนมัติ
  • กำหนดข้อกำหนด buttonPressAnimation ซึ่งควบคุมเวลาและการค่อยๆ เปลี่ยนของทรานซิชัน โดยจะระบุ tween (ย่อมาจาก in-between) ที่มีระยะเวลา 400 มิลลิวินาทีและเส้นโค้ง EaseInOut ซึ่งหมายความว่าภาพเคลื่อนไหวจะเริ่มช้า เร็วขึ้นตรงกลาง และช้าลงในตอนท้าย
  • กำหนด Box ด้วยเชนของฟังก์ชันตัวแก้ไขที่ใช้พร็อพเพอร์ตี้ภาพเคลื่อนไหวทั้งหมดเพื่อสร้างองค์ประกอบภาพ ซึ่งรวมถึงสิ่งต่อไปนี้
    • .clickable(): ตัวแก้ไขที่ทำให้ Box เป็นแบบอินเทอร์แอกทีฟ
    • .dropShadow(): ระบบจะใช้เงาตกกระทบด้านนอก 2 รายการก่อน สีและพร็อพเพอร์ตี้อัลฟ่าจะลิงก์กับค่าที่เคลื่อนไหว (blueDropShadow ฯลฯ) และสร้างลักษณะที่ยกขึ้นในตอนแรก
    • .innerShadow(): มีการวาดเงาด้านใน 2 เงาที่ด้านบนของพื้นหลัง พร็อพเพอร์ตี้ขององค์ประกอบเหล่านี้จะลิงก์กับชุดค่าภาพเคลื่อนไหวอีกชุดหนึ่ง (innerShadowColor1 ฯลฯ) และสร้างลักษณะที่เยื้องเข้าไป

ผลลัพธ์

รูปที่ 4 ภาพเคลื่อนไหวเงาเมื่อผู้ใช้กด

สร้างเงาแบบไล่ระดับสี

เงาไม่ได้จำกัดอยู่แค่สีทึบเท่านั้น Shadow API ยอมรับ Brush ซึ่งช่วยให้คุณสร้างเงาแบบไล่ระดับสีได้

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

ประเด็นสำคัญเกี่ยวกับโค้ด

  • dropShadow() เพิ่มเงาด้านหลังกล่อง
  • brush = Brush.sweepGradient(colors) จะลงสีเงาด้วยการไล่ระดับสีที่หมุนเวียนผ่านรายการ colors ที่กำหนดไว้ล่วงหน้า ซึ่งจะสร้างเอฟเฟกต์คล้ายสีรุ้ง

ผลลัพธ์

คุณสามารถใช้แปรงเป็นเงาเพื่อสร้างการไล่ระดับสี dropShadow() ด้วยภาพเคลื่อนไหวแบบ "หายใจ" ได้ดังนี้

รูปที่ 5 เงาตกกระทบแบบไล่ระดับสีเคลื่อนไหว

รวมเงา

คุณสามารถรวมและวางเลเยอร์ตัวแก้ไข dropShadow() และ innerShadow() เพื่อสร้างเอฟเฟกต์ที่หลากหลายได้ ส่วนต่อไปนี้จะแสดงวิธีสร้างเงาแบบนิวอมอร์ฟิก นีโอบรูทัลลิสต์ และสมจริงด้วยเทคนิคนี้

สร้างเงาแบบ Neumorphic

เงาแบบนิวอมอร์ฟิกมีลักษณะที่นุ่มนวลซึ่งปรากฏขึ้นอย่างเป็นธรรมชาติจากพื้นหลัง หากต้องการสร้างเงาแบบนิวอมอร์ฟิก ให้ทำดังนี้

  1. ใช้องค์ประกอบที่มีสีเดียวกับพื้นหลัง
  2. ใช้เงาตกกระทบจางๆ 2 เงาที่ตรงข้ามกัน โดยให้เงาสว่างอยู่ที่มุมหนึ่ง และเงาเข้มอยู่ที่มุมตรงข้าม

ข้อมูลโค้ดต่อไปนี้จะวางเลเยอร์ตัวแก้ไข dropShadow() 2 รายการเพื่อสร้างเอฟเฟกต์นิวอมอร์ฟิก

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

รูปทรงสี่เหลี่ยมผืนผ้าสีขาวที่มีเอฟเฟกต์นิวโมฟิกบนพื้นหลังสีขาว
รูปที่ 6 เอฟเฟกต์เงาแบบนิวอมอร์ฟิก

สร้างเงาแบบนีโอบรูทัลลิสต์

สไตล์นีโอบรูทัลลิสต์โดดเด่นด้วยเลย์เอาต์แบบบล็อกที่มีคอนทราสต์สูง สีสันสดใส และเส้นขอบหนา หากต้องการสร้างเอฟเฟกต์นี้ ให้ใช้ dropShadow() ที่ไม่มีการเบลอและมีออฟเซ็ตที่ชัดเจน ดังที่แสดงในข้อมูลโค้ดต่อไปนี้

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

ขอบสีแดงรอบสี่เหลี่ยมผืนผ้าสีขาวที่มีเงาสีน้ำเงินบนพื้นหลังสีเหลือง
รูปที่ 7 เอฟเฟกต์เงาแบบนีโอบรูทัลลิสต์

สร้างเงาที่สมจริง

เงาที่สมจริงจะเลียนแบบเงาในโลกจริง โดยจะปรากฏขึ้นเมื่อมีแหล่งกำเนิดแสงหลัก ทำให้เกิดทั้งเงาโดยตรงและเงาที่กระจายออกไปมากขึ้น คุณสามารถวางซ้อนอินสแตนซ์ dropShadow() และ innerShadow() หลายรายการที่มีพร็อพเพอร์ตี้ต่างกันเพื่อสร้างเอฟเฟกต์เงาที่สมจริงขึ้นมาใหม่ได้ ดังที่แสดงในข้อมูลโค้ดต่อไปนี้

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

ประเด็นสำคัญเกี่ยวกับโค้ด

  • ระบบจะใช้ตัวแก้ไข dropShadow() แบบต่อเชื่อม 2 รายการที่มีพร็อพเพอร์ตี้ต่างกัน ตามด้วยตัวแก้ไข background()
  • ระบบจะใช้ตัวแก้ไข innerShadow() แบบเชื่อมโยงเพื่อสร้างเอฟเฟกต์ขอบโลหะรอบขอบของคอมโพเนนต์

ผลลัพธ์

ข้อมูลโค้ดก่อนหน้าจะสร้างผลลัพธ์ต่อไปนี้

เงาสีขาวสมจริงรอบรูปทรงโค้งมนสีดำ
รูปที่ 8 เอฟเฟกต์เงาที่สมจริง