Übergänge mit gemeinsam genutzten Elementen sind eine nahtlose Möglichkeit für den Übergang zwischen zusammensetzbaren Funktionen, deren Inhalte einheitlich sind. Sie werden häufig für die Navigation verwendet, sodass Sie verschiedene Bildschirme visuell verbinden können, wenn Nutzer zwischen ihnen wechseln.
Im folgenden Video sehen Sie beispielsweise, wie das Bild und der Titel des Snacks von der Seite „Eintrag“ auf die Detailseite übernommen werden.
In Compose gibt es einige allgemeine APIs, mit denen Sie gemeinsame Elemente erstellen können:
SharedTransitionLayout
: Das äußerste Layout, das zum Implementieren von Übergängen für freigegebene Elemente erforderlich ist. Es stellt einenSharedTransitionScope
bereit. Zusammensetzbare Elemente müssen sich in einemSharedTransitionScope
befinden, damit Modifikatoren für gemeinsam genutzte Elemente verwendet werden können.Modifier.sharedElement()
: Der Modifikator, mit demSharedTransitionScope
die zusammensetzbare Funktion gekennzeichnet wird, die mit einer anderen zusammensetzbaren Funktion abgeglichen werden soll.Modifier.sharedBounds()
: Der Modifikator, mit demSharedTransitionScope
angegeben wird, dass die Grenzen dieser zusammensetzbaren Funktion als Containergrenzen für den Ort verwendet werden sollen, an dem der Übergang stattfinden soll. Im Gegensatz zusharedElement()
istsharedBounds()
für visuell unterschiedliche Inhalte konzipiert.
Ein wichtiges Konzept beim Erstellen gemeinsam genutzter Elemente in Compose ist die Funktionsweise von Overlays und Clipping. Weitere Informationen zu diesem wichtigen Thema findest du im Abschnitt Zuschneiden und Overlays.
Grundlegende Verwendung
In diesem Abschnitt wird die folgende Überleitung erstellt, die vom kleineren Listenelement zum größeren Detailelement führt:

Modifier.sharedElement()
eignet sich am besten in Kombination mit AnimatedContent
, AnimatedVisibility
oder NavHost
, da der Übergang zwischen den Composeables so automatisch für Sie verwaltet wird.
Der Ausgangspunkt ist eine vorhandene einfache AnimatedContent
mit einer zusammensetzbaren Funktion MainContent
und DetailsContent
, bevor gemeinsam genutzte Elemente hinzugefügt werden:

AnimatedContent
ohne Übergänge mit gemeinsamen Elementen.Damit die gemeinsamen Elemente zwischen den beiden Layouts animiert werden, setzen Sie die zusammensetzbare
AnimatedContent
-Datei mitSharedTransitionLayout
. Die Bereiche vonSharedTransitionLayout
undAnimatedContent
werden anMainContent
undDetailsContent
übergeben: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 ) } } }
Füge der Kette mit den Composeable-Modifizierern bei den beiden übereinstimmenden Composeables
Modifier.sharedElement()
hinzu. Erstellen Sie einSharedContentState
-Objekt und merken Sie sich es mitrememberSharedContentState()
. DasSharedContentState
-Objekt speichert den eindeutigen Schlüssel, der bestimmt, welche Elemente gemeinsam genutzt werden. Geben Sie einen eindeutigen Schlüssel an, um den Inhalt zu identifizieren, und verwenden SierememberSharedContentState()
, damit das Element gespeichert wird. DieAnimatedContentScope
wird an den Modifier übergeben, der die Animation koordiniert.@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 ) // ... } } }
Wenn Sie wissen möchten, ob eine Übereinstimmung mit einem gemeinsam genutzten Element vorliegt, extrahieren Sie rememberSharedContentState()
in eine Variable und fragen Sie isMatchFound
ab.
Dies führt zur folgenden automatischen Animation:

Möglicherweise stellen Sie fest, dass für die Hintergrundfarbe und Größe des gesamten Containers weiterhin die Standardeinstellungen von AnimatedContent
verwendet werden.
Gemeinsam genutzte Begrenzungen im Vergleich zu gemeinsam genutzten Elementen
Modifier.sharedBounds()
ähnelt Modifier.sharedElement()
.
Die Modifikatoren unterscheiden sich jedoch in folgenden Punkten:
sharedBounds()
ist für visuell unterschiedliche Inhalte gedacht, die in den einzelnen Status aber denselben Bereich abdecken sollen. BeisharedElement()
hingegen müssen die Inhalte identisch sein.- Bei
sharedBounds()
ist der Inhalt, der den Bildschirm ein- und wieder verlässt, während des Übergangs zwischen den beiden Zuständen sichtbar, während beisharedElement()
nur der Zielinhalt innerhalb der Transformationsgrenzen gerendert wird.Modifier.sharedBounds()
hat die Parameterenter
undexit
, mit denen angegeben werden kann, wie die Inhalte übergehen sollen. Das funktioniert ähnlich wie beiAnimatedContent
. - Der häufigste Anwendungsfall für
sharedBounds()
ist das Container-Transformationsmuster, während fürsharedElement()
ein Hero-Übergang als Beispiel verwendet wird. - Wenn Sie
Text
-Kompositionen verwenden, wirdsharedBounds()
bevorzugt, um Schriftartenänderungen wie den Wechsel zwischen Kursiv- und Fettdruck oder Farbänderungen zu unterstützen.
Wenn wir in den beiden verschiedenen Szenarien Modifier.sharedBounds()
zu Row
und Column
hinzufügen, können wir die Grenzen der beiden Elemente teilen und die Übergangsanimation ausführen, sodass sie sich gegenseitig überlagern:
@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() ) // ... ) { // ... } } }
Bereiche verstehen
Wenn du Modifier.sharedElement()
verwenden möchtest, muss sich das Element in einer SharedTransitionScope
befinden. Die zusammensetzbare Funktion SharedTransitionLayout
stellt das SharedTransitionScope
bereit. Achten Sie darauf, sich an demselben Punkt der obersten Ebene in Ihrer UI-Hierarchie zu platzieren, der die freizugebenden Elemente enthält.
Im Allgemeinen sollten die zusammensetzbaren Funktionen auch in einem AnimatedVisibilityScope
platziert werden. Das ist in der Regel möglich, wenn Sie mit AnimatedContent
zwischen den Komponenten wechseln oder AnimatedVisibility
direkt verwenden oder die Komponente NavHost
verwenden, es sei denn, Sie verwalten die Sichtbarkeit manuell. Wenn Sie mehrere Bereiche verwenden möchten, speichern Sie die erforderlichen Bereiche in einer CompositionLocal, verwenden Sie Kontextempfänger in Kotlin oder übergeben Sie die Bereiche als Parameter an Ihre Funktionen.
Verwenden Sie CompositionLocals
in einem Szenario, in dem Sie mehrere Bereiche im Auge behalten müssen oder wenn Sie eine tief verschachtelte Hierarchie haben. Mit CompositionLocal
können Sie genau die Bereiche auswählen, die gespeichert und verwendet werden sollen. Wenn Sie andererseits Kontextempfänger verwenden, könnten die angegebenen Bereiche versehentlich durch andere Layouts in Ihrer Hierarchie überschrieben werden.
Wenn Sie beispielsweise mehrere verschachtelte AnimatedContent
haben, können die Bereiche überschrieben werden.
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") } // ... } } } }
Wenn Ihre Hierarchie nicht tief verschachtelt ist, können Sie die Bereiche auch als Parameter übergeben:
@Composable fun MainContent( animatedVisibilityScope: AnimatedVisibilityScope, sharedTransitionScope: SharedTransitionScope ) { } @Composable fun Details( animatedVisibilityScope: AnimatedVisibilityScope, sharedTransitionScope: SharedTransitionScope ) { }
Für AnimatedVisibility
freigegebene Elemente
In den vorherigen Beispielen wurde gezeigt, wie Sie freigegebene Elemente mit AnimatedContent
verwenden. Sie funktionieren aber auch mit AnimatedVisibility
.
In diesem Beispiel für ein Lazy Grid-Layout ist beispielsweise jedes Element in AnimatedVisibility
eingehüllt. Wenn auf das Element geklickt wird, entsteht der visuelle Effekt, dass der Inhalt aus der Benutzeroberfläche in eine dialogähnliche Komponente gezogen wird.
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
Modifikatorreihenfolge
Bei Modifier.sharedElement()
und Modifier.sharedBounds()
spielt die Reihenfolge der Modifikatoren eine Rolle, genau wie beim Rest von Compose. Die falsche Platzierung von Modifikatoren, die sich auf die Größe auswirken, kann zu unerwarteten visuellen Sprüngen beim Abgleich gemeinsamer Elemente führen.
Wenn Sie beispielsweise einen Innenrandmodifikator an einer anderen Position für zwei gemeinsam genutzte Elemente platzieren, gibt es einen visuellen Unterschied in der Animation.
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 ) } } } }
Übereinstimmende Grenzen |
Nicht übereinstimmende Grenzen: Beachten Sie, dass die Animation des gemeinsamen Elements etwas abseits angezeigt wird, da sie an die falschen Grenzen angepasst werden muss. |
---|---|
Die vor den Modifikatoren für gemeinsam genutzten Elemente verwendeten Modifizierer stellen Beschränkungen für die Modifikatoren für gemeinsam genutzte Elemente bereit. Diese werden dann verwendet, um die Anfangs- und Zielgrenzen und anschließend die Begrenzungsanimation abzuleiten.
Die Modifikatoren, die nach den Modifikatoren für gemeinsame Elemente verwendet werden, verwenden die zuvor festgelegten Einschränkungen, um die Zielgröße des untergeordneten Elements zu messen und zu berechnen. Mit den Modifikatoren für freigegebene Elemente werden eine Reihe von animierten Einschränkungen erstellt, um das untergeordnete Element schrittweise von der ursprünglichen Größe in die Zielgröße zu transformieren.
Eine Ausnahme hiervon ist, wenn Sie resizeMode = ScaleToBounds()
für die Animation oder Modifier.skipToLookaheadSize()
für eine zusammensetzbare Funktion verwenden. In diesem Fall ordnet Compose das untergeordnete Element anhand der Zieleinschränkungen an und verwendet einen Skalierungsfaktor, um die Animation auszuführen, anstatt die Layoutgröße selbst zu ändern.
Eindeutige Schlüssel
Bei der Arbeit mit komplexen gemeinsam genutzten Elementen empfiehlt es sich, einen Schlüssel zu erstellen, der kein String ist, da Strings fehleranfällig sein können. Jeder Schlüssel muss eindeutig sein, damit Übereinstimmungen auftreten. In Jetsnack gibt es beispielsweise die folgenden gemeinsamen Elemente:

Sie könnten eine Aufzählung erstellen, die den gemeinsamen Elementtyp darstellt. In diesem Beispiel kann die gesamte Snackkarte auch an mehreren Stellen auf dem Startbildschirm angezeigt werden, z. B. in den Bereichen „Beliebt“ und „Empfohlen“. Sie können einen Schlüssel mit dem snackId
, dem origin
(„Beliebt“/„Empfohlen“) und dem type
des gemeinsam genutzten Elements erstellen, das freigegeben wird:
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 ) ) // ... }
Für Schlüssel werden Datenklassen empfohlen, da sie hashCode()
und isEquals()
implementieren.
Sichtbarkeit freigegebener Elemente manuell verwalten
Wenn Sie AnimatedVisibility
oder AnimatedContent
nicht verwenden, können Sie die Sichtbarkeit des freigegebenen Elements selbst verwalten. Verwenden Sie Modifier.sharedElementWithCallerManagedVisibility()
und geben Sie eine eigene Bedingung an, die bestimmt, wann ein Element sichtbar sein soll oder nicht:
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) } }
Aktuelle Einschränkungen
Diese APIs haben einige Einschränkungen. Vor allem:
- Die Interoperabilität zwischen Google Tabellen und Google Docs wird nicht unterstützt. Dazu gehören alle Elemente, die
AndroidView
umschließen, z. B.Dialog
. - In den folgenden Fällen werden automatische Animationen nicht unterstützt:
- Zusammensetzbare Funktionen von freigegebenen Bildern:
ContentScale
ist nicht standardmäßig animiert. Er wird an das festgelegte EndeContentScale
angedockt.
- Formabschneiden: Es gibt keine integrierte Unterstützung für die automatische Animation zwischen Formen, z. B. die Animation von einem Quadrat zu einem Kreis, wenn das Element übergeht.
- Verwenden Sie in den nicht unterstützten Fällen
Modifier.sharedBounds()
anstelle vonsharedElement()
und fügen Sie den ElementenModifier.animateEnterExit()
hinzu.
- Zusammensetzbare Funktionen von freigegebenen Bildern: