Personalizza la transizione degli elementi condivisi

Per personalizzare l'esecuzione dell'animazione di transizione degli elementi condivisi, puoi utilizzare alcuni parametri per modificare la transizione degli elementi condivisi.

Specifica dell'animazione

Per modificare la specifica dell'animazione utilizzata per il movimento di dimensioni e posizione, puoi specificare un parametro boundsTransform diverso in Modifier.sharedElement(). Questo fornisce la posizione Rect iniziale e la posizione Rect di destinazione.

Ad esempio, per fare in modo che il testo nell'esempio precedente si muova con un arco movimento, specifica il boundsTransform parametro per utilizzare una keyframes specifica:

val textBoundsTransform = BoundsTransform { initialBounds, targetBounds ->
    keyframes {
        durationMillis = boundsAnimationDurationMillis
        initialBounds at 0 using ArcMode.ArcBelow using FastOutSlowInEasing
        targetBounds at boundsAnimationDurationMillis
    }
}
Text(
    "Cupcake", fontSize = 28.sp,
    modifier = Modifier.sharedBounds(
        rememberSharedContentState(key = "title"),
        animatedVisibilityScope = animatedVisibilityScope,
        boundsTransform = textBoundsTransform
    )
)

Puoi utilizzare qualsiasi AnimationSpec. Questo esempio utilizza una specifica keyframes.

Figura 1. Esempio che mostra diversi parametri boundsTransform

Modalità di ridimensionamento

Quando esegui l'animazione tra due limiti condivisi, puoi impostare il parametro resizeMode su RemeasureToBounds o ScaleToBounds. Questo parametro determina la transizione dell'elemento condiviso tra i due stati. ScaleToBounds misura prima il layout secondario con i vincoli di lookahead (o di destinazione). Poi, il layout stabile dell'elemento secondario viene scalato per adattarsi ai limiti condivisi. Puoi considerare ScaleToBounds come una "scala grafica" tra gli stati.

Al contrario, RemeasureToBounds misura e riorganizza il layout secondario di sharedBounds con vincoli fissi animati in base alle dimensioni di destinazione. La nuova misurazione viene attivata dalla modifica delle dimensioni dei limiti, che potrebbe avvenire potenzialmente ogni frame.

Per i composable Text, ti consigliamo di utilizzare ScaleToBounds, in quanto evita il riorganizzazione e il riflusso del testo su righe diverse. Ti consigliamo di utilizzare RemeasureToBounds per i limiti con proporzioni diverse e se vuoi una continuità fluida tra i due elementi condivisi.

La differenza tra le due modalità di ridimensionamento è visibile negli esempi che seguono:

ScaleToBounds

RemeasureToBounds

Attivare e disattivare dinamicamente gli elementi condivisi

Per impostazione predefinita, sharedElement() e sharedBounds() sono configurati per animare le modifiche del layout ogni volta che viene trovata una chiave corrispondente nello stato di destinazione. Tuttavia, potresti voler disattivare questa animazione in modo dinamico in base a condizioni specifiche, ad esempio la direzione di navigazione o lo stato attuale dell'interfaccia utente.

Per controllare se la transizione dell'elemento condiviso si verifica, puoi personalizzare SharedContentConfig passato a rememberSharedContentState(). La proprietà isEnabled determina se l'elemento condiviso è attivo.

L'esempio seguente mostra come definire una configurazione che attiva la transizione condivisa solo quando si naviga tra schermate specifiche (ad es. solo da A a B), disattivandola per le altre.

SharedTransitionLayout {
    val transition = updateTransition(currentState)
    transition.AnimatedContent { targetState ->
        // Create the configuration that depends on state changing.
        fun animationConfig() : SharedTransitionScope.SharedContentConfig {
            return object : SharedTransitionScope.SharedContentConfig {
                override val SharedTransitionScope.SharedContentState.isEnabled: Boolean
                    // For this example, we only enable the transition in one direction
                    // from A -> B and not the other way around.
                    get() =
                        transition.currentState == "A" && transition.targetState == "B"
            }
        }
        when (targetState) {
            "A" -> Box(
                modifier = Modifier
                    .sharedElement(
                        rememberSharedContentState(
                            key = "shared_box",
                            config = animationConfig()
                        ),
                        animatedVisibilityScope = this
                    )
                    // ...
            ) {
                // Your content
            }
            "B" -> {
                Box(
                    modifier = Modifier
                        .sharedElement(
                            rememberSharedContentState(
                                key = "shared_box",
                                config = animationConfig()
                            ),
                            animatedVisibilityScope = this
                        )
                        // ...
                ) {
                    // Your content
                }
            }
        }
    }
}

Per impostazione predefinita, se un elemento condiviso viene disattivato durante un'animazione in corso, l'animazione viene comunque completata per evitare di rimuovere accidentalmente le animazioni in corso. Se devi rimuovere l'elemento mentre l'animazione è in corso, puoi sostituire shouldKeepEnabledForOngoingAnimation nell'interfaccia SharedContentConfig per restituire false.

Vai al layout finale

Per impostazione predefinita, quando si esegue la transizione tra due layout, le dimensioni del layout vengono animate tra lo stato iniziale e quello finale. Questo comportamento potrebbe non essere desiderabile quando si animano contenuti come il testo.

L'esempio seguente illustra il testo della descrizione "Lorem Ipsum" che entra nella schermata in due modi diversi. Nel primo esempio, il testo viene rifluito quando entra man mano che il contenitore aumenta di dimensioni. Nel secondo esempio, il testo non viene rifluito quando aumenta. L'aggiunta di Modifier.skipToLookaheadSize() impedisce il riflusso durante la crescita.

Nessun Modifier.skipToLookaheadSize() - nota il riflusso del testo "Lorem Ipsum"

Modifier.skipToLookaheadSize() - nota che il testo "Lorem Ipsum" mantiene lo stato finale all'inizio dell'animazione

Clip e overlay

Affinché gli elementi condivisi possano essere condivisi tra composable diversi, il rendering del composable viene elevato in un overlay di livello quando la transizione viene avviata alla sua corrispondenza nella destinazione. L'effetto è che sfuggirà ai limiti del padre e alle trasformazioni di livello (ad esempio, alfa e scala).

Verrà eseguito il rendering sopra gli altri elementi dell'interfaccia utente non condivisi. Al termine della transizione, l'elemento verrà eliminato dall'overlay al proprio DrawScope.

Per ritagliare un elemento condiviso in una forma, utilizza la funzione standard Modifier.clip(). Posizionalo dopo sharedElement():

Image(
    painter = painterResource(id = R.drawable.cupcake),
    contentDescription = "Cupcake",
    modifier = Modifier
        .size(100.dp)
        .sharedElement(
            rememberSharedContentState(key = "image"),
            animatedVisibilityScope = this@AnimatedContent
        )
        .clip(RoundedCornerShape(16.dp)),
    contentScale = ContentScale.Crop
)

Se devi assicurarti che un elemento condiviso non venga mai sottoposto a rendering all'esterno di un contenitore principale, puoi impostare clipInOverlayDuringTransition su sharedElement(). Per impostazione predefinita, per i limiti condivisi nidificati, clipInOverlayDuringTransition utilizza il percorso di ritaglio del sharedBounds() principale.

Per supportare il mantenimento di elementi dell'interfaccia utente specifici, ad esempio una barra inferiore o un pulsante di azione mobile, sempre in primo piano durante una transizione di elementi condivisi, utilizza Modifier.renderInSharedTransitionScopeOverlay(). Per impostazione predefinita, questo modificatore mantiene i contenuti nell'overlay durante il periodo in cui la transizione condivisa è attiva.

Ad esempio, in Jetsnack, BottomAppBar deve essere posizionato sopra l'elemento condiviso finché la schermata non è visibile. L'aggiunta del modificatore al composable lo mantiene elevato.

Senza Modifier.renderInSharedTransitionScopeOverlay()

Con Modifier.renderInSharedTransitionScopeOverlay()

Potresti voler animare anche il composable non condiviso e mantenerlo sopra gli altri composable prima della transizione. In questi casi, utilizza renderInSharedTransitionScopeOverlay().animateEnterExit() per animare il composable durante l'esecuzione della transizione dell'elemento condiviso:

JetsnackBottomBar(
    modifier = Modifier
        .renderInSharedTransitionScopeOverlay(
            zIndexInOverlay = 1f,
        )
        .animateEnterExit(
            enter = fadeIn() + slideInVertically {
                it
            },
            exit = fadeOut() + slideOutVertically {
                it
            }
        )
)

Figura 2. Barra dell'app in basso che scorre in entrata e in uscita durante la transizione dell'animazione.

Nel raro caso in cui non vuoi che l'elemento condiviso venga sottoposto a rendering in un overlay, puoi impostare renderInOverlayDuringTransition su sharedElement() su false.

Notificare ai layout fratelli le modifiche alle dimensioni dell'elemento condiviso

Per impostazione predefinita, sharedBounds() e sharedElement() non notificano al contenitore principale le modifiche delle dimensioni durante la transizione del layout.

Per propagare le modifiche delle dimensioni al contenitore principale durante la transizione, modifica il parametro placeholderSize in PlaceholderSize.AnimatedSize. In questo modo, l'elemento aumenta o diminuisce. Tutti gli altri elementi del layout rispondono alla modifica.

PlaceholderSize.ContentSize (valore predefinito)

PlaceholderSize.AnimatedSize

(Nota come gli altri elementi dell'elenco si spostano verso il basso in risposta all'aumento di un elemento)