องค์ประกอบแบบใช้ร่วมกันในการเปลี่ยนภาพเป็นวิธีที่ราบรื่นในการเปลี่ยนระหว่างคอมโพสิเบิลที่มีเนื้อหาสอดคล้องกัน มักใช้เพื่อการนําทาง ซึ่งช่วยให้คุณเชื่อมต่อหน้าจอต่างๆ เข้าด้วยกันได้เมื่อผู้ใช้ไปยังหน้าจอต่างๆ
ตัวอย่างเช่น ในวิดีโอต่อไปนี้ คุณจะเห็นรูปภาพและชื่อของอาหารว่างที่แชร์จากหน้าข้อมูลไปยังหน้ารายละเอียด
ในเครื่องมือเขียน มี API ระดับสูง 2-3 รายการที่จะช่วยคุณสร้างองค์ประกอบที่แชร์
SharedTransitionLayout
: เลย์เอาต์ด้านนอกสุดที่จําเป็นต่อการใช้การเปลี่ยนองค์ประกอบที่แชร์ มีSharedTransitionScope
คอมโพสิเบิลต้องอยู่ในSharedTransitionScope
จึงจะใช้ตัวแก้ไของค์ประกอบที่แชร์ได้Modifier.sharedElement()
: ตัวแก้ไขที่แจ้งให้SharedTransitionScope
คอมโพสิเบิลที่ควรจับคู่กับคอมโพสิเบิลอื่นModifier.sharedBounds()
: ตัวแก้ไขที่แจ้งให้SharedTransitionScope
ทราบว่าควรใช้ขอบเขตของคอมโพสิเบิลนี้เป็นขอบเขตของคอนเทนเนอร์สำหรับตำแหน่งที่ควรทำการเปลี่ยนsharedBounds()
ออกแบบมาสำหรับเนื้อหาที่แตกต่างออกไปเมื่อเทียบกับsharedElement()
แนวคิดสําคัญเมื่อสร้างองค์ประกอบที่แชร์ในเครื่องมือเขียนคือวิธีทํางานขององค์ประกอบเหล่านั้นกับการวางซ้อนและการครอบตัด ดูข้อมูลเพิ่มเติมเกี่ยวกับหัวข้อสำคัญนี้ได้ในส่วนการครอบตัดและการวางซ้อน
การใช้งานพื้นฐาน
ระบบจะสร้างทรานซิชันต่อไปนี้ในส่วนนี้ โดยเปลี่ยนจากรายการ "รายการ" ขนาดเล็กไปเป็นรายการแบบละเอียดขนาดใหญ่

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

AnimatedContent
โดยไม่ใช้องค์ประกอบแบบใช้ร่วมกันในการเปลี่ยนภาพหากต้องการให้องค์ประกอบที่แชร์เคลื่อนไหวระหว่างเลย์เอาต์ 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 ) } } }
เพิ่ม
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
ซึ่งจะทำให้เกิดภาพเคลื่อนไหวอัตโนมัติดังต่อไปนี้

คุณอาจเห็นว่าสีพื้นหลังและขนาดของคอนเทนเนอร์ทั้งใบยังคงใช้การตั้งค่า AnimatedContent
เริ่มต้น
ขอบเขตที่แชร์เทียบกับองค์ประกอบที่แชร์
Modifier.sharedBounds()
คล้ายกับ Modifier.sharedElement()
อย่างไรก็ตาม ตัวแก้ไขจะแตกต่างกันดังนี้
sharedBounds()
สำหรับเนื้อหาที่มีลักษณะแตกต่างกันแต่ควรมีขอบเขตเดียวกันระหว่างรัฐ ส่วนsharedElement()
คาดว่าเนื้อหาจะเหมือนกัน- เมื่อใช้
sharedBounds()
เนื้อหาที่เข้าสู่และออกจากหน้าจอจะปรากฏขึ้นในระหว่างการเปลี่ยนระหว่าง 2 สถานะ ขณะที่ใช้sharedElement()
ระบบจะแสดงผลเฉพาะเนื้อหาเป้าหมายในขอบเขตการเปลี่ยนรูปแบบModifier.sharedBounds()
มีพารามิเตอร์enter
และexit
สำหรับระบุวิธีเปลี่ยนเนื้อหา ซึ่งคล้ายกับวิธีการทำงานของAnimatedContent
- Use Case ที่พบบ่อยที่สุดสําหรับ
sharedBounds()
คือรูปแบบการเปลี่ยนรูปแบบคอนเทนเนอร์ ส่วนสําหรับsharedElement()
ตัวอย่าง Use Case คือการเปลี่ยนภาพฮีโร่ - เมื่อใช้คอมโพสิเบิล
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() ) // ... ) { // ... } } }
ทําความเข้าใจขอบเขต
หากต้องการใช้ Modifier.sharedElement()
คอมโพสิเบิลต้องอยู่ใน SharedTransitionScope
คอมโพสิเบิล SharedTransitionLayout
ให้
SharedTransitionScope
ตรวจสอบว่าได้วางไว้ที่จุดระดับบนสุดเดียวกันในลําดับชั้น UI ที่มีองค์ประกอบที่คุณต้องการแชร์
โดยทั่วไปแล้ว คุณควรวางคอมโพสิเบิลภายใน AnimatedVisibilityScope
ด้วย โดยปกติแล้ว ข้อมูลนี้จะระบุโดยใช้ AnimatedContent
เพื่อสลับระหว่างคอมโพสิเบิลหรือเมื่อใช้ AnimatedVisibility
โดยตรง หรือใช้ฟังก์ชันคอมโพสิเบิล 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 } ) }
AnimatedVisibility
การจัดเรียงตัวปรับแต่ง
สำหรับ Modifier.sharedElement()
และ Modifier.sharedBounds()
ลําดับของเชนตัวแก้ไขจะส่งผลต่อผลลัพธ์เช่นเดียวกับการคอมโพสิทส่วนอื่นๆ การวางตัวแก้ไขที่ส่งผลต่อขนาดไม่ถูกต้องอาจทําให้ภาพกระโดดขึ้นโดยไม่คาดคิดระหว่างการจับคู่องค์ประกอบที่แชร์
เช่น หากคุณวางตัวแก้ไขระยะห่างจากขอบในตําแหน่งอื่นในองค์ประกอบที่แชร์ 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 เรามีองค์ประกอบต่อไปนี้ที่แชร์กัน

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