Le transizioni tra elementi condivisi sono un modo semplice per passare da un composable all'altro con contenuti coerenti. Vengono spesso utilizzate per la navigazione, consentendoti di collegare visivamente diverse schermate mentre un utente naviga tra di esse.
Ad esempio, nel seguente video puoi vedere che l'immagine e il titolo dello snack vengono condivisi dalla pagina della scheda alla pagina dei dettagli.
In Compose, esistono alcune API di alto livello che ti aiutano a creare elementi condivisi:
SharedTransitionLayout
: il layout più esterno richiesto per implementare le transizioni degli elementi condivisi. Fornisce unSharedTransitionScope
. I composable devono trovarsi in unSharedTransitionScope
per utilizzare i modificatori degli elementi condivisi.Modifier.sharedElement()
: il modificatore che segnala aSharedTransitionScope
il composable che deve essere abbinato a un altro composable.Modifier.sharedBounds()
: il modificatore che segnala aSharedTransitionScope
che i limiti di questo elemento componibile devono essere utilizzati come limiti del contenitore in cui deve avvenire la transizione. A differenza disharedElement()
,sharedBounds()
è progettato per contenuti visivamente diversi.
Un concetto importante quando si creano elementi condivisi in Compose è il modo in cui funzionano con le sovrapposizioni e il ritaglio. Per saperne di più su questo importante argomento, consulta la sezione Ritaglio e sovrapposizioni.
Utilizzo di base
In questa sezione verrà creata la seguente transizione, che passa dall'elemento "elenco" più piccolo all'elemento dettagliato più grande:

Il modo migliore per utilizzare Modifier.sharedElement()
è in combinazione con
AnimatedContent
, AnimatedVisibility
o NavHost
, in quanto gestisce
automaticamente la transizione tra i composable.
Il punto di partenza è un AnimatedContent
di base esistente con un
MainContent
e DetailsContent
componibili prima di aggiungere elementi condivisi:

AnimatedContent
senza transizioni di elementi condivisi.Per animare gli elementi condivisi tra i due layout, racchiudi il composable
AnimatedContent
conSharedTransitionLayout
. Gli ambiti diSharedTransitionLayout
eAnimatedContent
vengono passati aMainContent
eDetailsContent
: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 ) } } }
Aggiungi
Modifier.sharedElement()
alla catena di modificatori componibili nei due componibili corrispondenti. Crea un oggettoSharedContentState
e memorizzalo conrememberSharedContentState()
. L'oggettoSharedContentState
memorizza la chiave univoca che determina gli elementi condivisi. Fornisci una chiave univoca per identificare i contenuti e utilizzarememberSharedContentState()
per memorizzare l'elemento.AnimatedContentScope
viene passato al modificatore, che viene utilizzato per coordinare l'animazione.@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 ) // ... } } }
Per ottenere informazioni su una corrispondenza di elementi condivisi, estrai
rememberSharedContentState()
in una variabile ed esegui una query su isMatchFound
.
Ciò comporta la seguente animazione automatica:

Potresti notare che il colore di sfondo e le dimensioni dell'intero contenitore
utilizzano ancora le impostazioni predefinite di AnimatedContent
.
Limiti condivisi e elemento condiviso
Modifier.sharedBounds()
è simile a Modifier.sharedElement()
.
Tuttavia, i modificatori sono diversi nei seguenti modi:
sharedBounds()
è per i contenuti visivamente diversi, ma che devono condividere la stessa area tra gli stati, mentresharedElement()
prevede che i contenuti siano identici.- Con
sharedBounds()
, i contenuti che entrano ed escono dallo schermo sono visibili durante la transizione tra i due stati, mentre consharedElement()
vengono visualizzati solo i contenuti di destinazione nei limiti di trasformazione.Modifier.sharedBounds()
ha i parametrienter
eexit
per specificare la transizione dei contenuti, in modo simile al funzionamento diAnimatedContent
. - Il caso d'uso più comune per
sharedBounds()
è il pattern di trasformazione dei container, mentre persharedElement()
l'esempio di caso d'uso è una transizione hero. - Quando utilizzi i composable
Text
,sharedBounds()
è preferibile per supportare le modifiche ai caratteri, ad esempio la transizione tra corsivo e grassetto o le modifiche al colore.
Dall'esempio precedente, l'aggiunta di Modifier.sharedBounds()
a Row
e
Column
nei due scenari diversi ci consentirà di condividere i limiti dei
due elementi ed eseguire l'animazione di transizione, consentendo loro di crescere
l'uno rispetto all'altro:
@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() ) // ... ) { // ... } } }
Informazioni sugli ambiti
Per utilizzare Modifier.sharedElement()
, il composable deve trovarsi in un elemento
SharedTransitionScope
. Il composable SharedTransitionLayout
fornisce
SharedTransitionScope
. Assicurati di posizionarlo nello stesso punto di primo livello della gerarchia
dell'interfaccia utente che contiene gli elementi che vuoi condividere.
In genere, i composable devono essere inseriti anche all'interno di un
AnimatedVisibilityScope
. In genere, questo viene fornito utilizzando AnimatedContent
per passare da un elemento componibile all'altro o quando si utilizza AnimatedVisibility
direttamente oppure tramite
la funzione componibile NavHost
, a meno che tu non gestisca la visibilità
manualmente. Per utilizzare più ambiti, salva gli ambiti richiesti in un CompositionLocal
, utilizza i context receiver in Kotlin o passa gli ambiti come parametri alle funzioni.
Utilizza CompositionLocals
nello scenario in cui devi tenere traccia di più ambiti o di una gerarchia nidificata in profondità. Un CompositionLocal
ti consente di scegliere gli ambiti esatti da salvare e utilizzare. D'altra parte, quando utilizzi i ricevitori di contesto,
altri layout nella gerarchia potrebbero sovrascrivere accidentalmente gli ambiti forniti.
Ad esempio, se hai più AnimatedContent
nidificati, gli ambiti potrebbero essere
ignorati.
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") } // ... } } } }
In alternativa, se la gerarchia non è nidificata in profondità, puoi passare gli ambiti come parametri:
@Composable fun MainContent( animatedVisibilityScope: AnimatedVisibilityScope, sharedTransitionScope: SharedTransitionScope ) { } @Composable fun Details( animatedVisibilityScope: AnimatedVisibilityScope, sharedTransitionScope: SharedTransitionScope ) { }
Elementi condivisi con AnimatedVisibility
Gli esempi precedenti mostravano come utilizzare gli elementi condivisi con AnimatedContent
, ma
gli elementi condivisi funzionano anche con AnimatedVisibility
.
Ad esempio, in questo esempio di griglia pigra, ogni elemento è racchiuso in
AnimatedVisibility
. Quando si fa clic sull'elemento, i contenuti hanno l'effetto visivo di essere estratti dall'interfaccia utente in un componente simile a una finestra di dialogo.
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
.Ordinamento dei modificatori
Con Modifier.sharedElement()
e Modifier.sharedBounds()
, l'ordine della catena di
modificatori è importante,
come per il resto di Compose. Il posizionamento errato dei modificatori che influiscono sulle dimensioni
può causare salti visivi imprevisti durante la corrispondenza degli elementi condivisi.
Ad esempio, se posizioni un modificatore di spaziatura interna in una posizione diversa su due elementi condivisi, l'animazione presenta una differenza visiva.
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 ) } } } }
Limiti corrispondenti |
Limiti non corrispondenti: nota come l'animazione dell'elemento condiviso appaia un po' fuori posto, in quanto deve essere ridimensionata in base ai limiti errati |
---|---|
I modificatori utilizzati prima dei modificatori degli elementi condivisi forniscono vincoli ai modificatori degli elementi condivisi, che vengono poi utilizzati per derivare i limiti iniziali e di destinazione e, successivamente, l'animazione dei limiti.
I modificatori utilizzati dopo i modificatori degli elementi condivisi utilizzano i vincoli precedenti per misurare e calcolare le dimensioni target del figlio. I modificatori dell'elemento condiviso creano una serie di vincoli animati per trasformare gradualmente l'elemento secondario dalle dimensioni iniziali a quelle di destinazione.
L'eccezione a questa regola è se utilizzi resizeMode = ScaleToBounds()
per
l'animazione o Modifier.skipToLookaheadSize()
in un elemento componibile. In questo
caso, Compose dispone il figlio utilizzando i vincoli di destinazione e utilizza
un fattore di scala per eseguire l'animazione anziché modificare le dimensioni del layout
stesso.
Chiavi univoche
Quando lavori con elementi condivisi complessi, è consigliabile creare una chiave che non sia una stringa, perché le stringhe possono essere soggette a errori di corrispondenza. Ogni chiave deve essere univoca per poter trovare corrispondenze. Ad esempio, in Jetsnack abbiamo i seguenti elementi condivisi:

Potresti creare un'enumerazione per rappresentare il tipo di elemento condiviso. In questo esempio,
l'intera scheda dello snack può essere visualizzata anche in diverse posizioni della schermata Home, ad esempio nelle sezioni "Popolari" e "Consigliati". Puoi creare una
chiave che contenga snackId
, origin
("Popolare" / "Consigliato") e
type
dell'elemento condiviso che verrà condiviso:
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 ) ) // ... }
Le classi di dati sono consigliate per le chiavi perché implementano hashCode()
e
isEquals()
.
Gestire manualmente la visibilità degli elementi condivisi
Nei casi in cui non utilizzi AnimatedVisibility
o AnimatedContent
,
puoi gestire autonomamente la visibilità degli elementi condivisi. Utilizza
Modifier.sharedElementWithCallerManagedVisibility()
e fornisci la tua
condizione che determina quando un elemento deve essere visibile o meno:
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) } }
Limitazioni attuali
Queste API presentano alcune limitazioni. In particolare:
- Non è supportata l'interoperabilità tra Views e Compose. Sono inclusi
tutti i componenti componibili che racchiudono
AndroidView
, ad esempioDialog
oModalBottomSheet
. - Non è previsto il supporto automatico delle animazioni per quanto segue:
- Componenti combinabili Shared Image:
ContentScale
non è animato per impostazione predefinita. Si aggancia alla fine impostataContentScale
.
- Ritaglio della forma: non è previsto il supporto integrato per l'animazione automatica tra le forme, ad esempio l'animazione da un quadrato a un cerchio durante la transizione dell'elemento.
- Per i casi non supportati, utilizza
Modifier.sharedBounds()
anzichésharedElement()
e aggiungiModifier.animateEnterExit()
agli articoli.
- Componenti combinabili Shared Image: