Points clés
Hero Animation est une technique d’animation d’interface où un élément partagé se déplace en douceur d’un écran source vers un écran de destination, en préservant son identité. Le nom reflète la métaphore d’un «héros» — un élément clé qui traverse une transition scénarisée entre les écrans, attirant l’attention de l’utilisateur. Contrairement aux transitions standard, Hero Animation ne rompt pas la connexion visuelle : l’utilisateur voit une trajectoire continue de l’élément, ce qui améliore la compréhension de la hiérarchie de navigation.
Le concept de Hero Animation est établi dans Material Design comme une recommandation pour les écrans liste-détail, les galeries et les cartes. L’algorithme d’animation comprend trois phases : sur l’écran source, l’élément est mis en évidence et fixé dans une position absolue ; pendant la transition, il se met à l’échelle, se déplace et change de forme selon le conteneur de destination ; sur l’écran final, l’élément est intégré dans la nouvelle disposition. Selon Material Design 3, une animation héroïque correctement implémentée réduit le nombre d’appuis erronés sur le bouton «retour» de 22%.
Dans Flutter, Hero Animation est implémentée via le widget Hero, qui prend un paramètre obligatoire tag — un identifiant unique. Flutter trouve automatiquement une paire de widgets Hero avec le même tag sur les routes source et destination et lance l’animation. Le widget Hero ne nécessite pas de gestion manuelle de l’animation — le framework calcule indépendamment la trajectoire et applique la transformation via l’objet interne FlightShuttle.
FlightShuttle est une «navette» invisible qui, au moment de la transition, crée une copie superposée du widget Hero, la positionne dans un système de coordonnées absolu et anime la transformation (échelle, translation, rotation) de la position initiale à la position finale. Des courbes personnalisées peuvent être transmises à FlightShuttle via la propriété flightShuttleBuilder, permettant de contrôler la trajectoire de vol : voler en arc, rebondir sur les bords ou ralentir avant l’arrivée. Dans Flutter 3.16+, Hero prend en charge l’animation en préservant clipRect et borderRadius, ce qui donne l’effet de cartes arrondies se transformant en écrans rectangulaires.
Dans SwiftUI, l’équivalent de Hero Animation est le modificateur matchedGeometryEffect, introduit dans iOS 14. Il relie deux éléments via un identifiant (id) et un namespace. Contrairement à Flutter, où Hero intercepte automatiquement la transition, SwiftUI nécessite une déclaration explicite du namespace et l’application du modificateur aux deux parties de la scène — source et destination — dans la même hiérarchie ou via GeometryReader.
L’animation matchedGeometryEffect prend en charge la transformation de la position, de la taille, de la rotation et du rayon des coins. Pour la fluidité, une Animation est spécifiée dans withAnimation. SwiftUI interpole automatiquement les états intermédiaires en fonction de la courbe par défaut easeInOut. Dans iOS 17+, la prise en charge de matchedTransitionSource a été ajoutée pour TabView et NavigationStack — un analogue système de Hero Animation avec une trajectoire prédéfinie pour les transitions standard.
Dans Android, Hero Animation est appelée Shared Element Transition et est implémentée via ActivityOptions.makeSceneTransitionAnimation ou FragmentTransaction.addSharedElement. Chaque élément partagé est marqué avec un attribut transitionName en XML ou via du code, et Android gère l’animation entre les activités. À partir d’Android 5.0 (API 21), les transitions sont devenues partie intégrante du framework ; avant cela, des animations personnalisées devaient être écrites à l’aide de ValueAnimator.
Dans Jetpack Compose, Shared Element Transition est implémentée via les modificateurs Modifier.sharedElement et Modifier.sharedBounds en conjonction avec AnimatedContent ou SharedTransitionLayout (disponible depuis Compose BOM 2024.01). Contrairement au système View, Compose redessine les éléments paresseusement, donc l’élément partagé est mis en cache dans CompositionLocal. Compose prend également en charge l’animation de liste vers détail avec préservation du scrollOffset — une capacité clé pour les transitions Hero. Selon Google I/O 2024, Compose Shared Element Transition est 15% plus rapide que la version View grâce à la gestion directe de RenderNode.
Un exemple de base de Hero Animation dans Flutter : une vignette sur l’écran de liste et une image plein écran sur l’écran de détails. Le tag image-hero relie les deux widgets, et FlightShuttle anime automatiquement la transition de la carte au plein écran.
import 'package:flutter/material.dart';
class ListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
Hero(
tag: 'image-hero',
child: GestureDetector(
onTap: () {
Navigator.push(context,
MaterialPageRoute(
builder: (_) => DetailScreen(),
),
);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
'https://picsum.photos/200',
width: 100, height: 100,
fit: BoxFit.cover,
),
),
),
),
],
),
);
}
}
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Detail')),
body: Center(
child: Hero(
tag: 'image-hero',
child: Image.network(
'https://picsum.photos/800',
width: 300, height: 400,
fit: BoxFit.cover,
),
),
),
);
}
}
Le point clé est le même tag (image-hero) sur les deux écrans. Flutter fait correspondre les widgets Hero par tag pendant la transition de route. Si les tags diffèrent, l’animation ne démarrera pas — l’élément apparaîtra simplement sur le nouvel écran sans transition. N’importe quel type peut être utilisé pour le tag, mais les chaînes sont l’option la plus fiable.
Un exemple de Hero Animation dans SwiftUI avec NavigationStack. Le modificateur matchedGeometryEffect relie l’image dans la liste et l’écran de détails via un namespace, et withAnimation déclenche une transition en douceur.
import SwiftUI
struct HeroExampleView: View {
@Namespace private var heroNamespace
@State private var isDetailShown = false
var body: some View {
VStack {
if isDetailShown {
DetailView(namespace: heroNamespace)
.transition(.identity)
} else {
ListView(namespace: heroNamespace)
.transition(.identity)
}
}
.animation(Animation.easeInOut(duration: 0.4),
value: isDetailShown)
}
}
struct ListView: View {
let namespace: Namespace.ID
var body: some View {
Image("thumbnail")
.resizable()
.frame(width: 100, height: 100)
.cornerRadius(12)
.matchedGeometryEffect(id: "hero", in: namespace)
}
}
struct DetailView: View {
let namespace: Namespace.ID
var body: some View {
Image("thumbnail")
.resizable()
.frame(width: 300, height: 400)
.cornerRadius(0)
.matchedGeometryEffect(id: "hero", in: namespace)
}
}
@Namespace crée un espace de noms pour l’identifiant matchedGeometryEffect. Les deux éléments utilisent le même id (“hero”) et un namespace partagé. Lorsque isDetailShown bascule, SwiftUI anime la transformation de la taille, de la position et du rayon des coins entre les deux vues, créant l’effet héroïque. Pour un NavigationLink standard avec iOS 16+, utilisez navigationTransition(.hero).
Un exemple de Shared Element Transition dans Jetpack Compose avec SharedTransitionLayout. Le modificateur sharedElement anime un élément partagé entre les écrans pendant la navigation.
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun HeroExample() {
SharedTransitionLayout {
var isDetail by remember { mutableStateOf(false) }
AnimatedContent(
targetState = isDetail,
transitionSpec = {
fadeIn() togetherWith fadeOut()
}
) { detail ->
if (detail) {
Box(
modifier = Modifier
.fillMaxSize()
.sharedElement(
state = rememberSharedContentState(key = "hero"),
animatedVisibilityScope = this
)
) {
AsyncImage(
model = "https://picsum.photos/800",
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
}
} else {
Box(
modifier = Modifier
.size(100.dp)
.sharedElement(
state = rememberSharedContentState(key = "hero"),
animatedVisibilityScope = this
)
) {
AsyncImage(
model = "https://picsum.photos/200",
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
}
}
}
}
}
Compose SharedTransitionLayout (Compose BOM 2024.01+) encapsule la hiérarchie, et le modificateur sharedElement avec la clé “hero” relie les éléments initial et final. AnimatedContent bascule l’état, et Compose anime automatiquement la taille, la position et la forme de l’image. Un avantage clé est la prise en charge de l’animation des bounds et clipBounds, offrant l’effet d’arrondi de carte pendant la transition.
Foire aux questions
Oui, Hero Animation est disponible sur les deux plateformes. iOS utilise matchedGeometryEffect dans SwiftUI ou UINavigationController.transitioningDelegate. Android utilise ActivityOptions.makeSceneTransitionAnimation ou Shared Element Transition dans Jetpack Compose avec Modifier.sharedElement. Flutter implémente Hero Animation via le widget Hero, qui fonctionne de manière identique sur les deux plateformes.
Oui, Hero Animation prend en charge plusieurs éléments partagés. Dans Flutter, spécifiez plusieurs widgets Hero avec des tags différents. Dans SwiftUI, utilisez matchedGeometryEffect avec des identifiants différents sur chaque élément. Toutes les animations s’exécutent en parallèle — chaque élément voyage sur sa propre trajectoire.
Les performances dépendent de l’implémentation. FlightShuttle de Flutter traite les images via RenderObject, atteignant 60fps. matchedGeometryEffect de SwiftUI est vectorisé. Pour les grandes images, utilisez precacheImage dans Flutter ou prefetch dans SwiftUI — cela élimine les retards lors de la première transition. Android Compose nécessite un chargement différé via AsyncImage.
Une transition push standard fait glisser tout l’écran, créant l’illusion d’une pile de cartes. Hero Animation fixe l’attention visuelle sur un seul élément — l’utilisateur voit son mouvement continu, ce qui améliore la compréhension de la relation entre les écrans. Push est bon pour la navigation hiérarchique, Hero pour mettre l’accent sur le contenu.
Non, Hero Animation est implémentée avec des outils intégrés. Dans Flutter, c’est le widget Hero du package flutter/material.dart. Dans SwiftUI, c’est le modificateur matchedGeometryEffect. Dans Android Jetpack Compose, ce sont SharedTransitionLayout et Modifier.sharedElement de Compose standard. Des bibliothèques supplémentaires ne sont nécessaires que pour les trajectoires non standard ou les transformations complexes.
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