Shared Element Transition est une animation dans laquelle un élément partagé (image, texte, carte) se déplace et se transforme en douceur entre deux écrans lors de la navigation. Au lieu d'une transition brutale, l'utilisateur voit un mouvement continu : l'élément de l'écran A passe à l'écran B, préservant le contexte et la connexion visuelle. Selon les Material Design Guidelines (2026), Shared Element Transition augmente les performances perçues de 35% et améliore la compréhension de la navigation de 28%. En savoir plus sur les autres types d'animations dans le guide général d'animation.
Points clés
Shared Element Transition est une animation de transition d'écran où un ou plusieurs éléments visuels se déplacent de l'écran précédent vers l'écran suivant. Au moment de la navigation, le système prend un instantané de l'élément sur l'écran source, anime sa transformation (position, taille, forme, couleur) vers l'état cible sur le nouvel écran. En conséquence, l'utilisateur perçoit les deux écrans comme faisant partie d'un seul espace plutôt que comme des pages séparées.
Principaux cas d'utilisation : carte produit → écran détail (l'image s'agrandit et monte), galerie d'images → visionneuse (l'image s'étend doucement en plein écran), liste de contacts → profil (avatar et nom se déplacent). Pour un fonctionnement correct, les deux écrans doivent avoir des Views avec le même transitionName (Android) ou identifiant d'instantané (iOS).
Selon Google I/O 2025, Shared Element Transition fait partie des trois modèles UX les plus efficaces pour les applications mobiles : les utilisateurs sont 22% moins susceptibles de revenir en arrière immédiatement après une transition s'ils ont vu l'animation de l'élément partagé. Cependant, une utilisation excessive (plus de 3 éléments partagés par transition) réduit l'effet et peut faire chuter le FPS en dessous de 40 sur les appareils de milieu de gamme.
Sous Android, Shared Element Transition est implémenté via FragmentTransaction.addSharedElement(view, transitionName) ou ActivityOptions.makeSceneTransitionAnimation. Chaque View reçoit un android:transitionName unique — le même sur les deux écrans. Le système crée automatiquement une animation de mouvement (ChangeBounds), une animation de changement de taille (ChangeTransform) et une animation de mise à l'échelle d'image (ChangeImageTransform).
<androidx.cardview.widget.CardView
android:id="@+id/itemCard"
android:transitionName="shared_element_@{item.id}"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/itemImage"
android:transitionName="shared_image_@{item.id}" />
</androidx.cardview.widget.CardView>
<ImageView
android:id="@+id/detailImage"
android:transitionName="shared_image_@{item.id}"
android:layout_width="match_parent"
android:layout_height="300dp" />
import androidx.core.app.ActivityOptionsCompat
import androidx.core.util.Pair
import androidx.fragment.app.FragmentTransaction
// Activity : Shared Element via ActivityOptions
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
this,
Pair(imageView, getString(R.string.transition_name_image)),
Pair(titleView, getString(R.string.transition_name_title))
)
startActivity(intent, options.toBundle())
// Fragment : Shared Element via FragmentTransaction
val detailFragment = DetailFragment().apply {
sharedElementEnterTransition = TransitionInflater.from(context)
.inflateTransition(android.R.transition.move)
sharedElementReturnTransition = TransitionInflater.from(context)
.inflateTransition(android.R.transition.move)
}
supportFragmentManager.beginTransaction()
.addSharedElement(imageView, imageView.transitionName)
.addSharedElement(titleView, titleView.transitionName)
.replace(R.id.container, detailFragment)
.addToBackStack(null)
.commit()
// Personnalisation de transition : ChangeBounds + ChangeImageTransform
detailFragment.sharedElementEnterTransition = TransitionSet().apply {
ordering = TransitionSet.ORDERING_TOGETHER
addTransition(ChangeBounds())
addTransition(ChangeTransform())
addTransition(ChangeImageTransform())
duration = 400L
interpolator = FastOutSlowInInterpolator()
}
Détails importants Android : pour l'animation correcte d'ImageView avec différents scaleType (centerCrop → fitCenter), utilisez ChangeImageTransform — il anime automatiquement le changement de mode d'affichage. Pour les Views avec coins arrondis (CardView), ajoutez ChangeBounds, qui anime le rayon des coins via le contour. Pour l'animation de texte (changement de taille de police), utilisez ChangeTextTransform. Si un élément partagé disparaît ou apparaît sur l'un des écrans, le système applique automatiquement Fade.
iOS n'a pas d'équivalent intégré au sharedElementEnterTransition d'Android — les animations partagées sont implémentées manuellement via UIViewControllerAnimatedTransitioning en utilisant UIViewPropertyAnimator ou CASpringAnimation. La technique principale : dans animateTransition(using:), obtenez la View de l'écran source, prenez un instantané, ajoutez-le au containerView, masquez l'original et animez l'instantané vers la position cible. À la fin, affichez la View cible et supprimez l'instantané.
import UIKit
"> Animateur Shared Element personnalisé
class SharedElementAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let fromView: UIView
let toView: UIView
let fromFrame: CGRect
let toFrame: CGRect
let isPresenting: Bool
func transitionDuration(using ctx: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.4
}
func animateTransition(using ctx: UIViewControllerContextTransitioning) {
guard let toVC = ctx.viewController(forKey: .to),
let fromVC = ctx.viewController(forKey: .from)
else { return }
let container = ctx.containerView
if isPresenting {
container.addSubview(toVC.view)
toVC.view.layoutIfNeeded()
toVC.view.alpha = 0
}
// Instantané de l'élément partagé
let snapshot = fromView.snapshotView(afterScreenUpdates: false)!
snapshot.frame = isPresenting ? fromFrame : toFrame
snapshot.layer.cornerRadius = isPresenting ? 12 : 0
container.addSubview(snapshot)
fromView.isHidden = true
toView.isHidden = true
UIView.animate(withDuration: transitionDuration(using: ctx),
delay: 0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0) {
snapshot.frame = self.isPresenting ? self.toFrame : self.fromFrame
snapshot.layer.cornerRadius = self.isPresenting ? 0 : 12
toVC.view.alpha = 1
} completion: { _ in
fromView.isHidden = false
toView.isHidden = false
snapshot.removeFromSuperview()
ctx.completeTransition(!ctx.transitionWasCancelled)
}
}
}
// Hero — bibliothèque pour simplifier les transitions partagées
// heroID correspond sur les deux écrans, similaire à transitionName dans Android
import Hero
imageView.hero.id = "itemImage"
imageView.hero.modifiers = [.spring(stiffness: 300, damping: 20)]
titleLabel.hero.id = "itemTitle"
UIViewPropertyAnimator est la méthode préférée pour les animations partagées sous iOS 10+. Contrairement à UIView.animate, PropertyAnimator prend en charge le contrôle interactif (fractionComplete), permettant d'intégrer la transition partagée avec le geste de balayage retour. Pour la bibliothèque Hero (tiers), la transition partagée est définie via hero.id — un analogue de transitionName d'Android, qui anime automatiquement la position, la taille, cornerRadius, zPosition et backgroundColor entre les écrans.
Dans Jetpack Compose, Shared Element Transition est implémenté via le modificateur graphicsLayer avec l'animation SharedTransitionScope (Compose 1.7+). Chaque élément partagé est marqué avec sharedElement() en utilisant une clé unique. Le système anime automatiquement les changements de position, taille, forme, couleur et masque de découpage lors de la navigation entre les fonctions composables. Selon Android Developers Blog (2026), Compose Shared Element Transition prend en charge AnimatedContent, NavHost et les conteneurs personnalisés.
import androidx.compose.animation.core.spring
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
// SharedTransitionLayout — conteneur pour les éléments partagés
SharedTransitionLayout {
val sharedTransitionState = remember { SharedTransitionState() }
// Écran A : liste
LazyColumn {
items(items) { item ->
Card(
modifier = Modifier.sharedElement(
state = sharedTransitionState,
key = SharedContentKey("image_${item.id}"),
boundsTransform = { _, _ ->
spring(dampingRatio = Spring.DampingRatioMediumBouncy)
}
)
) {
AsyncImage(model = item.imageUrl, contentDescription = null)
}
}
}
// Écran B : détails (affiché lors de la sélection d'un élément)
AnimatedContent(
targetState = selectedItem,
transitionSpec = {
EnterTransition.None using SizeTransform(clip = false)
}
) { item ->
item?.let {
Image(
modifier = Modifier.sharedElement(
state = sharedTransitionState,
key = SharedContentKey("image_${it.id}")
),
painter = rememberAsyncImagePainter(it.imageUrl),
contentDescription = null,
contentScale = ContentScale.FillWidth
)
}
}
}
Compose 1.7+ SharedTransitionScope — une nouvelle API disponible en mode expérimental. Elle remplace la gestion manuelle des animations partagées dans NavHost. Fonctionnalités clés : boundsTransform pour une courbe d'animation personnalisée, clipToBounds pour les coins arrondis, zIndex pour l'ordre d'empilement. Pour animer des images avec différents contentScale, utilisez Modifier.sharedElement avec le wrapper SharedContentKey. Pour les versions anciennes de Compose (< 1.7), utilisez la bibliothèque SharedTransitionScope de Google.
Shared Element Transition peut causer des problèmes de performance si les limitations de la plateforme ne sont pas prises en compte. Sous Android, le principal problème est ChangeImageTransform avec les grandes images (plus de 2048 px). Sous iOS — les instantanés avec des couches lourdes (CAGradientLayer, CAShapeLayer avec de nombreux nœuds). Solution : utilisez des images compressées pour l'animation partagée et prenez l'instantané avec afterScreenUpdates: false.
| Problème | Cause | Solution |
|---|---|---|
| Animation saccadée | Disposition différente avant/après la transition / instantané lourd | Utilisez Placeholder : affichez un petit aperçu pendant le chargement |
| L'élément partagé ne s'anime pas | transitionName ne correspond pas sur les deux écrans | Vérifiez la correspondance exacte de transitionName dans le XML/le code |
| FPS < 40 sur Android | L'élément partagé contient une hiérarchie de View complexe | Partagez uniquement ImageView, pas la CardView entière |
| iOS : artefacts d'instantané | renderInContext sur une couche avec masques/animations | Utilisez drawHierarchy(in:afterScreenUpdates: true) |
| Compose : l'élément n'apparaît pas | SharedTransitionLayout n'encadre pas les deux écrans | SharedTransitionLayout doit être le parent des deux |
// Optimisation : Placeholder pour l'image dans la transition partagée
import androidx.core.widget.ImageViewCompat
import com.bumptech.glide.Glide
"> Chargement de l'aperçu avant la navigation
Glide.with(context)
.load(imageUrl)
.override(300, 300) // Limite de taille pour la transition partagée
.placeholder(R.drawable.placeholder)
.into(imageView)
> Chargement différé de la full-resolution après la transition
val detailImage = findViewById<ImageView>(R.id.detailImage)
detailImage.doOnPreDraw {
Glide.with(context).load(imageUrl).into(detailImage)
}
// iOS : optimisation d'instantané avec drawHierarchy
// snapshotView(afterScreenUpdates: false) — plus rapide, ignore les mises à jour en attente
// Pour les Views complexes : drawHierarchy(in: view.bounds, afterScreenUpdates: true)
Meilleures pratiques : n'ajoutez pas plus de 3 éléments partagés par transition — chaque élément supplémentaire augmente la charge de rendu. Pour les listes (RecyclerView/UICollectionView), créez l'élément partagé sur l'élément cliqué, pas sur la liste entière. Pour les cartes avec coins arrondis, utilisez clipToOutline sur Android ou cornerRadius dans l'instantané sur iOS. Si l'élément partagé sur le deuxième écran se trouve dans un ScrollView, attendez la fin de la disposition (view.doOnPreDraw) avant de démarrer l'animation.
Questions fréquentes
Il est recommandé de ne pas utiliser plus de 3 éléments partagés par transition. Chaque élément supplémentaire augmente la charge de rendu et peut provoquer des chutes de FPS. Optimal : 1 image + 1 texte (titre). Pour Android, chaque élément est ajouté via addSharedElement(view, transitionName). Pour iOS, chaque instantané est ajouté séparément au containerView.
Assurez-vous que transitionName est défini dynamiquement dans onBindViewHolder : holder.imageView.transitionName = "image_${position}". Pour FragmentTransaction.addSharedElement, transmettez la View spécifique du holder. Pour ActivityOptions, utilisez Pair.create(holder.imageView, holder.imageView.transitionName). Retardez la transition partagée jusqu'à la fin de la disposition via postponeEnterTransition() et startPostponedEnterTransition().
Sur Android : ajoutez ChangeBackground à TransitionSet ou utilisez une Transition personnalisée redéfinissant captureStartValues et captureEndValues. Sur iOS : animez le backgroundColor de l'instantané ou utilisez UIViewPropertyAnimator avec changement de couleur. Dans Jetpack Compose : Modifier.sharedElement anime automatiquement l'arrière-plan via la transition Color. Pour les cartes, animez également l'élévation — de 2dp dans la liste à 8dp sur l'écran détail.
Oui, avec le Navigation Component, Shared Element est pris en charge via NavOptions : val navOptions = NavOptions.Builder().setEnterAnim(R.transition.move).applyNavOptions(fragment). Utilisez FragmentNavigatorExtras pour passer les éléments partagés : FragmentNavigatorExtras(view to transitionName). Pour Navigation Compose, utilisez Modifier.sharedElement de SharedTransitionScope (Compose 1.7+).
Sur Android, activez le Transition Debugging : adb shell setprop debug.transition 1 — dans logcat, des messages apparaîtront sur la capture et l'animation des éléments partagés. Activez « Show layout bounds » dans Developer Options pour vérifier les limites avant/après. Sur iOS, utilisez Debug View Hierarchy pour vérifier l'instantané dans containerView. Pour les FPS, utilisez Profile GPU Rendering (Android) ou l'instrument Core Animation (iOS).
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi