Przejścia między elementami współdzielonymi to bezproblemowy sposób na przechodzenie między komponentami, które mają spójne treści. Często są one używane do nawigacji, umożliwiając wizualne połączenie różnych ekranów podczas przechodzenia przez nie przez użytkownika.
Na przykład w tym filmie widać, jak obraz i tytuł przekąski są udostępniane z strony z informacjami o produkcie na stronę z szczegółami.
W usłudze Compose jest kilka interfejsów API na najwyższym poziomie, które ułatwiają tworzenie współdzielonych elementów:
SharedTransitionLayout
: układ zewnętrzny wymagany do implementowania przejść elementów współdzielonych. Dostarcza onSharedTransitionScope
. Aby móc używać modyfikatorów elementu współdzielonego, komponenty muszą znajdować się w elementachSharedTransitionScope
.Modifier.sharedElement()
: modyfikator, który wskazuje komponentowiSharedTransitionScope
, że należy dopasować go do innego komponentu.Modifier.sharedBounds()
: modyfikator, który wskazujeSharedTransitionScope
, że zasięg tego komponentu powinien być używany jako zasięg kontenera, w którym ma nastąpić przejście. W przeciwieństwie dosharedElement()
,sharedBounds()
jest przeznaczony do treści wizualnie różniących się od siebie.
Podczas tworzenia elementów współdzielonych w Compose ważne jest to, jak elementy te współdziałają z nakładkami i przycinaniem. Aby dowiedzieć się więcej na ten temat, zapoznaj się z sekcją Odcięcie i nakładki.
Podstawowe zastosowanie
W tej sekcji zostanie utworzone przejście z mniejszego elementu „lista” do większego elementu z informacjami szczegółowymi:
Najlepiej używać komponentu Modifier.sharedElement()
w połączeniu z komponentem AnimatedContent
, AnimatedVisibility
lub NavHost
, ponieważ komponenty te automatycznie zarządzają przejściami między komponentami.
Punktem wyjścia jest podstawowy element AnimatedContent
, który zawiera komponenty MainContent
i DetailsContent
, zanim dodasz elementy wspólne:
Aby animować wspólne elementy między tymi dwoma układami, otacz kompozycję
AnimatedContent
elementemSharedTransitionLayout
. Zakresy zSharedTransitionLayout
iAnimatedContent
są przekazywane doMainContent
iDetailsContent
: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 ) } } }
Dodaj
Modifier.sharedElement()
do łańcucha modyfikatorów w komponowalnych, które pasują do siebie. Utwórz obiektSharedContentState
i zapamiętaj go za pomocąrememberSharedContentState()
. ObiektSharedContentState
przechowuje unikalny klucz, który określa udostępniane elementy. Podaj unikalny klucz, aby zidentyfikować treści, i użyj wartościrememberSharedContentState()
, aby zapamiętać element. WartośćAnimatedContentScope
jest przekazywana do modyfikatora, który służy do koordynowania animacji.@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 ) // ... } } }
Aby uzyskać informacje o tym, czy wystąpiło dopasowanie elementów wspólnych, wyodrębnij zmienną rememberSharedContentState()
i utwórz zapytanie isMatchFound
.
Powoduje to automatyczną animację:
Możesz zauważyć, że kolor tła i rozmiar całego kontenera nadal korzystają z domyślnych ustawień AnimatedContent
.
Różnica między wspólnymi zasięgami a wspólnymi elementami
Modifier.sharedBounds()
jest podobny do Modifier.sharedElement()
.
Modyfikatory różnią się jednak w tych aspektach:
sharedBounds()
jest przeznaczony do treści, które różnią się wizualnie, ale powinny dotyczyć tego samego obszaru w różnych stanach, podczas gdysharedElement()
wymaga, aby treści były takie same.- W przypadku
sharedBounds()
treści wchodzące i wychodzące z ekranu są widoczne podczas przejścia między 2 stanami, natomiast w przypadkusharedElement()
renderowane są tylko treści docelowe w ramach przekształcania.Modifier.sharedBounds()
ma parametryenter
iexit
, które określają sposób przejścia między treściami, podobnie jak w przypadkuAnimatedContent
. - Najczęstszym przypadkiem użycia funkcji
sharedBounds()
jest przekształcenie kontenera, natomiast w przypadku funkcjisharedElement()
przykładowym przypadkiem użycia jest przejście główne. - Jeśli używasz komponentów
Text
, zalecamy użycie atrybutusharedBounds()
, aby obsługiwać zmiany czcionki, takie jak przejście między kursywą a pogrubieniem lub zmiany koloru.
W poprzednim przykładzie dodanie Modifier.sharedBounds()
do Row
i Column
w 2 różnych scenariuszach pozwoli nam udostępnić granice tych 2 elementów i wykonać animację przejścia, która pozwoli im się powiększać naprzemiennie:
@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() ) // ... ) { // ... } } }
Omówienie zakresów
Aby używać Modifier.sharedElement()
, komponent musi znajdować się w SharedTransitionScope
. Komponent SharedTransitionLayout
zapewnia SharedTransitionScope
. Upewnij się, że umieszczasz je na tym samym najwyższym poziomie hierarchii interfejsu użytkownika, który zawiera elementy, które chcesz udostępniać.
Ogólnie komponenty powinny być umieszczane w elementach AnimatedVisibilityScope
. Zwykle jest to realizowane za pomocą funkcji AnimatedContent
do przełączania się między elementami składanymi lub bezpośrednio za pomocą funkcji AnimatedVisibility
lub funkcji składanej NavHost
, chyba że zarządzasz widocznością ręcznie. Aby używać wielu zakresów, zapisz wymagane zakresy w CompositionLocal, użyj odbiorników kontekstu w Kotlinie lub przekaż zakresy jako parametry do swoich funkcji.
Użyj CompositionLocals
, gdy masz wiele zakresów, które chcesz śledzić, lub głęboko zagnieżdżoną hierarchię. CompositionLocal
pozwala wybrać dokładne zakresy do zapisania i użycia. Z drugiej strony, gdy używasz odbiorników kontekstu, inne układy w hierarchii mogą przypadkowo zastąpić podane zakresy.
Jeśli na przykład masz wiele zagnieżdżonych AnimatedContent
, zakresy mogą zostać zastąpione.
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") } // ... } } } }
Jeśli hierarchia nie jest głęboko zagnieżdżona, możesz przekazać zakresy jako parametry:
@Composable fun MainContent( animatedVisibilityScope: AnimatedVisibilityScope, sharedTransitionScope: SharedTransitionScope ) { } @Composable fun Details( animatedVisibilityScope: AnimatedVisibilityScope, sharedTransitionScope: SharedTransitionScope ) { }
Udostępnione elementy użytkownikowi AnimatedVisibility
W poprzednich przykładach pokazaliśmy, jak używać elementów współdzielonych w komponencie AnimatedContent
, ale elementy współdzielone działają też w komponencie AnimatedVisibility
.
Na przykład w tym przykładzie siatki leniwej każdy element jest zawijany w element AnimatedVisibility
. Gdy użytkownik kliknie element, treść zostanie wyciągnięta z interfejsu do komponentu przypominającego okno dialogowe.
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 } ) }
Kolejność modyfikatorów
W przypadku funkcji Modifier.sharedElement()
i Modifier.sharedBounds()
istotna jest kolejność modyfikatorów, podobnie jak w przypadku reszty funkcji w edytorze. Nieprawidłowe umieszczenie modyfikatorów wpływających na rozmiar może powodować nieoczekiwane przeskakiwanie elementów podczas dopasowywania elementów współdzielonych.
Jeśli na przykład umieścisz modyfikator wypełnienia w innej pozycji w przypadku 2 wspólnych elementów, animacja będzie się wizualnie różnić.
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 ) } } } }
Dopasowane granice |
Niezgodne granice: zwróć uwagę, że animacja udostępnionego elementu wygląda nieco dziwnie, ponieważ musi dostosować rozmiar do nieprawidłowych granic |
---|---|
Modyfikatory używane przed modyfikatorami elementu udostępnionego zapewniają ograniczenia dla modyfikatorów elementu udostępnionego, które są następnie wykorzystywane do wyprowadzenia początkowych i docelowych granic oraz animacji granic.
Modyfikatory używane po modyfikatorach elementu współdzielonego korzystają z ograniczeń z poprzedniego etapu, aby zmierzyć i obliczyć rozmiar docelowy elementu podrzędnego. Modyfikatory elementu wspólnego tworzą serię animowanych ograniczeń, aby stopniowo przekształcać dziecko z początkowego rozmiaru do rozmiaru docelowego.
Wyjątkiem jest użycie resizeMode = ScaleToBounds()
do animacji lub Modifier.skipToLookaheadSize()
do komponentu. W takim przypadku kompozytor rozmieszcza element potomny, korzystając z ograniczeń docelowych, a do wykonania animacji używa współczynnika skali zamiast zmieniać rozmiar samego układu.
Unikalne klucze
Podczas pracy z zaawansowanymi współdzielonymi elementami warto utworzyć klucz, który nie jest ciągiem znaków, ponieważ ciągi znaków mogą być podatne na błędy podczas dopasowywania. Aby dopasowania mogły wystąpić, każdy klucz musi być niepowtarzalny. Na przykład w Jetsnack mamy te elementy wspólne:
Możesz utworzyć enum, aby reprezentować typ elementu współdzielonego. W tym przykładzie karta może się wyświetlać w różnych miejscach na ekranie głównym, np. w sekcji „Popularne” i „Polecane”. Możesz utworzyć klucz, który zawiera snackId
, origin
(„Popularne” lub „Polecane”) oraz type
udostępnianego elementu, który będzie udostępniany:
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 ) ) // ... }
W przypadku kluczy zalecamy używanie klas danych, ponieważ implementują one hashCode()
i isEquals()
.
Ręczne zarządzanie widocznością udostępnionych elementów
Jeśli nie używasz elementów AnimatedVisibility
ani AnimatedContent
, widoczności udostępnionych elementów możesz zarządzać samodzielnie. Użyj elementu Modifier.sharedElementWithCallerManagedVisibility()
i utwórz własne wyrażenie warunkowe, które określa, kiedy element ma być widoczny:
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) } }
Obecne ograniczenia
Te interfejsy API mają kilka ograniczeń. Najważniejsze:
- Nieobsługiwana jest interoperacyjność między widokami a edytorem. Dotyczy to każdego kompozytu, który zawiera
AndroidView
, np.Dialog
. - Automatyczna animacja nie jest obsługiwana w przypadku tych elementów:
- Komponenty udostępnionego obrazu:
ContentScale
nie jest domyślnie animowany. Przyciąga się do ustawionego końca.ContentScale
- Wycinanie kształtów – nie ma wbudowanego wsparcia dla automatycznej animacji między kształtami, np. animacji od kwadratu do koła podczas przejścia między elementami.
- W przypadku nieobsługiwanych przypadków użyj wartości
Modifier.sharedBounds()
zamiastsharedElement()
i dodaj do elementów wartośćModifier.animateEnterExit()
.
- Komponenty udostępnionego obrazu: