Hiệu ứng chuyển đổi thành phần 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 các thành phần đó. Các màn hình này thường được dùng để điều hướng, cho phép bạn kết nối trực quan nhiều màn hình 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 đến trang chi tiết.
Trong Compose, có một số API cấp cao giúp bạn tạo phần tử dùng chung:
SharedTransitionLayout
: Bố cục ngoài cùng cần thiết để triển khai các hiệu ứng chuyển đổi thành phần dùng chung. Lớp này cung cấp mộtSharedTransitionScope
. Các thành phần kết hợp cần phải nằm trongSharedTransitionScope
để sử dụng đối tượng sửa đổi phần tử dùng chung.Modifier.sharedElement()
: Đối tượng sửa đổi gắn cờ đếnSharedTransitionScope
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ờ đếnSharedTransitionScope
mà các giới hạn của thành phần kết hợp này sẽ được dùng làm giới hạn của vùng chứa ở nơi quá trình chuyển đổi sẽ diễn ra. Trái ngược vớisharedElement()
,sharedBounds()
được thiết kế cho nội dung trực quan khác.
Một khái niệm quan trọng khi tạo các thành phần dùng chung trong Compose là cách các thành phần này hoạt động với lớp phủ và tính năng 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
Hiệu ứng chuyển đổi sau đây sẽ được xây dựng 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:
![](https://developer.android.google.cn/static/develop/ui/compose/images/animations/shared-element/basic_shared_element_jetsnack.gif?hl=vi)
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ó, có thành phần kết hợp MainContent
và DetailsContent
trước khi thêm các thành phần dùng chung:
![](https://developer.android.google.cn/static/develop/ui/compose/images/animations/shared-element/basic_no_animation_jetsnack.gif?hl=vi)
AnimatedContent
mà không có 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 dùng chung giữa 2 bố cục, hãy bao quanh thành phần kết hợp
AnimatedContent
bằngSharedTransitionLayout
. Các phạm vi từSharedTransitionLayout
vàAnimatedContent
được chuyể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 thành phần kết hợp trên 2 thành phần kết hợp phù hợp. 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 giúp xác định các phần tử được chia sẻ. Cung cấp một khoá duy nhất để xác định nội dung và sử dụngrememberSharedContentState()
để mục được 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 khớp với phần tử dùng chung hay không, hãy trích xuất rememberSharedContentState()
thành một biến và truy vấn isMatchFound
.
Điều gì dẫn đến ảnh động tự động sau đây:
![](https://developer.android.google.cn/static/develop/ui/compose/images/animations/shared-element/basic_shared_element_jetsnack.gif?hl=vi)
Bạn có thể nhận thấy 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.
Giới hạn chung so với phần tử dùng chung
Modifier.sharedBounds()
tương tự như Modifier.sharedElement()
.
Tuy nhiên, đối tượng sửa đổi sẽ khác nhau ở những điểm sau:
sharedBounds()
dành cho nội dung có hình ảnh khác nhau nhưng cần chia sẻ cùng một khu vực giữa các trạng thái, trong khisharedElement()
yêu cầu nội dung giống nhau.- Với
sharedBounds()
, nội dung vào và thoát 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 mục tiêu được hiển thị trong giới hạn biến đổi.Modifier.sharedBounds()
có các tham sốenter
vàexit
để chỉ định cách nội dung cần 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 mẫu là hiệu ứng chuyển đổi chính. - Khi sử dụng thành phần kết hợp
Text
,sharedBounds()
được ưu tiên hỗ trợ các thay đổi về phông chữ, chẳng hạn như chuyển đổi giữa kiểu in nghiêng và in đậm hoặc thay đổi màu sắc.
Từ ví dụ trước, việc thêm Modifier.sharedBounds()
vào Row
và Column
trong hai tình huống khác nhau sẽ cho phép chúng ta chia sẻ các giới hạn của hai và thực hiện ảnh động chuyển đổi, cho phép các thành phần này phát triển với 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 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
. Hãy nhớ đặ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 có chứa các phần tử bạn muốn chia sẻ.
Nhìn chung, bạn cũng nên đặt các thành phần kết hợp bên trong 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 AnimatedVisibility
trực tiếp hay 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 của bạn trong CompositionLocal, sử dụng trình tiếp nhận ngữ cảnh trong Kotlin hoặc truyền các phạm vi dưới dạng tham số vào hàm.
Sử dụng CompositionLocals
trong trường hợp bạn có nhiều phạm vi cần theo dõi hoặc có một hệ phân cấp được lồng 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 trình tiếp nhận ngữ cảnh, các bố cục khác trong hệ phân cấp của bạn có thể vô tình ghi đè các phạm vi đã 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ệ phân cấp của bạn không được lồng sâu, bạn có thể chuyển 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ử được chia sẻ với AnimatedVisibility
Các ví dụ trước cho thấy cách sử dụng các thành phần dùng chung với AnimatedContent
, nhưng các thành phần 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 gói trong AnimatedVisibility
. Khi người dùng nhấp vào mục, nội dung có
hiệu ứng hình ảnh đượ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( state = 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ự đối tượng sửa đổi
Với Modifier.sharedElement()
và Modifier.sharedBounds()
, chuỗi thứ tự của đối tượng sửa đổi rất quan trọng, giống như phần còn lại của Compose. Vị trí không chính xác của các đối tượng sửa đổi ảnh hưởng đến kích thước có thể gây ra bước nhảy hình ảnh không mong muốn trong quá trình so khớp phần tử được chia sẻ.
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 thành phần 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 ) } } } }
Giới hạn phù hợp |
Giới hạn không khớp: Lưu ý cách ảnh động phần tử dùng chung có vẻ hơi lệch vì cần đổi kích thước thành giới hạn không chính xác |
---|---|
Đối tượng sửa đổi phần tử dùng chung đã sử dụng trước đối tượng sửa đổi phần tử dùng chung cung cấp các quy tắc ràng buộc cho các đối tượng sửa đổi phần tử dùng chung. Sau đó, đối tượng sửa đổi này được dùng để lấy giới hạn ban đầu và giới hạn mục tiêu, sau đó là ảnh động giới hạn.
Đối tượng sửa đổi được sử dụng sau đối tượng sửa đổi phần tử dùng chung sử dụng các điều kiện 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 phần tử con. Đối tượng sửa đổi phần tử dùng chung tạo ra một loạt các điều kiện ràng buộc dạng ảnh động để dần dần chuyển đổi phần tử 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ách sử dụng các điều kiện 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 khoá không phải là chuỗi, vì các chuỗi có thể dễ gặp lỗi khi so khớp. Mỗi khoá phải là duy nhất để kết quả trùng khớp xảy ra. Ví dụ: trong Jetsnack, chúng ta có các thành phần chung sau đây:
![](https://developer.android.google.cn/static/develop/ui/compose/images/animations/shared-element/unique_keys_shared_elements.jpeg?hl=vi)
Bạn có thể tạo một enum để đại diện cho loại phần tử dùng chung. Trong ví dụ này, toàn bộ thẻ món ăn vặt cũng có thể xuất hiện từ nhiều vị trí trên màn hình chính, chẳng hạn như trong phần "Phổ biến" và "Đề 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 sử dụng các lớp dữ liệu cho khoá vì các lớp này triển khai hashCode()
và
isEquals()
.
Quản lý chế độ hiển thị của các thành phần dùng chung theo cách thủ công
Trong trường hợp không dùng AnimatedVisibility
hoặc AnimatedContent
, bạn có thể tự quản lý chế độ hiển thị của phần tử dùng chung. Sử dụng Modifier.sharedElementWithCallerManagedVisibility()
và cung cấp điều kiện của riêng bạn để xác định thời điểm một mục có thể xuất hiện hoặc không xuất hiện:
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) } }
Hạn chế hiện tại
Những 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 Khung hiển thị và Compose. Điều này bao gồm mọi thành phần kết hợp gói
AndroidView
, chẳng hạn nhưDialog
. - Không hỗ trợ ảnh động tự động cho những mục sau:
- Thành phần kết hợp Hình ảnh được chia sẻ:
ContentScale
không được tạo ảnh động theo mặc định. Nó điều chỉnh theo điểm kết thúc đã đặtContentScale
.
- Cắt 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 sử 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 được chia sẻ: