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 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
: najbardziej zewnętrzny układ wymagany do implementacji przejść elementów wspólnych. 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 go dopasować do innego komponentu.Modifier.sharedBounds()
: modyfikator wskazujący wartośćSharedTransitionScope
, że granice tej funkcji kompozycyjnej powinny być używane jako granice kontenera, w którym powinno 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 działają one w połączeniu z przekryciami i przycinaniem. Zapoznaj się z sekcją o klipach i nakładkach, aby dowiedzieć się więcej o tym ważnym zagadnieniu.
Podstawowe zastosowanie
W tej sekcji zostanie wykonane poniższe przejście, przechodząc z mniejszego elementu na liście do większego elementu szczegółowego:
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 początkowym jest istniejący podstawowy element AnimatedContent
, który zawiera elementy kompozycyjne MainContent
i DetailsContent
, zanim dodasz udostępnione elementy:
Aby wspólne elementy były animowane między 2 układami, otocz kompozycyjny
AnimatedContent
elementSharedTransitionLayout
. Zakresy zSharedTransitionLayout
iAnimatedContent
są przekazywane do metodMainContent
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 komponentach, 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żyjrememberSharedContentState()
dla elementu, który ma zostać zapamiętany. ElementAnimatedContentScope
jest przekazywany 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 utworzenie takiej automatycznej animacji:
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 podobne do Modifier.sharedElement()
.
Modyfikatory różnią się jednak pod kilkoma względami:
sharedBounds()
dotyczy treści, które różnią się wizualnie, ale powinny mieć ten sam obszar w poszczególnych stanach, asharedElement()
oczekuje, że treści będą takie same.- W przypadku
sharedBounds()
treści wchodzące i wychodzące z ekranu są widoczne podczas przejścia między 2 stanami, podczas gdy 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 treści (podobnie jak w przypadku elementuAnimatedContent
). - 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.
Z poprzedniego przykładu dodanie Modifier.sharedBounds()
do pól Row
i Column
w 2 różnych scenariuszach pozwoli nam wyznaczyć granice tych 2 aspektów i wykonać animację przejścia, dzięki czemu będą się one powiększać:
@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() ) // ... ) { // ... } } }
Informacje o zakresach
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 funkcje kompozycyjne powinny być też umieszczone w elemencie AnimatedVisibilityScope
. Zwykle dzieje się tak, gdy używasz funkcji AnimatedContent
do przełączania się między elementami kompozycyjnymi lub używasz bezpośrednio AnimatedVisibility
albo funkcji kompozycyjnej 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
Poprzednie przykłady pokazują, jak używać udostępnionych elementów w AnimatedContent
, ale elementy udostępnione działają też w 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.
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 funkcjach Modifier.sharedElement()
i Modifier.sharedBounds()
kolejność łańcucha modyfikatorów ma znaczenie, tak jak w przypadku pozostałych funkcji Compose. Nieprawidłowe umiejscowienie modyfikatorów wpływających na rozmiar może spowodować nieoczekiwane skoki wizualne podczas dopasowywania wspólnych elementów.
Jeśli np. umieścisz modyfikator dopełnienia w innym miejscu w przypadku 2 udostępnionych elementów, animacja będzie się różnić pod względem wizualnym.
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 ) } } } }
Pasujące granice |
Niedopasowane granice: zwróć uwagę, że animacja udostępnionego elementu wygląda na odrobinę bliższą, 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 elementów udostępnionych tworzą serię animowanych ograniczeń, aby stopniowo przekształcić element podrzędny z rozmiaru początkowego na docelowy.
Wyjątkiem od tej reguły jest użycie polecenia resizeMode = ScaleToBounds()
dla animacji lub Modifier.skipToLookaheadSize()
w przypadku funkcji kompozycyjnej. 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
W przypadku złożonych wspólnych elementów dobrze jest tworzyć klucz, który nie jest ciągiem znaków, ponieważ wtedy ciągi tekstowe mogą być podatne na błędy. 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 zawierający snackId
, origin
(„Popularne”/„Zalecane”) oraz 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 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:
- Elementy kompozycyjne udostępnianych obrazów:
ContentScale
nie jest domyślnie animowany. Ustawienie zostanie przyciągnięte do ustawionego końcaContentScale
.
- 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 nieobsługiwanych przypadkach użyj
Modifier.sharedBounds()
zamiastsharedElement()
i dodaj do produktówModifier.animateEnterExit()
.
- Elementy kompozycyjne udostępnianych obrazów: