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

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

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

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

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

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

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

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

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

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

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

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

รูปที่ 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() ลงในเชนตัวแก้ไขคอมโพสิเบิลในคอมโพสิเบิล 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 ตัวอย่างพื้นฐานของการเปลี่ยนองค์ประกอบที่แชร์ระหว่างคอมโพสิเบิล 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() คอมโพสิเบิลต้องอยู่ใน SharedTransitionScope คอมโพสิเบิล SharedTransitionLayout ให้ SharedTransitionScope ตรวจสอบว่าได้วางที่ระดับบนสุดเดียวกันในลำดับชั้น UI ที่มีองค์ประกอบที่คุณต้องการแชร์

โดยทั่วไปแล้ว คุณควรวางคอมโพสิเบิลภายใน 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 การวางตำแหน่งที่ไม่ถูกต้องของตัวแก้ไขที่มีผลต่อขนาด อาจทำให้ภาพกระตุกโดยไม่คาดคิดระหว่างการจับคู่องค์ประกอบที่แชร์

เช่น หากคุณวางตัวแก้ไขระยะห่างจากขอบในตําแหน่งอื่นในองค์ประกอบที่แชร์ 2 รายการ ภาพเคลื่อนไหวจะแตกต่างกัน

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() ในคอมโพสิเบิล ในกรณีนี้ 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 เหล่านี้มีข้อจํากัดบางอย่าง โดยการเปลี่ยนแปลงที่สำคัญที่สุดมีดังนี้

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