Le transizioni degli elementi condivisi sono un modo semplice per passare da un composable all'altro con contenuti coerenti tra loro. Spesso vengono utilizzate per la navigazione, consentendoti di collegare visivamente schermate diverse mentre un utente si sposta tra di esse.
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 richiesto per implementare le transizioni degli elementi condivisi. Fornisce unSharedTransitionScope. I componibili devono trovarsi in unSharedTransitionScopeper utilizzare i modificatori degli elementi condivisi.Modifier.sharedElement(): il modificatore che segnala aSharedTransitionScopeil composable che deve essere abbinato a un altro composable.Modifier.sharedBounds(): il modificatore che segnala aSharedTransitionScopeche i limiti di questo composable devono essere utilizzati come limiti del container per il punto 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 funzionano con overlay e ritaglio. Per scoprire di più su questo argomento importante, consulta la sezione Clipping e overlay.
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 componibili.
Il punto di partenza è un AnimatedContent di base esistente che ha un
MainContent e un composable DetailsContent prima di aggiungere elementi condivisi:
AnimatedContent senza transizioni degli elementi condivisi.Per animare gli elementi condivisi tra i due layout, racchiudi il componibile
AnimatedContentinSharedTransitionLayout. Gli ambiti diSharedTransitionLayouteAnimatedContentvengono passati aMainContenteDetailsContent: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 sui due componibili corrispondenti. Crea un oggettoSharedContentStatee memorizzalo conrememberSharedContentState(). L'oggettoSharedContentStatememorizza la chiave univoca che determina gli elementi condivisi. Fornisci una chiave univoca per identificare i contenuti e utilizzarememberSharedContentState()per l'elemento da ricordare.AnimatedContentScopeviene 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 sull'eventuale corrispondenza di un elemento condiviso, estrai rememberSharedContentState() in una variabile ed esegui una query su isMatchFound.
In questo modo viene generata 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 ed elemento condiviso
Modifier.sharedBounds() è simile a Modifier.sharedElement().
Tuttavia, i modificatori differiscono come segue:
sharedBounds()è per i contenuti visivamente diversi, ma che devono condividere la stessa area tra gli stati, mentresharedElement()prevede che i contenuti siano gli stessi.- Con
sharedBounds(), i contenuti che entrano ed escono dallo schermo sono visibili durante la transizione tra i due stati, mentre consharedElement()viene eseguito il rendering solo dei contenuti di destinazione nei limiti di trasformazione.Modifier.sharedBounds()ha i parametrientereexitper specificare come deve avvenire la transizione dei contenuti, in modo simile a come funzionaAnimatedContent. - Il caso d'uso più comune per
sharedBounds()è il pattern di trasformazione del contenitore, mentre persharedElement()il caso d'uso di esempio è una transizione hero. - Quando utilizzi i componibili
Text, è preferibilesharedBounds()per supportare le modifiche dei caratteri, ad esempio la transizione tra corsivo e grassetto o le modifiche del colore.
Nell'esempio precedente, l'aggiunta di Modifier.sharedBounds() a Row e
Column nei due diversi scenari ci consentirà di condividere i limiti dei
due elementi ed 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 componibile deve trovarsi in un
SharedTransitionScope. Il componibile SharedTransitionLayout fornisce SharedTransitionScope. Assicurati di posizionarlo nello stesso punto di primo livello
nella gerarchia della UI che contiene gli elementi che vuoi condividere.
In genere, i componibili devono essere inseriti anche all'interno di un
AnimatedVisibilityScope. In genere, questo viene fornito utilizzando AnimatedContent
per passare da un composable all'altro o quando utilizzi direttamente AnimatedVisibility, oppure dalla
funzione composable NavHost, a meno che non gestisci la visibilità
manualmente. Per utilizzare più ambiti, salva quelli richiesti in un CompositionLocal, utilizza i ricevitori di contesto in Kotlin o passa gli ambiti come parametri alle tue funzioni.
Utilizza CompositionLocals nello scenario in cui hai più ambiti da monitorare o una gerarchia con nidificazione profonda. 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 eseguire l'override involontario degli scope forniti.
Ad esempio, se hai più AnimatedContent nidificati, gli ambiti potrebbero essere
sostituiti.
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 modo profondo, 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 lazy, ogni elemento è inserito in
AnimatedVisibility. Quando l'utente fa clic sull'elemento, i contenuti hanno l'effetto visivo di essere estratti dalla UI 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 inserisci 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 appare un po' fuori posto, poiché 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 ricavare 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 dell'elemento secondario. I modificatori degli elementi condivisi 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() su un composable. In questo caso, Compose dispone l'elemento secondario utilizzando i vincoli di destinazione e utilizza un fattore di scalabilità per eseguire l'animazione anziché modificare le dimensioni del layout stesso.
Chiavi uniche
Quando lavori con elementi condivisi complessi, è buona norma creare una chiave che non sia una stringa, perché le stringhe possono essere soggette a errori di corrispondenza. Ogni chiave deve essere univoca affinché si verifichino corrispondenze. Ad esempio, in Jetsnack abbiamo i seguenti elementi condivisi:
Puoi creare un enum per rappresentare il tipo di elemento condiviso. In questo esempio, l'intera scheda dello snack può essere visualizzata anche in più posizioni diverse della schermata Home, ad esempio in una sezione "Popolari" e in una sezione "Consigliati". Puoi creare una chiave che abbia 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 equals().
Gestisci 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 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 composable che aggregano
AndroidView, comeDialogoModalBottomSheet. - Non è previsto il supporto dell'animazione automatica per i seguenti elementi:
- Componibili di immagini condivise:
ContentScalenon è animato per impostazione predefinita. Si aggancia alla fine impostataContentScale.
- Ritaglio delle forme: non esiste un 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 elementi.
- Componibili di immagini condivise: