Le transizioni agli elementi condivisi sono un modo semplice per passare tra componenti componibili che hanno contenuti coerenti tra loro. Vengono spesso utilizzati per la navigazione, in modo da consentire di collegare visivamente diverse schermate mentre l'utente passa da una all'altra.
Ad esempio, nel video seguente puoi vedere che l'immagine e il titolo dello snack vengono condivisi dalla pagina della scheda alla pagina dei dettagli.
In Compose sono disponibili alcune API di alto livello che ti aiutano a creare elementi condivisi:
SharedTransitionLayout
: il layout più esterno necessario per implementare le transizioni degli elementi condivisi. Fornisce unSharedTransitionScope
. I componibili devono essere in unSharedTransitionScope
per utilizzare i modificatori degli elementi condivisi.Modifier.sharedElement()
: il modificatore che segnala alSharedTransitionScope
il composable da abbinare a un altro composable.Modifier.sharedBounds()
: il modificatore che indica aSharedTransitionScope
che i limiti di questo composable 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 durante la creazione di elementi condivisi in Compose è il modo in cui questi elementi interagiscono con overlay e clip. Per saperne di più su questo importante argomento, consulta la sezione relativa a taglio e overlay.
Utilizzo di base
In questa sezione verrà creata la seguente transizione, dall'elemento "elenco" più piccolo all'elemento dettagliato più grande:

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

AnimatedContent
senza transizioni di elementi condivisi.Per rendere gli elementi condivisi animati tra i due layout, circonda il componibile
AnimatedContent
conSharedTransitionLayout
. Gli scopi diSharedTransitionLayout
eAnimatedContent
vengono trasmessi 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 tua catena di modificatori componibili sui due componibili corrispondenti. Crea un oggettoSharedContentState
e ricordalo conrememberSharedContentState()
. L'oggettoSharedContentState
sta memorizzando la chiave univoca che determina gli elementi condivisi. Fornisci una chiave univoca per identificare i contenuti e usarememberSharedContentState()
per l'elemento da ricordare. L'elementoAnimatedContentScope
viene passato nel 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 sapere se si è verificata una corrispondenza di elementi condivisi, estrai rememberSharedContentState()
in una variabile ed esegui una query su isMatchFound
.
Il che genera la seguente animazione automatica:

Puoi notare che il colore e la dimensione dello sfondo dell'intero contenitore continuano a utilizzare le impostazioni predefinite di AnimatedContent
.
Limiti condivisi ed elemento condiviso
Modifier.sharedBounds()
è simile a Modifier.sharedElement()
.
Tuttavia, i modificatori sono diversi nei seguenti modi:
sharedBounds()
si rivolge a contenuti che sono visivamente diversi, ma che devono condividere la stessa area tra stati, mentresharedElement()
si aspetta che i contenuti siano gli stessi.- Con
sharedBounds()
, i contenuti che entrano e escono dallo schermo sono visibili durante la transizione tra i due stati, mentre consharedElement()
solo i contenuti target vengono visualizzati negli intervalli di trasformazione.Modifier.sharedBounds()
ha i parametrienter
eexit
per specificare la transizione dei contenuti, in modo simile a come funzionaAnimatedContent
. - Il caso d'uso più comune per
sharedBounds()
è il pattern di trasformazione del container, mentre persharedElement()
il caso d'uso di esempio è una transizione hero. - Quando utilizzi i composabili
Text
, è preferibilesharedBounds()
per supportare le modifiche dei caratteri, ad esempio il passaggio tra corsivo e grassetto o le modifiche di colore.
Nell'esempio precedente, l'aggiunta di Modifier.sharedBounds()
a Row
e
Column
nei due diversi scenari ci consentirà di condividere i limiti di entrambi e di eseguire l'animazione di transizione, consentendo loro di crescere tra loro:
@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
SharedTransitionScope
. Il composable SharedTransitionLayout
fornisce il
SharedTransitionScope
. Assicurati di collocarlo nello stesso punto di primo livello nella gerarchia UI contenente gli elementi che vuoi condividere.
In genere, i componibili devono essere posizionati anche all'interno di un elemento
AnimatedVisibilityScope
. In genere, questa opzione viene fornita utilizzando AnimatedContent
per passare da un elemento componibile all'altro o quando si utilizza direttamente AnimatedVisibility
oppure tramite
la funzione componibile NavHost
, a meno che tu non gestisca la visibilità
manualmente. Per
utilizzare più ambiti, salva gli ambiti richiesti in
ComposeLocal, utilizza i ricevitori di contesto in Kotlin o passa
gli ambiti come parametri alle tue funzioni.
Utilizza CompositionLocals
se hai più ambiti da monitorare o una gerarchia nidificata in modo complesso. Un'istruzione 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 sostituire accidentalmente gli ambiti forniti.
Ad esempio, se disponi di più elementi AnimatedContent
nidificati, potrebbe essere eseguito l'override degli ambiti.
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 è profondamente nidificata, puoi trasferire 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 lazy ogni elemento è racchiuso in
AnimatedVisibility
. Quando viene fatto 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( state = 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 modi è 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, viene rilevata una differenza visiva nell'animazione.
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 abbinati: nota che l'animazione dell'elemento condiviso appare leggermente fuori luogo perché deve essere ridimensionata rispetto ai limiti errati. |
---|---|
I modificatori utilizzati prima dei modificatori degli elementi condivisi forniscono vincoli ai modificatori degli elementi condivisi, che vengono poi utilizzati per dedurre i limiti iniziali e di destinazione e successivamente l'animazione dei limiti.
I modificatori utilizzati dopo i modificatori degli elementi condivisi utilizzano le limitazioni precedenti per misurare e calcolare le dimensioni target dell'elemento secondario. I modificatori di elementi condivisi creano una serie di vincoli animati per trasformare gradualmente il file secondario dalla dimensione iniziale alla dimensione di destinazione.
Fanno eccezione i casi in cui utilizzi resizeMode = ScaleToBounds()
per
l'animazione o Modifier.skipToLookaheadSize()
in un componibile. In questo
caso, Compose dispone il componente secondario utilizzando i vincoli di destinazione e, anziché modificare le dimensioni del layout,
utilizza un fattore di scala per eseguire l'animazione.
Chiavi univoche
Quando si lavora con elementi condivisi complessi, è buona norma creare una chiave che non sia una stringa, perché le stringhe possono essere soggette a errori. Ogni chiave deve essere univoca affinché si verifichino le corrispondenze. Ad esempio, in Jetsnack abbiamo i seguenti elementi condivisi:

Puoi creare un enum per rappresentare il tipo di elemento condiviso. In questo esempio, tutta la scheda snack può essere visualizzata anche in più punti della schermata iniziale, ad esempio nelle sezioni "Popolari" e "Consigliati". Puoi creare una
chiave con snackId
, origin
("Popolari"/"Consigliati") 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 poiché implementano hashCode()
e
isEquals()
.
Gestire manualmente la visibilità degli elementi condivisi
Se non utilizzi AnimatedVisibility
o AnimatedContent
,
puoi gestire autonomamente la visibilità degli elementi condivisi. Utilizza
Modifier.sharedElementWithCallerManagedVisibility()
e fornisci il tuo
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 Visualizza e Componi. Sono inclusi
qualsiasi composable che aggrega
AndroidView
, ad esempio unDialog
. - L'animazione automatica non è supportata per i seguenti elementi:
- Combinazioni di immagini condivise:
ContentScale
non è animato per impostazione predefinita. Si aggancia alla fine impostataContentScale
.
- Taglio delle forme: non è disponibile il supporto integrato per l'animazione automatica tra le forme, ad esempio l'animazione da un quadrato a un cerchio durante la transizione degli elementi.
- Per le richieste non supportate, utilizza
Modifier.sharedBounds()
anzichésharedElement()
e aggiungiModifier.animateEnterExit()
agli elementi.
- Combinazioni di immagini condivise: