Personnaliser la transition d'élément partagé

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

Spécification de l'animation

Pour modifier la spécification d'animation utilisée pour le mouvement de la taille et de la 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 avec 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 de prévisualisation (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 remet en page la mise en page enfant de sharedBounds avec des contraintes fixes animées en fonction de la taille cible. La nouvelle mesure est déclenchée par le changement de taille des limites, qui peut potentiellement être chaque frame.

Pour les composables Text, ScaleToBounds est recommandé, car il évite de modifier la mise en page et de redistribuer le texte sur différentes lignes. RemeasureToBounds est recommandé pour les limites dont les proportions sont différentes et si vous souhaitez une continuité fluide entre les deux éléments partagés.

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

ScaleToBounds

RemeasureToBounds

Activer et désactiver dynamiquement les éléments partagés

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 le sens de la navigation ou l'état actuel de l'interface utilisateur.

Pour contrôler si la transition d'élément partagé a lieu, 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 les animations en cours. Si vous devez supprimer l'élément pendant l'animation, vous pouvez remplacer shouldKeepEnabledForOngoingAnimation dans l'interface SharedContentConfig pour renvoyer la valeur "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 est animée entre son état de début et son état final. Ce comportement peut être indésirable lors de l'animation de contenu tel 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 et que sa taille augmente. Dans le deuxième exemple, le texte n'est pas redistribué à mesure qu'il s'agrandit. L'ajout de Modifier.skipToLookaheadSize() empêche la redistribution à mesure qu'il s'agrandit.

Aucun Modifier.skipToLookaheadSize() : notez la redistribution du texte "Lorem Ipsum"

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

Découpage et superpositions

Pour que les éléments partagés soient partagés entre différents composables, le rendu du composable est élevé dans une superposition de calque lorsque la transition est lancée vers sa correspondance dans la destination. L'effet est qu'il échappe aux limites du parent et à ses transformations de calque (par exemple, l'alpha et l'échelle).

Il s'affiche 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é en 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é ne s'affiche jamais 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 chemin de découpage du parent sharedBounds().

Pour que des éléments d'interface utilisateur spécifiques, tels qu'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 période où la transition partagée est active.

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 le maintient élevé.

Sans Modifier.renderInSharedTransitionScopeOverlay()

Avec Modifier.renderInSharedTransitionScopeOverlay()

Vous pouvez également animer votre composable non partagé et le laisser au-dessus des autres composables avant la transition. Dans ce cas, utilisez renderInSharedTransitionScopeOverlay().animateEnterExit() pour animer le composable lorsque la transition d'élément partagé s'exécute :

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 le cas rare où vous ne souhaitez pas que votre élément partagé soit rendu dans une superposition, vous pouvez définir renderInOverlayDuringTransition sur sharedElement() sur "false".

Notifier les mises en page frères des modifications apportées à la taille de l'élément partagé

Par défaut, sharedBounds() et sharedElement() n'informent pas le conteneur parent des modifications de taille lors de la transition de la 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. Tous les autres éléments de la mise en page répondent à la modification.

PlaceholderSize.ContentSize (par défaut)

PlaceholderSize.AnimatedSize

(Notez comment les autres éléments de la liste descendent en réponse à l'agrandissement d'un élément)