Personnaliser la transition d'élément partagé

Pour personnaliser l'exécution de l'animation de transition d'élément partagé, vous pouvez utiliser quelques paramètres pour modifier la transition des éléments partagés.

Spécifications de l'animation

Pour modifier la spécification d'animation utilisée pour le mouvement de taille et de position, vous pouvez spécifier un autre paramètre boundsTransform sur Modifier.sharedElement(). Cela fournit la position Rect initiale et la position Rect cible.

Par exemple, pour que le texte de l'exemple précédent se déplace selon un mouvement d'arc, spécifiez le paramètre boundsTransform pour utiliser une spécification 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
    )
)

Vous pouvez utiliser n'importe quel AnimationSpec. Cet exemple utilise une spécification keyframes.

Figure 1. Exemple montrant différents paramètres boundsTransform

Mode de redimensionnement

Lorsque vous animez entre deux limites partagées, vous pouvez définir le paramètre resizeMode sur RemeasureToBounds ou ScaleToBounds. Ce paramètre détermine la façon dont l'élément partagé passe d'un état à l'autre. ScaleToBounds mesure d'abord la mise en page enfant avec les contraintes d'anticipation (ou cibles). Ensuite, la mise en page stable de l'enfant est mise à l'échelle pour s'adapter aux limites partagées. ScaleToBounds peut être considéré comme une "échelle graphique" entre les états.

En revanche, RemeasureToBounds remesure et réorganise la mise en page enfant de sharedBounds avec des contraintes fixes animées basées sur la taille cible. La nouvelle mesure est déclenchée par le changement de taille des limites, qui peut potentiellement se produire à chaque frame.

Pour les composables Text, nous vous recommandons d'utiliser ScaleToBounds, car cela évite de réorganiser et de redistribuer le texte sur différentes lignes. RemeasureToBounds est recommandé pour les limites qui ont des formats différents et si vous souhaitez une continuité fluide entre les deux éléments partagés.

La différence entre les deux modes de redimensionnement est illustrée dans les exemples suivants :

ScaleToBounds

RemeasureToBounds

Activer et désactiver des éléments partagés de manière dynamique

Par défaut, sharedElement() et sharedBounds() sont configurés pour animer les modifications de mise en page chaque fois qu'une clé correspondante est trouvée dans l'état cible. Toutefois, vous pouvez désactiver cette animation de manière dynamique en fonction de conditions spécifiques, telles que la direction de la navigation ou l'état actuel de l'interface utilisateur.

Pour contrôler si la transition d'élément partagé se produit, vous pouvez personnaliser le SharedContentConfig transmis à rememberSharedContentState(). La propriété isEnabled détermine si l'élément partagé est actif.

L'exemple suivant montre comment définir une configuration qui n'active la transition partagée que lors de la navigation entre des écrans spécifiques (par exemple, uniquement de A à B), tout en la désactivant pour les autres.

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
                }
            }
        }
    }
}

Par défaut, si un élément partagé est désactivé pendant une animation en cours, il termine l'animation en cours pour éviter de supprimer accidentellement des animations en cours. Si vous devez supprimer l'élément pendant l'animation, vous pouvez remplacer shouldKeepEnabledForOngoingAnimation dans l'interface SharedContentConfig pour renvoyer "false".

Passer à la mise en page finale

Par défaut, lors de la transition entre deux mises en page, la taille de la mise en page s'anime entre son état initial et son état final. Ce comportement peut être indésirable lors de l'animation de contenus tels que du texte.

L'exemple suivant illustre le texte de description "Lorem Ipsum" qui s'affiche à l'écran de deux manières différentes. Dans le premier exemple, le texte est redistribué à mesure qu'il entre dans le conteneur, dont la taille augmente. Dans le deuxième exemple, le texte ne se réorganise pas à mesure qu'il s'agrandit. L'ajout de Modifier.skipToLookaheadSize() empêche le réajustement à mesure qu'il s'agrandit.

Non Modifier.skipToLookaheadSize() : notez le réajustement du texte "Lorem Ipsum"

Modifier.skipToLookaheadSize() : notez que le texte "Lorem Ipsum" conserve son état final au début de l'animation.

Extrait et superpositions

Pour que les éléments partagés puissent être partagés entre différents composables, le rendu du composable est élevé dans une superposition de calques lorsque la transition est lancée vers sa correspondance dans la destination. Cela aura pour effet d'échapper aux limites du parent et à ses transformations de calque (par exemple, l'alpha et l'échelle).

Il s'affichera au-dessus des autres éléments d'interface utilisateur non partagés. Une fois la transition terminée, l'élément est supprimé de la superposition vers son propre DrawScope.

Pour découper un élément partagé selon une forme, utilisez la fonction Modifier.clip() standard. Placez-le après 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
)

Si vous devez vous assurer qu'un élément partagé n'est jamais affiché en dehors d'un conteneur parent, vous pouvez définir clipInOverlayDuringTransition sur sharedElement(). Par défaut, pour les limites partagées imbriquées, clipInOverlayDuringTransition utilise le tracé de détourage du sharedBounds() parent.

Pour que certains éléments d'interface utilisateur, comme une barre inférieure ou un bouton d'action flottant, restent toujours au premier plan lors d'une transition d'élément partagé, utilisez Modifier.renderInSharedTransitionScopeOverlay(). Par défaut, ce modificateur conserve le contenu dans la superposition pendant la durée de la transition partagée.

Par exemple, dans Jetsnack, le BottomAppBar doit être placé au-dessus de l'élément partagé jusqu'à ce que l'écran ne soit plus visible. L'ajout du modificateur au composable permet de le maintenir en hauteur.

Sans Modifier.renderInSharedTransitionScopeOverlay()

Avec Modifier.renderInSharedTransitionScopeOverlay()

Vous pouvez souhaiter que votre composable non partagé s'anime et reste au-dessus des autres composables avant la transition. Dans ce cas, utilisez renderInSharedTransitionScopeOverlay().animateEnterExit() pour animer le composable pendant l'exécution de la transition d'élément partagé :

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

Figure 2. Barre d'application inférieure qui glisse vers l'intérieur et l'extérieur lors de la transition de l'animation

Dans les rares cas où vous ne souhaitez pas que votre élément partagé s'affiche dans une superposition, vous pouvez définir renderInOverlayDuringTransition sur sharedElement() sur "false".

Informer les mises en page sœurs des modifications apportées à la taille des éléments partagés

Par défaut, sharedBounds() et sharedElement() n'informent pas le conteneur parent des changements de taille lors des transitions de mise en page.

Pour propager les modifications de taille au conteneur parent lors de la transition, remplacez le paramètre placeholderSize par PlaceholderSize.AnimatedSize. L'élément s'agrandit ou rétrécit en conséquence. Tous les autres éléments de la mise en page s'adaptent à la modification.

PlaceholderSize.ContentSize (par défaut)

PlaceholderSize.AnimatedSize

(Notez que les autres éléments de la liste se déplacent vers le bas en réponse à l'agrandissement de l'élément.)