การเปลี่ยนองค์ประกอบที่แชร์ใน Compose

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

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

รูปที่ 1 การสาธิตองค์ประกอบที่แชร์ของ Jetsnack

ใน Compose มี API ระดับสูง 2-3 รายการที่จะช่วยคุณสร้างการแชร์ องค์ประกอบ:

  • SharedTransitionLayout: เลย์เอาต์ด้านนอกสุดจำเป็นสำหรับการใช้พื้นที่ที่แชร์ การเปลี่ยนองค์ประกอบ และมี SharedTransitionScope ความต้องการของ Composable อยู่ใน SharedTransitionScope เพื่อใช้ตัวแก้ไของค์ประกอบที่แชร์
  • Modifier.sharedElement(): ตัวแก้ไขที่จะแจ้ง SharedTransitionScope Composable ที่ควรจับคู่กับ Composable
  • Modifier.sharedBounds(): ตัวแก้ไขที่จะแจ้ง SharedTransitionScope ว่าขอบเขตของ Composable นี้ควรใช้เป็น ขอบเขตของคอนเทนเนอร์ที่ควรใช้ ตรงข้ามกับ sharedElement(), sharedBounds() ออกแบบมาให้ดูแตกต่าง เนื้อหา

แนวคิดสำคัญเมื่อสร้างองค์ประกอบที่ใช้ร่วมกันใน Compose คือวิธีการทำงาน ด้วยการวางซ้อนและการคลิป ลองดูที่การตัดคลิปและ การวางซ้อน เพื่อดูข้อมูลเพิ่มเติมเกี่ยวกับหัวข้อที่สำคัญนี้

การใช้งานพื้นฐาน

การเปลี่ยนแปลงต่อไปนี้จะสร้างขึ้นในส่วนนี้ โดยเปลี่ยนจาก "รายการ" เล็กลง ไปยังรายการที่มีรายละเอียดขนาดใหญ่กว่านี้

วันที่
รูปที่ 2 ตัวอย่างพื้นฐานของการเปลี่ยนองค์ประกอบที่แชร์ระหว่าง Composable 2 รายการ

วิธีที่ดีที่สุดในการใช้ Modifier.sharedElement() คือเมื่อใช้ร่วมกับ AnimatedContent, AnimatedVisibility หรือ NavHost เพื่อจัดการ การเปลี่ยนระหว่าง Composable โดยอัตโนมัติให้คุณ

จุดเริ่มต้นคือ AnimatedContent พื้นฐานที่มีอยู่ซึ่งมี MainContent และ DetailsContent ที่เขียนได้ด้วย Compose ได้ก่อนเพิ่มองค์ประกอบที่แชร์:

วันที่
รูปที่ 3 เริ่มตั้งแต่ AnimatedContent โดยไม่มีการเปลี่ยนองค์ประกอบที่แชร์

  1. วิธีทำให้องค์ประกอบที่แชร์มีการเคลื่อนไหวระหว่าง 2 เลย์เอาต์ ล้อมรอบ AnimatedContent ที่เขียนได้ด้วย SharedTransitionLayout ขอบเขตจาก SharedTransitionLayout และ AnimatedContent มีการส่งผ่าน ไปยัง MainContent และ DetailsContent:

    var showDetails by remember {
        mutableStateOf(false)
    }
    SharedTransitionLayout {
        AnimatedContent(
            showDetails,
            label = "basic_transition"
        ) { targetState ->
            if (!targetState) {
                MainContent(
                    onShowDetails = {
                        showDetails = true
                    },
                    animatedVisibilityScope = this@AnimatedContent,
                    sharedTransitionScope = this@SharedTransitionLayout
                )
            } else {
                DetailsContent(
                    onBack = {
                        showDetails = false
                    },
                    animatedVisibilityScope = this@AnimatedContent,
                    sharedTransitionScope = this@SharedTransitionLayout
                )
            }
        }
    }

  2. เพิ่ม Modifier.sharedElement() ลงในเชนตัวปรับแต่งที่ประกอบกันได้บนแท็ก Composable ที่ตรงกัน 2 รายการ สร้างออบเจ็กต์ SharedContentState และ ก็ให้จดจำด้วย rememberSharedContentState() ออบเจ็กต์ SharedContentState จัดเก็บคีย์ที่ไม่ซ้ำกันซึ่งเป็นตัวกำหนด องค์ประกอบร่วมกัน ใส่คีย์ที่ไม่ซ้ำกันเพื่อระบุเนื้อหา และ ใช้ rememberSharedContentState() เพื่อให้รายการจำได้ AnimatedContentScope จะอยู่ในคีย์ตัวปรับแต่ง ซึ่งใช้เพื่อ ประสานงานภาพเคลื่อนไหว

    @Composable
    private fun MainContent(
        onShowDetails: () -> Unit,
        modifier: Modifier = Modifier,
        sharedTransitionScope: SharedTransitionScope,
        animatedVisibilityScope: AnimatedVisibilityScope
    ) {
        Row(
            // ...
        ) {
            with(sharedTransitionScope) {
                Image(
                    painter = painterResource(id = R.drawable.cupcake),
                    contentDescription = "Cupcake",
                    modifier = Modifier
                        .sharedElement(
                            rememberSharedContentState(key = "image"),
                            animatedVisibilityScope = animatedVisibilityScope
                        )
                        .size(100.dp)
                        .clip(CircleShape),
                    contentScale = ContentScale.Crop
                )
                // ...
            }
        }
    }
    
    @Composable
    private fun DetailsContent(
        modifier: Modifier = Modifier,
        onBack: () -> Unit,
        sharedTransitionScope: SharedTransitionScope,
        animatedVisibilityScope: AnimatedVisibilityScope
    ) {
        Column(
            // ...
        ) {
            with(sharedTransitionScope) {
                Image(
                    painter = painterResource(id = R.drawable.cupcake),
                    contentDescription = "Cupcake",
                    modifier = Modifier
                        .sharedElement(
                            rememberSharedContentState(key = "image"),
                            animatedVisibilityScope = animatedVisibilityScope
                        )
                        .size(200.dp)
                        .clip(CircleShape),
                    contentScale = ContentScale.Crop
                )
                // ...
            }
        }
    }

หากต้องการดูข้อมูลว่าองค์ประกอบที่แชร์ตรงกันหรือไม่ ให้แตกข้อมูล rememberSharedContentState() ลงในตัวแปร แล้วค้นหา isMatchFound

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

วันที่
รูปที่ 4 ตัวอย่างพื้นฐานของการเปลี่ยนองค์ประกอบที่แชร์ระหว่าง Composable 2 รายการ

คุณอาจสังเกตเห็นว่าสีพื้นหลังและขนาดของทั้งคอนเทนเนอร์ยังคง ใช้การตั้งค่า AnimatedContent เริ่มต้น

ขอบเขตที่แชร์และองค์ประกอบที่แชร์

Modifier.sharedBounds() คล้ายกับ Modifier.sharedElement() อย่างไรก็ตาม คีย์ตัวปรับแต่งจะแตกต่างออกไปในลักษณะต่อไปนี้

  • sharedBounds() มีไว้สำหรับเนื้อหาที่มีลักษณะแตกต่างกันแต่ควรแชร์ พื้นที่เดียวกันระหว่างรัฐ แต่ sharedElement() คาดว่าเนื้อหาจะ เหมือนเดิม
  • เมื่อใช้ sharedBounds() เนื้อหาที่เข้าและออกจากหน้าจอจะ มองเห็นได้ระหว่างการเปลี่ยนสถานะระหว่าง 2 รัฐ ขณะที่ sharedElement() เฉพาะเนื้อหาเป้าหมายเท่านั้นที่แสดงผลในการเปลี่ยนรูปแบบ ขอบเขต Modifier.sharedBounds() มี enter และ exit พารามิเตอร์สำหรับ ระบุลักษณะการเปลี่ยนเนื้อหา คล้ายกับวิธี AnimatedContent ใช้งานได้
  • กรณีการใช้งานที่พบบ่อยที่สุดสำหรับ sharedBounds() คือการเปลี่ยนรูปแบบคอนเทนเนอร์ รูปแบบ ขณะที่สำหรับ sharedElement() กรณีการใช้งานตัวอย่างคือการเปลี่ยนผ่านหลัก
  • เมื่อใช้ Composable ของ Text ควรใช้ sharedBounds() เพื่อรองรับแบบอักษร เช่น การเปลี่ยนไปมาระหว่างตัวเอียงกับตัวหนา หรือการเปลี่ยนสี

จากตัวอย่างก่อนหน้า การเพิ่ม Modifier.sharedBounds() ลงใน Row และ Column ใน 2 สถานการณ์ที่แตกต่างกันจะทำให้เราสามารถแบ่งปันขอบเขตของ 2 และสร้างภาพเคลื่อนไหวการเปลี่ยน ระหว่างกัน

@Composable
private fun MainContent(
    onShowDetails: () -> Unit,
    modifier: Modifier = Modifier,
    sharedTransitionScope: SharedTransitionScope,
    animatedVisibilityScope: AnimatedVisibilityScope
) {
    with(sharedTransitionScope) {
        Row(
            modifier = Modifier
                .padding(8.dp)
                .sharedBounds(
                    rememberSharedContentState(key = "bounds"),
                    animatedVisibilityScope = animatedVisibilityScope,
                    enter = fadeIn(),
                    exit = fadeOut(),
                    resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds()
                )
                // ...
        ) {
            // ...
        }
    }
}

@Composable
private fun DetailsContent(
    modifier: Modifier = Modifier,
    onBack: () -> Unit,
    sharedTransitionScope: SharedTransitionScope,
    animatedVisibilityScope: AnimatedVisibilityScope
) {
    with(sharedTransitionScope) {
        Column(
            modifier = Modifier
                .padding(top = 200.dp, start = 16.dp, end = 16.dp)
                .sharedBounds(
                    rememberSharedContentState(key = "bounds"),
                    animatedVisibilityScope = animatedVisibilityScope,
                    enter = fadeIn(),
                    exit = fadeOut(),
                    resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds()
                )
                // ...

        ) {
            // ...
        }
    }
}

รูปที่ 5 มีขอบเขตที่แชร์ระหว่าง Composable 2 รายการ

ทำความเข้าใจขอบเขต

หากต้องการใช้ Modifier.sharedElement() Composable จะต้องอยู่ในรูปแบบ SharedTransitionScope Composable SharedTransitionLayout จะระบุค่า SharedTransitionScope โปรดตรวจสอบว่าคุณได้วางที่จุดระดับบนสุดเดียวกันใน ลำดับชั้น UI ที่มีองค์ประกอบที่คุณต้องการแชร์

โดยทั่วไป Composable ควรวางไว้ใน AnimatedVisibilityScope ซึ่งโดยปกติจะดำเนินการโดยใช้ AnimatedContent เพื่อสลับระหว่าง Composable หรือเมื่อใช้ AnimatedVisibility โดยตรงหรือตาม ฟังก์ชัน Composable ของ NavHost เว้นแต่คุณจะจัดการระดับการเข้าถึง ด้วยตนเอง อยู่ในคำสั่งซื้อ เมื่อต้องการใช้หลายขอบเขต ให้บันทึกขอบเขตที่ต้องการใน CompositionLocal ให้ใช้ตัวรับบริบทใน Kotlin หรือส่ง เป็นพารามิเตอร์ให้กับฟังก์ชัน

ใช้ CompositionLocals ในกรณีที่คุณมีขอบเขตที่ต้องการเก็บไว้หลายขอบเขต หรือลำดับชั้นที่ซ้อนหลายระดับ CompositionLocalช่วยให้คุณเลือก ขอบเขตที่แน่นอนในการบันทึกและใช้งาน แต่เมื่อคุณใช้ตัวรับบริบท เลย์เอาต์อื่นๆ ในลำดับชั้นอาจลบล้างขอบเขตที่ระบุโดยไม่ตั้งใจ ตัวอย่างเช่น หากคุณมี AnimatedContent ที่ฝังไว้หลายรายการ ขอบเขตอาจเป็นดังนี้ ถูกลบล้าง

val LocalNavAnimatedVisibilityScope = compositionLocalOf<AnimatedVisibilityScope?> { null }
val LocalSharedTransitionScope = compositionLocalOf<SharedTransitionScope?> { null }

@Composable
private fun SharedElementScope_CompositionLocal() {
    // An example of how to use composition locals to pass around the shared transition scope, far down your UI tree.
    // ...
    SharedTransitionLayout {
        CompositionLocalProvider(
            LocalSharedTransitionScope provides this
        ) {
            // This could also be your top-level NavHost as this provides an AnimatedContentScope
            AnimatedContent(state, label = "Top level AnimatedContent") { targetState ->
                CompositionLocalProvider(LocalNavAnimatedVisibilityScope provides this) {
                    // Now we can access the scopes in any nested composables as follows:
                    val sharedTransitionScope = LocalSharedTransitionScope.current
                        ?: throw IllegalStateException("No SharedElementScope found")
                    val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current
                        ?: throw IllegalStateException("No AnimatedVisibility found")
                }
                // ...
            }
        }
    }
}

อีกวิธีหนึ่งคือ หากลำดับชั้นไม่ได้ฝังลึก คุณข้ามขอบเขตได้ ลงเป็นพารามิเตอร์:

@Composable
fun MainContent(
    animatedVisibilityScope: AnimatedVisibilityScope,
    sharedTransitionScope: SharedTransitionScope
) {
}

@Composable
fun Details(
    animatedVisibilityScope: AnimatedVisibilityScope,
    sharedTransitionScope: SharedTransitionScope
) {
}

องค์ประกอบที่แชร์กับ AnimatedVisibility

ตัวอย่างก่อนหน้านี้แสดงวิธีใช้องค์ประกอบที่แชร์กับ AnimatedContent แต่ องค์ประกอบที่แชร์ก็สามารถทำงานกับ AnimatedVisibility ได้เช่นกัน

ตัวอย่างเช่น ในตัวอย่างตารางกริดแบบ Lazy นี้แต่ละองค์ประกอบจะรวมอยู่ใน AnimatedVisibility เมื่อมีการคลิกรายการ - เนื้อหาจะมี ของการดึงออกจาก UI ไปยังคอมโพเนนต์ที่คล้ายกับกล่องโต้ตอบ

var selectedSnack by remember { mutableStateOf<Snack?>(null) }

SharedTransitionLayout(modifier = Modifier.fillMaxSize()) {
    LazyColumn(
        // ...
    ) {
        items(listSnacks) { snack ->
            AnimatedVisibility(
                visible = snack != selectedSnack,
                enter = fadeIn() + scaleIn(),
                exit = fadeOut() + scaleOut(),
                modifier = Modifier.animateItem()
            ) {
                Box(
                    modifier = Modifier
                        .sharedBounds(
                            sharedContentState = rememberSharedContentState(key = "${snack.name}-bounds"),
                            // Using the scope provided by AnimatedVisibility
                            animatedVisibilityScope = this,
                            clipInOverlayDuringTransition = OverlayClip(shapeForSharedElement)
                        )
                        .background(Color.White, shapeForSharedElement)
                        .clip(shapeForSharedElement)
                ) {
                    SnackContents(
                        snack = snack,
                        modifier = Modifier.sharedElement(
                            state = rememberSharedContentState(key = snack.name),
                            animatedVisibilityScope = this@AnimatedVisibility
                        ),
                        onClick = {
                            selectedSnack = snack
                        }
                    )
                }
            }
        }
    }
    // Contains matching AnimatedContent with sharedBounds modifiers.
    SnackEditDetails(
        snack = selectedSnack,
        onConfirmClick = {
            selectedSnack = null
        }
    )
}

รูปที่ 6 องค์ประกอบที่แชร์กับ AnimatedVisibility

การจัดเรียงตัวปรับแต่ง

เมื่อใช้ Modifier.sharedElement() และ Modifier.sharedBounds() คำสั่งซื้อของ ตัวปรับแต่งเชนในกรณี เช่นเดียวกับส่วนอื่นๆ ใน Compose ตำแหน่งที่ไม่ถูกต้องของตัวปรับที่มีผลต่อขนาด อาจทำให้เกิดการข้ามภาพที่ไม่คาดคิดระหว่างการจับคู่องค์ประกอบที่แชร์

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

var selectFirst by remember { mutableStateOf(true) }
val key = remember { Any() }
SharedTransitionLayout(
    Modifier
        .fillMaxSize()
        .padding(10.dp)
        .clickable {
            selectFirst = !selectFirst
        }
) {
    AnimatedContent(targetState = selectFirst, label = "AnimatedContent") { targetState ->
        if (targetState) {
            Box(
                Modifier
                    .padding(12.dp)
                    .sharedBounds(
                        rememberSharedContentState(key = key),
                        animatedVisibilityScope = this@AnimatedContent
                    )
                    .border(2.dp, Color.Red)
            ) {
                Text(
                    "Hello",
                    fontSize = 20.sp
                )
            }
        } else {
            Box(
                Modifier
                    .offset(180.dp, 180.dp)
                    .sharedBounds(
                        rememberSharedContentState(
                            key = key,
                        ),
                        animatedVisibilityScope = this@AnimatedContent
                    )
                    .border(2.dp, Color.Red)
                    // This padding is placed after sharedBounds, but it doesn't match the
                    // other shared elements modifier order, resulting in visual jumps
                    .padding(12.dp)

            ) {
                Text(
                    "Hello",
                    fontSize = 36.sp
                )
            }
        }
    }
}

ขอบเขตที่ตรงกัน

ขอบเขตไม่ตรงกัน: สังเกตลักษณะที่ภาพเคลื่อนไหวขององค์ประกอบที่แชร์ปรากฏเล็กน้อยเนื่องจากต้องปรับขนาดตามขอบเขตที่ไม่ถูกต้อง

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

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

ข้อยกเว้นก็คือหากคุณใช้ resizeMode = ScaleToBounds() สำหรับ ภาพเคลื่อนไหวหรือ Modifier.skipToLookaheadSize() บน Composable ด้วยวิธีนี้ Compose จะวางองค์ประกอบย่อยโดยใช้ข้อจำกัดเป้าหมายและใช้แทน ตัวคูณมาตราส่วนในการดำเนินการภาพเคลื่อนไหวแทนการเปลี่ยนขนาดของเลย์เอาต์ โดยตรง

คีย์ที่ไม่ซ้ำกัน

เมื่อทำงานกับองค์ประกอบที่ใช้ร่วมกันที่ซับซ้อน การสร้างคีย์จึงเป็นแนวทางปฏิบัติที่ดี ที่ไม่ใช่สตริง เนื่องจากสตริงอาจมีข้อผิดพลาดที่จะจับคู่ได้ แต่ละคีย์จะต้อง ไม่ซ้ำกันเพื่อให้การจับคู่เกิดขึ้น เช่น ใน Jetsnack เรามีสิ่งต่อไปนี้ องค์ประกอบที่แชร์:

วันที่
รูปที่ 7 รูปภาพแสดง Jetsnack พร้อมคำอธิบายประกอบสำหรับ UI แต่ละส่วน

คุณสร้าง enum เพื่อแสดงประเภทองค์ประกอบที่แชร์ได้ ในตัวอย่างนี้ การ์ดขนมขบเคี้ยวทั้งใบก็อาจปรากฏขึ้นจากหลายๆ ตำแหน่งในบ้านด้วย ตัวอย่างเช่น ในหน้าจอ "ยอดนิยม" และ "แนะนำ" คุณสามารถสร้าง ที่มี snackId, origin ("ยอดนิยม" / "แนะนำ") และ type ขององค์ประกอบที่แชร์ที่จะแบ่งปัน:

data class SnackSharedElementKey(
    val snackId: Long,
    val origin: String,
    val type: SnackSharedElementType
)

enum class SnackSharedElementType {
    Bounds,
    Image,
    Title,
    Tagline,
    Background
}

@Composable
fun SharedElementUniqueKey() {
    // ...
            Box(
                modifier = Modifier
                    .sharedElement(
                        rememberSharedContentState(
                            key = SnackSharedElementKey(
                                snackId = 1,
                                origin = "latest",
                                type = SnackSharedElementType.Image
                            )
                        ),
                        animatedVisibilityScope = this@AnimatedVisibility
                    )
            )
            // ...
}

เราขอแนะนำให้ใช้คลาสข้อมูลสำหรับคีย์ เนื่องจากคีย์เหล่านี้ใช้ hashCode() และ isEquals()

จัดการการเปิดเผยองค์ประกอบที่แชร์ด้วยตนเอง

ในกรณีที่คุณอาจไม่ได้ใช้ AnimatedVisibility หรือ AnimatedContent คุณสามารถจัดการการเปิดเผยองค์ประกอบ ที่แชร์ได้ด้วยตัวเอง ใช้ Modifier.sharedElementWithCallerManagedVisibility() และส่งข้อมูลของคุณเอง แบบมีเงื่อนไขที่กำหนดว่าควรจะแสดงหรือไม่แสดงสินค้าเมื่อใด

var selectFirst by remember { mutableStateOf(true) }
val key = remember { Any() }
SharedTransitionLayout(
    Modifier
        .fillMaxSize()
        .padding(10.dp)
        .clickable {
            selectFirst = !selectFirst
        }
) {
    Box(
        Modifier
            .sharedElementWithCallerManagedVisibility(
                rememberSharedContentState(key = key),
                !selectFirst
            )
            .background(Color.Red)
            .size(100.dp)
    ) {
        Text(if (!selectFirst) "false" else "true", color = Color.White)
    }
    Box(
        Modifier
            .offset(180.dp, 180.dp)
            .sharedElementWithCallerManagedVisibility(
                rememberSharedContentState(
                    key = key,
                ),
                selectFirst
            )
            .alpha(0.5f)
            .background(Color.Blue)
            .size(180.dp)
    ) {
        Text(if (selectFirst) "false" else "true", color = Color.White)
    }
}

ข้อจำกัดปัจจุบัน

API เหล่านี้มีข้อจำกัดบางประการ สิ่งที่เห็นได้ชัดที่สุดคือ

  • ระบบไม่รองรับการทำงานร่วมกันระหว่างข้อมูลพร็อพเพอร์ตี้และการเขียน ซึ่งรวมถึง Composable ที่รวม AndroidView เช่น Dialog
  • ไม่มีการรองรับภาพเคลื่อนไหวอัตโนมัติสำหรับรายการต่อไปนี้
    • รูปภาพที่แชร์ได้ด้วย Composable:
      • ContentScale ไม่เคลื่อนไหวโดยค่าเริ่มต้น ภาพสแนปจนถึงจุดสิ้นสุดที่ตั้งไว้ ContentScale
    • การตัดรูปร่าง - ไม่มีการสนับสนุนในตัวสำหรับโหมดอัตโนมัติ ภาพเคลื่อนไหวระหว่างรูปทรงต่างๆ เช่น การสร้างภาพเคลื่อนไหวจากสี่เหลี่ยมจัตุรัสเป็น วงกลมขณะที่รายการเปลี่ยน
    • สำหรับเคสที่ไม่รองรับ ให้ใช้ Modifier.sharedBounds() แทน sharedElement() และเพิ่ม Modifier.animateEnterExit() ลงในรายการ