Chuyển đổi phần tử dùng chung là một cách liền mạch để chuyển đổi giữa các thành phần kết hợp có nội dung nhất quán giữa chúng. Chúng thường được dùng để điều hướng, cho phép bạn kết nối trực quan các màn hình khác nhau khi người dùng di chuyển giữa các màn hình đó.
Ví dụ: trong video sau, bạn có thể thấy hình ảnh và tiêu đề của món ăn nhẹ được chia sẻ từ trang danh sách sang trang chi tiết.
Trong Compose, có một số API cấp cao giúp bạn tạo các phần tử dùng chung:
SharedTransitionLayout
: Bố cục ngoài cùng cần thiết để triển khai hiệu ứng chuyển đổi phần tử dùng chung. Thư viện này cung cấp mộtSharedTransitionScope
. Các thành phần kết hợp cần nằm trongSharedTransitionScope
để sử dụng các đối tượng sửa đổi phần tử được chia sẻ.Modifier.sharedElement()
: Đối tượng sửa đổi gắn cờ choSharedTransitionScope
thành phần kết hợp cần được so khớp với một thành phần kết hợp khác.Modifier.sharedBounds()
: Đối tượng sửa đổi gắn cờ choSharedTransitionScope
rằng ranh giới của thành phần kết hợp này sẽ được dùng làm ranh giới vùng chứa cho vị trí diễn ra hiệu ứng chuyển đổi. Ngược lại vớisharedElement()
,sharedBounds()
được thiết kế cho nội dung có sự khác biệt về hình ảnh.
Một khái niệm quan trọng khi tạo các phần tử dùng chung trong Compose là cách các phần tử này hoạt động với lớp phủ và thao tác cắt. Hãy xem phần đoạn cắt và lớp phủ để tìm hiểu thêm về chủ đề quan trọng này.
Cách sử dụng cơ bản
Quá trình chuyển đổi sau đây sẽ được tạo trong phần này, chuyển đổi từ mục "danh sách" nhỏ hơn sang mục chi tiết lớn hơn:

Cách tốt nhất để sử dụng Modifier.sharedElement()
là kết hợp với AnimatedContent
, AnimatedVisibility
hoặc NavHost
, vì cách này sẽ tự động quản lý quá trình chuyển đổi giữa các thành phần kết hợp cho bạn.
Điểm bắt đầu là một AnimatedContent
cơ bản hiện có chứa MainContent
và thành phần kết hợp DetailsContent
trước khi thêm các phần tử dùng chung:

AnimatedContent
mà không có bất kỳ hiệu ứng chuyển đổi phần tử dùng chung nào.Để tạo ảnh động cho các thành phần được chia sẻ giữa hai bố cục, hãy đặt thành phần kết hợp
AnimatedContent
trongSharedTransitionLayout
. Các phạm vi từSharedTransitionLayout
vàAnimatedContent
được truyền đếnMainContent
và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 ) } } }
Thêm
Modifier.sharedElement()
vào chuỗi đối tượng sửa đổi có khả năng kết hợp trên hai thành phần kết hợp khớp nhau. Tạo một đối tượngSharedContentState
và ghi nhớ đối tượng đó bằngrememberSharedContentState()
. Đối tượngSharedContentState
đang lưu trữ khoá duy nhất xác định các phần tử được chia sẻ. Cung cấp một khoá riêng biệt để xác định nội dung và sử dụngrememberSharedContentState()
cho mục cần ghi nhớ.AnimatedContentScope
được truyền vào đối tượng sửa đổi dùng để điều phối ảnh động.@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 ) // ... } } }
Để biết thông tin về việc có xảy ra trường hợp trùng khớp phần tử dùng chung hay không, hãy trích xuất rememberSharedContentState()
vào một biến và truy vấn isMatchFound
.
Điều này dẫn đến ảnh động tự động sau:

Bạn có thể nhận thấy rằng màu nền và kích thước của toàn bộ vùng chứa vẫn sử dụng chế độ cài đặt AnimatedContent
mặc định.
Ranh giới dùng chung so với phần tử dùng chung
Modifier.sharedBounds()
tương tự như Modifier.sharedElement()
.
Tuy nhiên, các đối tượng sửa đổi này khác nhau ở những điểm sau:
sharedBounds()
dành cho nội dung có sự khác biệt về hình ảnh nhưng phải dùng chung một vùng giữa các trạng thái, trong khisharedElement()
yêu cầu nội dung phải giống nhau.- Với
sharedBounds()
, nội dung vào và ra khỏi màn hình sẽ hiển thị trong quá trình chuyển đổi giữa hai trạng thái, trong khi vớisharedElement()
, chỉ nội dung đích được kết xuất trong các ranh giới biến đổi.Modifier.sharedBounds()
có các tham sốenter
vàexit
để chỉ định cách nội dung chuyển đổi, tương tự như cáchAnimatedContent
hoạt động. - Trường hợp sử dụng phổ biến nhất cho
sharedBounds()
là mẫu biến đổi vùng chứa, trong khi đối vớisharedElement()
, trường hợp sử dụng ví dụ là hiệu ứng chuyển đổi chính. - Khi sử dụng thành phần kết hợp
Text
, bạn nên dùngsharedBounds()
để hỗ trợ các thay đổi về phông chữ, chẳng hạn như chuyển đổi giữa chữ in nghiêng và chữ in đậm hoặc thay đổi màu sắc.
Trong ví dụ trước, việc thêm Modifier.sharedBounds()
vào Row
và Column
trong hai trường hợp khác nhau sẽ cho phép chúng ta chia sẻ ranh giới của hai thành phần này và thực hiện ảnh động chuyển đổi, cho phép chúng phát triển giữa nhau:
@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() ) // ... ) { // ... } } }
Tìm hiểu về phạm vi
Để sử dụng Modifier.sharedElement()
, thành phần kết hợp cần phải nằm trong SharedTransitionScope
. Thành phần kết hợp SharedTransitionLayout
cung cấp SharedTransitionScope
. Đảm bảo đặt ở cùng một điểm cấp cao nhất trong hệ phân cấp giao diện người dùng chứa các phần tử mà bạn muốn chia sẻ.
Nhìn chung, các thành phần kết hợp cũng phải được đặt bên trong một AnimatedVisibilityScope
. Điều này thường được cung cấp bằng cách sử dụng AnimatedContent
để chuyển đổi giữa các thành phần kết hợp hoặc khi sử dụng trực tiếp AnimatedVisibility
, hoặc bằng hàm có khả năng kết hợp NavHost
, trừ phi bạn quản lý chế độ hiển thị theo cách thủ công. Để sử dụng nhiều phạm vi, hãy lưu các phạm vi bắt buộc trong CompositionLocal
, sử dụng các đối tượng nhận ngữ cảnh trong Kotlin hoặc truyền các phạm vi làm tham số cho các hàm của bạn.
Sử dụng CompositionLocals
trong trường hợp bạn có nhiều phạm vi cần theo dõi hoặc một hệ thống phân cấp lồng nhau sâu. CompositionLocal
cho phép bạn chọn phạm vi chính xác để lưu và sử dụng. Mặt khác, khi bạn sử dụng các receiver theo bối cảnh, các bố cục khác trong hệ phân cấp có thể vô tình ghi đè các phạm vi được cung cấp.
Ví dụ: nếu bạn có nhiều AnimatedContent
lồng nhau, thì các phạm vi có thể bị ghi đè.
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") } // ... } } } }
Ngoài ra, nếu hệ thống phân cấp của bạn không được lồng sâu, bạn có thể truyền các phạm vi xuống dưới dạng tham số:
@Composable fun MainContent( animatedVisibilityScope: AnimatedVisibilityScope, sharedTransitionScope: SharedTransitionScope ) { } @Composable fun Details( animatedVisibilityScope: AnimatedVisibilityScope, sharedTransitionScope: SharedTransitionScope ) { }
Các phần tử dùng chung với AnimatedVisibility
Các ví dụ trước cho thấy cách sử dụng các phần tử dùng chung với AnimatedContent
, nhưng các phần tử dùng chung cũng hoạt động với AnimatedVisibility
.
Ví dụ: trong ví dụ về lưới tải từng phần này, mỗi phần tử được bao bọc trong AnimatedVisibility
. Khi người dùng nhấp vào mục này, nội dung sẽ có hiệu ứng hình ảnh là được kéo ra khỏi giao diện người dùng vào một thành phần giống như hộp thoại.
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 } ) }
AnimatedVisibility
.Thứ tự của đối tượng sửa đổi
Với Modifier.sharedElement()
và Modifier.sharedBounds()
, thứ tự của chuỗi đối tượng sửa đổi rất quan trọng, giống như các thành phần còn lại của Compose. Việc đặt sai vị trí của các đối tượng sửa đổi ảnh hưởng đến kích thước có thể gây ra các bước nhảy hình ảnh không mong muốn trong quá trình so khớp phần tử dùng chung.
Ví dụ: nếu bạn đặt một đối tượng sửa đổi khoảng đệm ở một vị trí khác trên 2 phần tử dùng chung, thì sẽ có sự khác biệt về hình ảnh trong ảnh động.
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 ) } } } }
Ranh giới trùng khớp |
Ranh giới không khớp: Lưu ý cách ảnh động phần tử được chia sẻ xuất hiện hơi lệch vì cần đổi kích thước thành ranh giới không chính xác |
---|---|
Các đối tượng sửa đổi được dùng trước các đối tượng sửa đổi phần tử dùng chung sẽ cung cấp các ràng buộc cho các đối tượng sửa đổi phần tử dùng chung. Sau đó, các đối tượng sửa đổi này được dùng để lấy các ranh giới ban đầu và ranh giới mục tiêu, rồi đến hoạt ảnh ranh giới.
Các đối tượng sửa đổi được dùng sau khi các đối tượng sửa đổi của phần tử dùng chung sử dụng các ràng buộc từ trước để đo lường và tính toán kích thước mục tiêu của thành phần con. Các đối tượng sửa đổi thành phần dùng chung tạo ra một loạt các ràng buộc có ảnh động để dần chuyển đổi thành phần con từ kích thước ban đầu sang kích thước mục tiêu.
Trường hợp ngoại lệ là nếu bạn sử dụng resizeMode = ScaleToBounds()
cho ảnh động hoặc Modifier.skipToLookaheadSize()
trên một thành phần kết hợp. Trong trường hợp này, Compose sẽ bố trí thành phần con bằng các ràng buộc mục tiêu và thay vào đó, sử dụng hệ số tỷ lệ để thực hiện ảnh động thay vì tự thay đổi kích thước bố cục.
Khoá duy nhất
Khi làm việc với các phần tử dùng chung phức tạp, bạn nên tạo một khoá không phải là chuỗi, vì các chuỗi có thể dễ bị lỗi khi so khớp. Mỗi khoá phải là duy nhất để có thể so khớp. Ví dụ: trong Jetsnack, chúng ta có các phần tử dùng chung sau:

Bạn có thể tạo một enum để biểu thị loại phần tử dùng chung. Trong ví dụ này, toàn bộ thẻ thông tin nhanh cũng có thể xuất hiện ở nhiều vị trí khác nhau trên màn hình chính, chẳng hạn như trong phần "Phổ biến" và "Được đề xuất". Bạn có thể tạo một khoá có snackId
, origin
("Phổ biến" / "Được đề xuất") và type
của phần tử dùng chung sẽ được chia sẻ:
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 ) ) // ... }
Bạn nên dùng các lớp dữ liệu cho khoá vì chúng triển khai hashCode()
và isEquals()
.
Quản lý chế độ hiển thị của các phần tử dùng chung theo cách thủ công
Trong trường hợp không sử dụng AnimatedVisibility
hoặc AnimatedContent
, bạn có thể tự quản lý khả năng hiển thị của phần tử dùng chung. Sử dụng Modifier.sharedElementWithCallerManagedVisibility()
và cung cấp điều kiện riêng của bạn để xác định thời điểm một mục sẽ hiển thị hay không:
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) } }
Các điểm hạn chế hiện tại
Các API này có một số hạn chế. Đáng chú ý nhất:
- Không hỗ trợ khả năng tương tác giữa Views và Compose. Điều này bao gồm mọi thành phần kết hợp bao bọc
AndroidView
, chẳng hạn nhưDialog
hoặcModalBottomSheet
. - Không có tính năng hỗ trợ ảnh động tự động cho những nội dung sau:
- Thành phần kết hợp Hình ảnh dùng chung:
ContentScale
không được tạo hiệu ứng động theo mặc định. Đường này sẽ khớp với điểm cuối đã đặtContentScale
.
- Cắt theo hình dạng – Không có tính năng hỗ trợ tích hợp sẵn cho ảnh động tự động giữa các hình dạng – ví dụ: tạo ảnh động từ hình vuông sang hình tròn khi mục chuyển đổi.
- Đối với các trường hợp không được hỗ trợ, hãy dùng
Modifier.sharedBounds()
thay vìsharedElement()
và thêmModifier.animateEnterExit()
vào các mục.
- Thành phần kết hợp Hình ảnh dùng chung: