การเปลี่ยนองค์ประกอบแบบใช้ร่วมกันในเครื่องมือเขียน

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

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

รูปที่ 1 การสาธิตองค์ประกอบที่ใช้ร่วมกันของ Jetsnack

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

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

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

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

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

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

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

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

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

  1. หากต้องการทำให้องค์ประกอบที่แชร์เคลื่อนไหวระหว่างเลย์เอาต์ทั้ง 2 ให้ ล้อมรอบ AnimatedContent Composable ด้วย 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 รายการและทําภาพเคลื่อนไหวการเปลี่ยนผ่านได้ ซึ่งจะช่วยให้ทั้ง 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 Composable จะมี 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 Grid นี้ องค์ประกอบแต่ละรายการจะอยู่ในแท็ก 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(
                            sharedContentState = 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() ใน Composable ในกรณีนี้ Compose จะวางเลย์เอาต์ขององค์ประกอบย่อยโดยใช้ข้อจํากัดเป้าหมาย และใช้ ปัจจัยการปรับขนาดเพื่อทําภาพเคลื่อนไหวแทนการเปลี่ยนขนาดเลย์เอาต์ เอง

requiredSize()

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

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

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

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

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