Przejścia między elementami współdzielonymi to płynny sposób przechodzenia między komponentami, które mają spójną treść. Często służą do nawigacji, umożliwiając wizualne połączenie różnych ekranów, gdy użytkownik przechodzi między nimi.
Na przykład w tym filmie widać, że obraz i tytuł przekąski są udostępniane ze strony z listą na stronę szczegółów.
W Compose jest kilka interfejsów API wysokiego poziomu, które pomagają tworzyć elementy współdzielone:
SharedTransitionLayout
: najbardziej zewnętrzny układ wymagany do wdrożenia przejść między udostępnionymi elementami. ZapewniaSharedTransitionScope
. Funkcje kompozycyjne muszą znajdować się w elemencieSharedTransitionScope
, aby można było używać modyfikatorów elementów udostępnionych.Modifier.sharedElement()
: modyfikator, który informujeSharedTransitionScope
o kompozycji, która powinna być dopasowana do innej kompozycji.Modifier.sharedBounds()
: modyfikator, który informujeSharedTransitionScope
, że granice tego komponentu kompozycyjnego powinny być używane jako granice kontenera, w którym ma nastąpić przejście. W przeciwieństwie dosharedElement()
,sharedBounds()
jest przeznaczony do treści, które różnią się wizualnie.
Ważną kwestią podczas tworzenia elementów wspólnych w Compose jest sposób, w jaki działają one z nakładkami i przycinaniem. Więcej informacji na ten ważny temat znajdziesz w sekcji przycinanie i nakładki.
Podstawowe użycie
W tej sekcji zostanie utworzone przejście od mniejszego elementu „listy” do większego elementu szczegółowego:

Najlepiej używać Modifier.sharedElement()
w połączeniu z AnimatedContent
, AnimatedVisibility
lub NavHost
, ponieważ automatycznie zarządza przejściem między funkcjami kompozycyjnymi.
Punktem wyjścia jest istniejący podstawowy element AnimatedContent
, który ma elementy MainContent
i DetailsContent
, które można łączyć przed dodaniem elementów wspólnych:

AnimatedContent
bez przejść między elementami współdzielonymi.Aby animować udostępnione elementy między dwoma układami, otocz funkcję kompozycyjną
AnimatedContent
funkcjąSharedTransitionLayout
. 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 kompozycyjnych w 2 kompozycjach, które pasują. Utwórz obiektSharedContentState
i zapamiętaj go za pomocąrememberSharedContentState()
. ObiektSharedContentState
przechowuje unikalny klucz, który określa udostępniane elementy. Podaj unikalny klucz identyfikujący treść i użyjrememberSharedContentState()
, aby zapamiętać element. W modyfikatorze przekazywany jest parametrAnimatedContentScope
, 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 elementu udostępnionego, wyodrębnij rememberSharedContentState()
do zmiennej i wykonaj zapytanie isMatchFound
.
Spowoduje to automatyczną animację:

Możesz zauważyć, że kolor tła i rozmiar całego kontenera nadal korzystają z ustawień domyślnych AnimatedContent
.
Wspólne granice a wspólny element
Modifier.sharedBounds()
jest podobne do Modifier.sharedElement()
.
Modyfikatory różnią się jednak w tych aspektach:
sharedBounds()
dotyczy treści, które różnią się wizualnie, ale powinny obejmować ten sam obszar między stanami, natomiastsharedElement()
oczekuje, że treść będzie taka sama.- W przypadku
sharedBounds()
treść wchodząca i wychodząca z ekranu jest widoczna podczas przejścia między dwoma stanami, natomiast w przypadkusharedElement()
w przekształcanych granicach renderowana jest tylko treść docelowa.Modifier.sharedBounds()
ma parametryenter
iexit
, które określają sposób przejścia treści, podobnie jak w przypadku funkcjiAnimatedContent
. - Najczęstszym przypadkiem użycia
sharedBounds()
jest wzorzec przekształcenia kontenera, natomiast w przypadkusharedElement()
przykładem jest przejście do elementu głównego. - W przypadku komponentów kompozycyjnych
Text
zaleca się używaniesharedBounds()
, aby obsługiwać zmiany czcionki, takie jak przejścia 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, umożliwiając im powiększanie się względem siebie:
@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żyć Modifier.sharedElement()
, funkcja kompozycyjna musi znajdować się w SharedTransitionScope
. Funkcja kompozycyjna SharedTransitionLayout
udostępnia
SharedTransitionScope
. Umieść go w tym samym punkcie najwyższego poziomu w hierarchii interfejsu, który zawiera elementy, które chcesz udostępnić.
Zwykle elementy kompozycyjne powinny być umieszczane w obrębie elementu AnimatedVisibilityScope
. Zwykle jest to zapewniane przez użycie AnimatedContent
do przełączania się między funkcjami kompozycyjnymi lub bezpośrednio przez AnimatedVisibility
albo przez funkcję kompozycyjną NavHost
, chyba że widoczność jest zarządzana ręcznie. Aby użyć wielu zakresów, zapisz wymagane zakresy w CompositionLocal
, użyj odbiorników kontekstu w Kotlinie lub przekaż zakresy jako parametry do funkcji.
Użyj CompositionLocals
, jeśli masz wiele zakresów do śledzenia lub głęboko zagnieżdżoną hierarchię. CompositionLocal
umożliwia wybór dokładnych zakresów 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 elementów 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 udostępnionych z elementem AnimatedContent
, ale elementy udostępnione działają też z elementem AnimatedVisibility
.
Na przykład w tym przykładzie leniwej siatki każdy element jest opakowany w tag AnimatedVisibility
. Gdy użytkownik kliknie element, treść będzie miała efekt wizualny wyciągania z interfejsu do komponentu przypominającego okno.
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
.Kolejność modyfikatorów
W przypadku funkcji Modifier.sharedElement()
i Modifier.sharedBounds()
kolejność łańcucha modyfikatorów ma znaczenie, podobnie jak w przypadku pozostałych elementów Compose. Nieprawidłowe umieszczenie modyfikatorów wpływających na rozmiar może powodować nieoczekiwane skoki wizualne podczas dopasowywania wspólnych elementów.
Jeśli na przykład umieścisz modyfikator dopełnienia w różnych miejscach w 2 wspólnych elementach, animacja będzie wyglądać inaczej.
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 |
Niedopasowane granice: zwróć uwagę, że animacja wspólnego elementu wygląda nieco dziwnie, ponieważ musi zmienić rozmiar do nieprawidłowych granic. |
---|---|
Modyfikatory użyte przed modyfikatorami elementu udostępnionego określają ograniczenia modyfikatorów elementu udostępnionego, które są następnie używane do określania początkowych i docelowych granic, a w konsekwencji animacji granic.
Modyfikatory użyte po modyfikatorach elementu udostępnionego używają ograniczeń z wcześniejszego etapu do pomiaru i obliczania rozmiaru docelowego elementu podrzędnego. Modyfikatory udostępnionego elementu tworzą serię animowanych ograniczeń, które stopniowo przekształcają element podrzędny z rozmiaru początkowego w rozmiar docelowy.
Wyjątkiem jest sytuacja, w której używasz resizeMode = ScaleToBounds()
w przypadku animacji lub Modifier.skipToLookaheadSize()
w funkcji kompozycyjnej. W tym przypadku Compose układa element podrzędny zgodnie z ograniczeniami docelowymi i zamiast zmieniać rozmiar układu, używa współczynnika skali do wykonania animacji.
Klucze unikalne
W przypadku złożonych elementów wspólnych warto utworzyć klucz, który nie jest ciągiem tekstowym, ponieważ dopasowywanie ciągów tekstowych może być podatne na błędy. Aby doszło do dopasowania, każdy klucz musi być niepowtarzalny. Na przykład w aplikacji Jetsnack mamy te wspólne elementy:

Możesz utworzyć wyliczenie, aby reprezentować typ udostępnionego elementu. W tym przykładzie cała karta z przekąską może też pojawiać się w różnych miejscach na ekranie głównym, np. w sekcjach „Popularne” i „Polecane”. Możesz utworzyć klucz, który będzie zawierać snackId
, origin
(„Popularne” / „Rekomendowane”) i type
udostępnianego elementu:
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 stosowanie klas danych, ponieważ implementują one hashCode()
i isEquals()
.
Ręczne zarządzanie widocznością udostępnionych elementów
Jeśli nie używasz AnimatedVisibility
ani AnimatedContent
, możesz samodzielnie zarządzać widocznością udostępnionego elementu. Użyj
Modifier.sharedElementWithCallerManagedVisibility()
i podaj własny warunek, który określa, kiedy produkt powinien 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ń. W szczególności:
- Nie jest obsługiwana interoperacyjność między widokami a Compose. Obejmuje to wszystkie funkcje kompozycyjne, które opakowują
AndroidView
, np.Dialog
lubModalBottomSheet
. - Automatyczna obsługa animacji nie jest dostępna w przypadku tych elementów:
- Komponenty kompozycyjne Shared Image:
ContentScale
nie jest domyślnie animowany. Zostanie przyciągnięty do ustawionego końcaContentScale
.
- Przycinanie kształtu – nie ma wbudowanej obsługi automatycznej animacji między kształtami, np. animacji przejścia z kwadratu do koła podczas przejścia elementu.
- W przypadku nieobsługiwanych przypadków użyj
Modifier.sharedBounds()
zamiastsharedElement()
i dodajModifier.animateEnterExit()
do produktów.
- Komponenty kompozycyjne Shared Image: