รูปร่างใน Compose

เมื่อใช้ Compose คุณจะสร้างรูปร่างที่ทำจากรูปหลายเหลี่ยมได้ ตัวอย่างเช่น คุณสามารถสร้างรูปร่างประเภทต่อไปนี้ได้

หกเหลี่ยมสีน้ำเงินตรงกลางพื้นที่วาดภาพ
รูปที่ 1 ตัวอย่างรูปทรงต่างๆ ที่คุณสร้างได้ด้วยไลบรารี graphics-shapes

หากต้องการสร้างรูปหลายเหลี่ยมโค้งมนที่กำหนดเองใน Compose ให้เพิ่มทรัพยากร Dependency graphics-shapes ลงใน app/build.gradle ดังนี้

implementation "androidx.graphics:graphics-shapes:1.0.1"

ไลบรารีนี้ช่วยให้คุณสร้างรูปร่างที่ทำจากรูปหลายเหลี่ยมได้ แม้ว่ารูปทรงหลายเหลี่ยมจะมีเฉพาะขอบตรงและมุมแหลม แต่รูปทรงเหล่านี้ก็ช่วยให้คุณเลือกใช้มุมโค้งมนได้ ซึ่งช่วยให้เปลี่ยนรูปร่างระหว่าง 2 รูปทรงที่แตกต่างกันได้อย่างง่ายดาย การเปลี่ยนรูปร่างระหว่างรูปร่างที่กำหนดเองเป็นเรื่องยาก และมักเป็นปัญหาในเวลาออกแบบ แต่ไลบรารีนี้ทำให้การเปลี่ยนรูปร่างระหว่างรูปทรงเหล่านี้เป็นเรื่องง่ายด้วยโครงสร้างรูปหลายเหลี่ยมที่คล้ายกัน

สร้างรูปหลายเหลี่ยม

ข้อมูลโค้ดต่อไปนี้จะสร้างรูปหลายเหลี่ยมพื้นฐานที่มี 6 จุดตรงกลางพื้นที่วาดภาพ

Box(
    modifier = Modifier
        .drawWithCache {
            val roundedPolygon = RoundedPolygon(
                numVertices = 6,
                radius = size.minDimension / 2,
                centerX = size.width / 2,
                centerY = size.height / 2
            )
            val roundedPolygonPath = roundedPolygon.toPath().asComposePath()
            onDrawBehind {
                drawPath(roundedPolygonPath, color = Color.Blue)
            }
        }
        .fillMaxSize()
)

หกเหลี่ยมสีน้ำเงินตรงกลางพื้นที่วาดภาพ
รูปที่ 2 หกเหลี่ยมสีน้ำเงินตรงกลางพื้นที่วาดภาพ

ในตัวอย่างนี้ ไลบรารีจะสร้าง RoundedPolygon ซึ่งเก็บเรขาคณิตที่แสดงรูปร่างที่ขอ หากต้องการวาดรูปร่างนั้นในแอป Compose คุณต้องรับออบเจ็กต์ Path จากแอปดังกล่าวเพื่อรับรูปร่างในรูปแบบที่ Compose รู้วิธีวาด

ปัดมุมของรูปหลายเหลี่ยมให้โค้งมน

หากต้องการทำให้มุมของรูปหลายเหลี่ยมโค้งมน ให้ใช้พารามิเตอร์ CornerRounding ซึ่งใช้พารามิเตอร์ 2 ตัว ได้แก่ radius และ smoothing มุมโค้งแต่ละมุมประกอบด้วยเส้นโค้งลูกบาศก์ 1-3 เส้น โดยตรงกลางมีรูปร่างเป็นส่วนโค้งวงกลม ขณะที่เส้นโค้งด้านข้าง ("ขนาบข้าง") 2 เส้นจะเปลี่ยนจากขอบของรูปร่างเป็นเส้นโค้งตรงกลาง

รัศมี

radius คือรัศมีของวงกลมที่ใช้ปัดเศษจุดยอด

ตัวอย่างเช่น สามเหลี่ยมมุมมนต่อไปนี้สร้างขึ้นดังนี้

สามเหลี่ยมที่มีมุมโค้งมน
รูปที่ 3 สามเหลี่ยมที่มีมุมโค้งมน
รัศมีการปัดเศษ r จะกำหนดขนาดการปัดเศษวงกลมของมุมโค้งมน
รูปที่ 4 รัศมีการปัดเศษ r จะกำหนดขนาดการปัดเศษวงกลมของมุมโค้งมน

ให้ผิวเรียบเนียน

การปรับให้เรียบเป็นปัจจัยที่กำหนดระยะเวลาที่ใช้ในการเปลี่ยนจากส่วนโค้งมนของมุมไปยังขอบ ปัจจัยการปรับให้เรียบเป็น 0 (ไม่ปรับให้เรียบ ซึ่งเป็นค่าเริ่มต้นสำหรับ CornerRounding) จะทำให้มุมโค้งมนเป็นวงกลมอย่างสมบูรณ์ ปัจจัยการปรับให้เรียบที่ไม่ใช่ศูนย์ (สูงสุด 1.0) จะส่งผลให้มุมโค้งมนด้วยเส้นโค้ง 3 เส้นที่แยกจากกัน

ปัจจัยการปรับให้เรียบเป็น 0 (ไม่ปรับให้เรียบ) จะสร้างเส้นโค้งลูกบาศก์เดี่ยวซึ่งเป็นไปตามวงกลมรอบมุมที่มีรัศมีการปัดเศษที่ระบุ ดังตัวอย่างก่อนหน้านี้
รูปที่ 5 ปัจจัยการปรับให้เรียบเป็น 0 (ไม่ปรับให้เรียบ) จะสร้างเส้นโค้งลูกบาศก์เดี่ยวซึ่งเป็นไปตามวงกลมรอบมุมที่มีรัศมีการปัดเศษที่ระบุ ดังตัวอย่างก่อนหน้านี้
ปัจจัยการปรับให้เรียบที่ไม่ใช่ศูนย์จะสร้างเส้นโค้งลูกบาศก์ 3 เส้นเพื่อปัดเศษจุดยอด ได้แก่ เส้นโค้งวงกลมด้านใน (เหมือนก่อนหน้านี้) บวกกับเส้นโค้งด้านข้าง 2 เส้นที่เปลี่ยนผ่านระหว่างเส้นโค้งด้านในกับขอบรูปหลายเหลี่ยม
รูปที่ 6 ปัจจัยการปรับให้เรียบที่ไม่ใช่ศูนย์จะสร้างเส้นโค้งลูกบาศก์ 3 เส้นเพื่อปัดเศษจุดยอด ได้แก่ เส้นโค้งวงกลมด้านใน (เหมือนก่อนหน้านี้) บวกกับเส้นโค้งด้านข้าง 2 เส้นที่เปลี่ยนผ่านระหว่างเส้นโค้งด้านในกับขอบรูปหลายเหลี่ยม

ตัวอย่างเช่น ข้อมูลโค้ดด้านล่างแสดงความแตกต่างเล็กน้อยในการตั้งค่าการปรับให้เรียบเป็น 0 เทียบกับ 1

Box(
    modifier = Modifier
        .drawWithCache {
            val roundedPolygon = RoundedPolygon(
                numVertices = 3,
                radius = size.minDimension / 2,
                centerX = size.width / 2,
                centerY = size.height / 2,
                rounding = CornerRounding(
                    size.minDimension / 10f,
                    smoothing = 0.1f
                )
            )
            val roundedPolygonPath = roundedPolygon.toPath().asComposePath()
            onDrawBehind {
                drawPath(roundedPolygonPath, color = Color.Black)
            }
        }
        .size(100.dp)
)

สามเหลี่ยมสีดำ 2 รูปแสดงความแตกต่างในพารามิเตอร์การปรับให้เรียบ
รูปที่ 7 สามเหลี่ยมสีดำ 2 รูปแสดงความแตกต่างในพารามิเตอร์การปรับให้เรียบ

ขนาดและตำแหน่ง

โดยค่าเริ่มต้น ระบบจะสร้างรูปร่างที่มีรัศมี 1 รอบจุดศูนย์กลาง (0, 0) รัศมีนี้แสดงระยะห่างระหว่างจุดศูนย์กลางและจุดยอดภายนอก ของรูปหลายเหลี่ยมที่ใช้เป็นฐานของรูปร่าง โปรดทราบว่าการลบมุมจะทำให้รูปร่างเล็กลงเนื่องจากมุมที่โค้งมนจะอยู่ใกล้กับจุดศูนย์กลางมากกว่าจุดยอดที่ถูกลบมุม หากต้องการปรับขนาดรูปหลายเหลี่ยม ให้ปรับค่า radius หากต้องการปรับตำแหน่ง ให้เปลี่ยน centerX หรือ centerY ของรูปหลายเหลี่ยม หรือจะเปลี่ยนรูปแบบออบเจ็กต์เพื่อเปลี่ยนขนาด ตำแหน่ง และการหมุนโดยใช้ฟังก์ชันการเปลี่ยนรูปแบบ DrawScope มาตรฐาน เช่น DrawScope#translate() ก็ได้

เปลี่ยนรูปร่าง

ออบเจ็กต์ Morph คือรูปร่างใหม่ที่แสดงภาพเคลื่อนไหวระหว่างรูปร่างหลายเหลี่ยม 2 รูป หากต้องการเปลี่ยนรูปร่างระหว่าง 2 รูปร่าง ให้สร้าง RoundedPolygons 2 รายการและออบเจ็กต์ Morph ที่ใช้รูปร่างทั้งสองนี้ หากต้องการคำนวณรูปร่างระหว่างรูปร่างเริ่มต้นและรูปร่างสิ้นสุด ให้ระบุค่า progress ระหว่าง 0 ถึง 1 เพื่อกำหนดรูปแบบระหว่างรูปร่างเริ่มต้น (0) และรูปร่างสิ้นสุด (1) ดังนี้

Box(
    modifier = Modifier
        .drawWithCache {
            val triangle = RoundedPolygon(
                numVertices = 3,
                radius = size.minDimension / 2f,
                centerX = size.width / 2f,
                centerY = size.height / 2f,
                rounding = CornerRounding(
                    size.minDimension / 10f,
                    smoothing = 0.1f
                )
            )
            val square = RoundedPolygon(
                numVertices = 4,
                radius = size.minDimension / 2f,
                centerX = size.width / 2f,
                centerY = size.height / 2f
            )

            val morph = Morph(start = triangle, end = square)
            val morphPath = morph
                .toPath(progress = 0.5f).asComposePath()

            onDrawBehind {
                drawPath(morphPath, color = Color.Black)
            }
        }
        .fillMaxSize()
)

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

50% ของระยะห่างระหว่างสามเหลี่ยมมุมมนกับสี่เหลี่ยมจัตุรัส
รูปที่ 8 50% ของระยะห่างระหว่างสามเหลี่ยมมุมมนกับสี่เหลี่ยมจัตุรัส

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

val infiniteAnimation = rememberInfiniteTransition(label = "infinite animation")
val morphProgress = infiniteAnimation.animateFloat(
    initialValue = 0f,
    targetValue = 1f,
    animationSpec = infiniteRepeatable(
        tween(500),
        repeatMode = RepeatMode.Reverse
    ),
    label = "morph"
)
Box(
    modifier = Modifier
        .drawWithCache {
            val triangle = RoundedPolygon(
                numVertices = 3,
                radius = size.minDimension / 2f,
                centerX = size.width / 2f,
                centerY = size.height / 2f,
                rounding = CornerRounding(
                    size.minDimension / 10f,
                    smoothing = 0.1f
                )
            )
            val square = RoundedPolygon(
                numVertices = 4,
                radius = size.minDimension / 2f,
                centerX = size.width / 2f,
                centerY = size.height / 2f
            )

            val morph = Morph(start = triangle, end = square)
            val morphPath = morph
                .toPath(progress = morphProgress.value)
                .asComposePath()

            onDrawBehind {
                drawPath(morphPath, color = Color.Black)
            }
        }
        .fillMaxSize()
)

เปลี่ยนรูปร่างระหว่างสี่เหลี่ยมจัตุรัสและสามเหลี่ยมมุมมนอย่างไม่สิ้นสุด
รูปที่ 9 เปลี่ยนรูปร่างระหว่างสี่เหลี่ยมจัตุรัสและสามเหลี่ยมมุมมนอย่างไม่สิ้นสุด

ใช้รูปหลายเหลี่ยมเป็นคลิป

การใช้ตัวแก้ไข clip ใน Compose เพื่อเปลี่ยนวิธีแสดงผล Composable และใช้ประโยชน์จากเงาที่วาดรอบพื้นที่การคลิปเป็นเรื่องปกติ

fun RoundedPolygon.getBounds() = calculateBounds().let { Rect(it[0], it[1], it[2], it[3]) }
class RoundedPolygonShape(
    private val polygon: RoundedPolygon,
    private var matrix: Matrix = Matrix()
) : Shape {
    private var path = Path()
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        path.rewind()
        path = polygon.toPath().asComposePath()
        matrix.reset()
        val bounds = polygon.getBounds()
        val maxDimension = max(bounds.width, bounds.height)
        matrix.scale(size.width / maxDimension, size.height / maxDimension)
        matrix.translate(-bounds.left, -bounds.top)

        path.transform(matrix)
        return Outline.Generic(path)
    }
}

จากนั้นคุณจะใช้รูปหลายเหลี่ยมเป็นคลิปได้ ดังที่แสดงในข้อมูลโค้ดต่อไปนี้

val hexagon = remember {
    RoundedPolygon(
        6,
        rounding = CornerRounding(0.2f)
    )
}
val clip = remember(hexagon) {
    RoundedPolygonShape(polygon = hexagon)
}
Box(
    modifier = Modifier
        .clip(clip)
        .background(MaterialTheme.colorScheme.secondary)
        .size(200.dp)
) {
    Text(
        "Hello Compose",
        color = MaterialTheme.colorScheme.onSecondary,
        modifier = Modifier.align(Alignment.Center)
    )
}

ซึ่งจะส่งผลดังนี้

รูปหกเหลี่ยมที่มีข้อความ "hello compose" อยู่ตรงกลาง
รูปที่ 10 รูปหกเหลี่ยมที่มีข้อความ "Hello Compose" อยู่ตรงกลาง

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

val hexagon = remember {
    RoundedPolygon(
        6,
        rounding = CornerRounding(0.2f)
    )
}
val clip = remember(hexagon) {
    RoundedPolygonShape(polygon = hexagon)
}
Box(
    modifier = Modifier.fillMaxSize(),
    contentAlignment = Alignment.Center
) {
    Image(
        painter = painterResource(id = R.drawable.dog),
        contentDescription = "Dog",
        contentScale = ContentScale.Crop,
        modifier = Modifier
            .graphicsLayer {
                this.shadowElevation = 6.dp.toPx()
                this.shape = clip
                this.clip = true
                this.ambientShadowColor = Color.Black
                this.spotShadowColor = Color.Black
            }
            .size(200.dp)

    )
}

สุนัขในรูปหกเหลี่ยมที่มีเงารอบขอบ
รูปที่ 11 ใช้รูปร่างที่กำหนดเองเป็นคลิปแล้ว

ปุ่มเปลี่ยนรูปร่างเมื่อคลิก

คุณสามารถใช้ไลบรารี graphics-shape เพื่อสร้างปุ่มที่เปลี่ยนรูปร่างไปมาระหว่าง 2 รูปทรงเมื่อกด ก่อนอื่น ให้สร้าง MorphPolygonShape ที่ขยาย Shape โดยปรับขนาดและแปลให้เหมาะสม โปรดสังเกตการส่งผ่านความคืบหน้าเพื่อให้รูปร่างเคลื่อนไหวได้

class MorphPolygonShape(
    private val morph: Morph,
    private val percentage: Float
) : Shape {

    private val matrix = Matrix()
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        // Below assumes that you haven't changed the default radius of 1f, nor the centerX and centerY of 0f
        // By default this stretches the path to the size of the container, if you don't want stretching, use the same size.width for both x and y.
        matrix.scale(size.width / 2f, size.height / 2f)
        matrix.translate(1f, 1f)

        val path = morph.toPath(progress = percentage).asComposePath()
        path.transform(matrix)
        return Outline.Generic(path)
    }
}

หากต้องการใช้รูปร่างมอร์ฟนี้ ให้สร้างรูปหลายเหลี่ยม 2 รูป ได้แก่ shapeA และ shapeB สร้างและจดจำMorph จากนั้นใช้การเปลี่ยนรูปแบบกับปุ่มเป็นเส้นขอบคลิป โดยใช้ interactionSource เมื่อกดเป็นแรงขับเคลื่อนเบื้องหลังภาพเคลื่อนไหว

val shapeA = remember {
    RoundedPolygon(
        6,
        rounding = CornerRounding(0.2f)
    )
}
val shapeB = remember {
    RoundedPolygon.star(
        6,
        rounding = CornerRounding(0.1f)
    )
}
val morph = remember {
    Morph(shapeA, shapeB)
}
val interactionSource = remember {
    MutableInteractionSource()
}
val isPressed by interactionSource.collectIsPressedAsState()
val animatedProgress = animateFloatAsState(
    targetValue = if (isPressed) 1f else 0f,
    label = "progress",
    animationSpec = spring(dampingRatio = 0.4f, stiffness = Spring.StiffnessMedium)
)
Box(
    modifier = Modifier
        .size(200.dp)
        .padding(8.dp)
        .clip(MorphPolygonShape(morph, animatedProgress.value))
        .background(Color(0xFF80DEEA))
        .size(200.dp)
        .clickable(interactionSource = interactionSource, indication = null) {
        }
) {
    Text("Hello", modifier = Modifier.align(Alignment.Center))
}

ซึ่งจะส่งผลให้เกิดภาพเคลื่อนไหวต่อไปนี้เมื่อแตะกล่อง

ใช้มอร์ฟเป็นคลิกระหว่างรูปร่าง 2 รูป
รูปที่ 12 ใช้มอร์ฟเป็นคลิกระหว่างรูปร่าง 2 รูป

ภาพเคลื่อนไหวที่เปลี่ยนรูปร่างไปเรื่อยๆ

หากต้องการสร้างภาพเคลื่อนไหวของรูปร่างที่เปลี่ยนรูปไปเรื่อยๆ ให้ใช้ rememberInfiniteTransition ด้านล่างนี้คือตัวอย่างรูปโปรไฟล์ที่เปลี่ยนรูปร่าง (และหมุน) อย่างต่อเนื่องเมื่อเวลาผ่านไป แนวทางนี้ใช้การปรับเปลี่ยนเล็กน้อยกับ MorphPolygonShape ที่แสดงด้านบน ดังนี้

class CustomRotatingMorphShape(
    private val morph: Morph,
    private val percentage: Float,
    private val rotation: Float
) : Shape {

    private val matrix = Matrix()
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        // Below assumes that you haven't changed the default radius of 1f, nor the centerX and centerY of 0f
        // By default this stretches the path to the size of the container, if you don't want stretching, use the same size.width for both x and y.
        matrix.scale(size.width / 2f, size.height / 2f)
        matrix.translate(1f, 1f)
        matrix.rotateZ(rotation)

        val path = morph.toPath(progress = percentage).asComposePath()
        path.transform(matrix)

        return Outline.Generic(path)
    }
}

@Preview
@Composable
private fun RotatingScallopedProfilePic() {
    val shapeA = remember {
        RoundedPolygon(
            12,
            rounding = CornerRounding(0.2f)
        )
    }
    val shapeB = remember {
        RoundedPolygon.star(
            12,
            rounding = CornerRounding(0.2f)
        )
    }
    val morph = remember {
        Morph(shapeA, shapeB)
    }
    val infiniteTransition = rememberInfiniteTransition("infinite outline movement")
    val animatedProgress = infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            tween(2000, easing = LinearEasing),
            repeatMode = RepeatMode.Reverse
        ),
        label = "animatedMorphProgress"
    )
    val animatedRotation = infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 360f,
        animationSpec = infiniteRepeatable(
            tween(6000, easing = LinearEasing),
            repeatMode = RepeatMode.Reverse
        ),
        label = "animatedMorphProgress"
    )
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Image(
            painter = painterResource(id = R.drawable.dog),
            contentDescription = "Dog",
            contentScale = ContentScale.Crop,
            modifier = Modifier
                .clip(
                    CustomRotatingMorphShape(
                        morph,
                        animatedProgress.value,
                        animatedRotation.value
                    )
                )
                .size(200.dp)
        )
    }
}

โค้ดนี้จะให้ผลลัพธ์ที่สนุกสนานดังนี้

รูปหัวใจ
รูปที่ 13 รูปโปรไฟล์ที่ถูกตัดด้วยรูปทรงหยักที่หมุนได้

รูปหลายเหลี่ยมที่กำหนดเอง

หากรูปร่างที่สร้างจากรูปหลายเหลี่ยมด้านเท่ามุมเท่าไม่ครอบคลุมกรณีการใช้งานของคุณ คุณสามารถสร้างรูปร่างที่กำหนดเองมากขึ้นด้วยรายการจุดยอด เช่น คุณอาจต้องการสร้างรูปหัวใจแบบนี้

รูปหัวใจ
รูปที่ 14 รูปหัวใจ

คุณสามารถระบุจุดยอดแต่ละจุดของรูปร่างนี้ได้โดยใช้โอเวอร์โหลด RoundedPolygon ที่รับอาร์เรย์แบบ Float ของพิกัด x, y

หากต้องการแยกรูปหลายเหลี่ยมรูปหัวใจ โปรดสังเกตว่าระบบพิกัดเชิงขั้วสำหรับการระบุจุดจะช่วยให้ทำได้ง่ายกว่าการใช้ระบบพิกัดคาร์ทีเซียน (x,y) โดยที่ 0° เริ่มต้นทางด้านขวามือและดำเนินต่อไปตามเข็มนาฬิกา โดยมี 270° อยู่ที่ตำแหน่ง 12 นาฬิกา

รูปหัวใจ
รูปที่ 15 รูปหัวใจพร้อมพิกัด

ตอนนี้คุณสามารถกำหนดรูปร่างได้ง่ายขึ้นโดยระบุมุม (𝜭) และรัศมีจากจุดศูนย์กลางที่แต่ละจุด ดังนี้

รูปหัวใจ
รูปที่ 16 รูปหัวใจพร้อมพิกัด โดยไม่มีการปัดเศษ

ตอนนี้คุณสามารถสร้างจุดยอดและส่งไปยังฟังก์ชัน RoundedPolygon ได้แล้ว

val vertices = remember {
    val radius = 1f
    val radiusSides = 0.8f
    val innerRadius = .1f
    floatArrayOf(
        radialToCartesian(radiusSides, 0f.toRadians()).x,
        radialToCartesian(radiusSides, 0f.toRadians()).y,
        radialToCartesian(radius, 90f.toRadians()).x,
        radialToCartesian(radius, 90f.toRadians()).y,
        radialToCartesian(radiusSides, 180f.toRadians()).x,
        radialToCartesian(radiusSides, 180f.toRadians()).y,
        radialToCartesian(radius, 250f.toRadians()).x,
        radialToCartesian(radius, 250f.toRadians()).y,
        radialToCartesian(innerRadius, 270f.toRadians()).x,
        radialToCartesian(innerRadius, 270f.toRadians()).y,
        radialToCartesian(radius, 290f.toRadians()).x,
        radialToCartesian(radius, 290f.toRadians()).y,
    )
}

คุณต้องแปลจุดยอดเป็นพิกัดคาร์ทีเซียนโดยใช้ฟังก์ชัน radialToCartesian นี้

internal fun Float.toRadians() = this * PI.toFloat() / 180f

internal val PointZero = PointF(0f, 0f)
internal fun radialToCartesian(
    radius: Float,
    angleRadians: Float,
    center: PointF = PointZero
) = directionVectorPointF(angleRadians) * radius + center

internal fun directionVectorPointF(angleRadians: Float) =
    PointF(cos(angleRadians), sin(angleRadians))

โค้ดก่อนหน้าจะให้จุดยอดดิบสำหรับหัวใจ แต่คุณต้องปัดมุมที่เฉพาะเจาะจงเพื่อให้ได้รูปหัวใจที่เลือก มุมที่ 90° และ 270° จะไม่มีการลบมุม แต่มุมอื่นๆ จะมีการลบมุม หากต้องการลบมุมที่กำหนดเองสำหรับแต่ละมุม ให้ใช้พารามิเตอร์ perVertexRounding ดังนี้

val rounding = remember {
    val roundingNormal = 0.6f
    val roundingNone = 0f
    listOf(
        CornerRounding(roundingNormal),
        CornerRounding(roundingNone),
        CornerRounding(roundingNormal),
        CornerRounding(roundingNormal),
        CornerRounding(roundingNone),
        CornerRounding(roundingNormal),
    )
}

val polygon = remember(vertices, rounding) {
    RoundedPolygon(
        vertices = vertices,
        perVertexRounding = rounding
    )
}
Box(
    modifier = Modifier
        .drawWithCache {
            val roundedPolygonPath = polygon.toPath().asComposePath()
            onDrawBehind {
                scale(size.width * 0.5f, size.width * 0.5f) {
                    translate(size.width * 0.5f, size.height * 0.5f) {
                        drawPath(roundedPolygonPath, color = Color(0xFFF15087))
                    }
                }
            }
        }
        .size(400.dp)
)

ซึ่งจะส่งผลให้เกิดหัวใจสีชมพู ดังนี้

รูปหัวใจ
รูปที่ 17 ผลลัพธ์รูปหัวใจ

หากรูปร่างก่อนหน้านี้ไม่ครอบคลุมกรณีการใช้งานของคุณ ให้ลองใช้คลาส Path เพื่อวาดรูปร่างที่กำหนดเอง หรือโหลดไฟล์ ImageVector จากดิสก์ ไลบรารี graphics-shapes ไม่ได้มีไว้สำหรับใช้กับรูปร่างที่กำหนดเอง แต่มีไว้เพื่อลดความซับซ้อนในการสร้างรูปหลายเหลี่ยมโค้งมนและภาพเคลื่อนไหวแบบมอร์ฟระหว่างรูปหลายเหลี่ยมเหล่านั้นโดยเฉพาะ

แหล่งข้อมูลเพิ่มเติม

ดูข้อมูลเพิ่มเติมและตัวอย่างได้จากแหล่งข้อมูลต่อไปนี้