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.

Specifiche 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(). Vengono fornite la posizione iniziale di Rect e la posizione di destinazione di Rect.

Ad esempio, per far muovere il testo nell'esempio precedente con un movimento ad arco, specifica il parametro boundsTransform per utilizzare una specifica keyframes:

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 boundsTransformparametri

Modalità di ridimensionamento

Quando esegui l'animazione tra due limiti condivisi, puoi impostare il parametro resizeMode su RemeasureToBounds o ScaleToBounds. Questo parametro determina il modo in cui gli elementi condivisi eseguono la transizione tra i due stati. ScaleToBounds first misura il layout secondario con i vincoli di lookahead (o di destinazione). Quindi, il layout stabile del bambino viene scalato per adattarsi ai limiti condivisi. ScaleToBounds può essere considerata 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 target. La nuova misurazione viene attivata dalla modifica delle dimensioni dei limiti, che potrebbe avvenire a ogni frame.

Per i composable Text, è consigliabile utilizzare ScaleToBounds, in quanto evita il riposizionamento e il reflow del testo su righe diverse. RemeasureToBounds è consigliato 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 al 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, come la direzione di navigazione o lo stato attuale dell'interfaccia utente.

Per controllare se si verifica la transizione degli elementi condivisi, 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 passa da 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, questa viene comunque completata per evitare di rimuovere accidentalmente le animazioni in corso. Se devi rimuovere l'elemento mentre l'animazione è in corso, puoi eseguire l'override di shouldKeepEnabledForOngoingAnimation nell'interfaccia SharedContentConfig per restituire false.

Passa al layout finale

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

L'esempio seguente illustra l'inserimento del testo della descrizione "Lorem Ipsum" nello schermo in due modi diversi. Nel primo esempio, il testo viene riformattato man mano che entra nel contenitore, che aumenta di dimensioni. Nel secondo esempio, il testo non si ridispone man mano che aumenta. L'aggiunta di Modifier.skipToLookaheadSize() impedisce il reflow man mano che cresce.

No Modifier.skipToLookaheadSize() - nota il reflow del testo "Lorem Ipsum"

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

Clip e overlay

Affinché gli elementi condivisi vengano condivisi tra diversi composable, il rendering del composable viene elevato in un overlay di livello quando la transizione viene avviata alla sua corrispondenza nella destinazione. In questo modo, l'elemento uscirà dai limiti del contenitore principale e dalle relative trasformazioni del livello (ad esempio, alpha e scala).

Verrà visualizzato sopra gli altri elementi dell'interfaccia utente non condivisi. Una volta completata la transizione, l'elemento verrà spostato dalla sovrapposizione 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 visualizzato al di fuori di un contenitore principale, puoi impostare clipInOverlayDuringTransition su sharedElement(). Per impostazione predefinita, per i limiti condivisi nidificati, clipInOverlayDuringTransition utilizza il percorso di ritaglio di sharedBounds().

Per supportare il mantenimento sempre in primo piano di elementi UI specifici, come una barra inferiore o un pulsante di azione fluttuante, 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é lo schermo non è visibile. L'aggiunta del modificatore al componente componibile lo mantiene elevato.

Senza Modifier.renderInSharedTransitionScopeOverlay()

Con Modifier.renderInSharedTransitionScopeOverlay()

Potresti voler animare la scomparsa del composable non condiviso e farlo rimanere sopra gli altri composable prima della transizione. In questi casi, utilizza renderInSharedTransitionScopeOverlay().animateEnterExit() per animare il composable in uscita durante l'esecuzione della transizione degli elementi condivisi:

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

Figura 2. La barra dell'app in basso scorre dentro e fuori durante la transizione dell'animazione.

Nel raro caso in cui non vuoi che l'elemento condiviso venga visualizzato in una sovrapposizione, puoi impostare renderInOverlayDuringTransition su sharedElement() su false.

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

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

Per propagare le modifiche alle dimensioni al contenitore principale durante la transizione, modifica il parametro placeholderSize in PlaceholderSize.AnimatedSize. In questo modo, l'elemento si ingrandisce o si rimpicciolisce. 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'elemento che si ingrandisce)