Aujourd’hui, nous savons qu’une personne passe en moyenne 3 heures sur son smartphone par jour. Un argument de poids, pour comprendre que l’usage et l’émotion sont deux aspects primordiaux dans la réalisation de nos applications.
La majeure partie de l’utilisation du smartphone passe par les réseaux sociaux et les jeux, nous devons donc faire en sorte que l’utilisateur ait envie de revenir sur notre application.
Il existe différentes façons de procurer une émotion dans notre application. Certains de mes articles Flutter Tips of the month se focaliseront sur les animations et transitions qui ne sont bien entendu qu’un des multiples facteurs d’émotions.
Hero
Ce super widget permet de créer une transition entre deux pages. Il a la faculté de se déplacer, se transformer tel un super-heros ! Si vous êtes accoutumé au développement mobile, vous devez connaître ce type de transition sous un autre nom : Shared Element Transitions.
Un peu de théorie
Le widget Hero est identifié grâce à un tag unique par page. Il est important de prendre en compte cette contrainte si vous l’utilisez dans des ListView par exemple. Lors d’une transition, tous les widgets Hero des pages source et cible sont récupérés afin de créer une animation pour chaque correspondance avec leurs contraintes (taille, position, etc).
À la transition un Overlay est créé dans le Navigator (grâce au widget OverlayEntry) pour interpoler le rendu pendant la durée de transition.
Voici en image ce qui se passe :
Lorsque vous utilisez ce widget pour la première fois tout s’anime par magie ! J’ai donc décidé de creuser un peu afin de comprendre comment tout cela se passe.
HeroController
Le Navigator que nous utilisons au quotidien nous permet d’écouter chaque action grâce à la classe NavigatorObserver. Celle-ci permet de recevoir tous les états avant qu’ils ne se produisent et d’appliquer une stratégie pour l’ensemble des méthodes suivantes :
didPush
didPop
didRemove
didReplace
didStartUserGesture
didStopUserGesture
HeroController hérite de NavigatorObserver
, il récupère l’ensemble des Hero, ajoute et applique tous les traitements d’animations évoqués précédemment sur la propriété overlay
de votre navigateur.
Voici comment vous pouvez ajouter un widget par dessus toute votre navigation :
OverlayEntry overlayEntry; // ... overlayEntry = OverlayEntry( builder: (BuildContext context) => Positioned( top: 30, right: 30, child: RepaintBoundary( child: RaisedButton( onPressed: () { overlayEntry.remove(); // Remove in Navigator.of(context).overlay }, child: Text("test"), ), ), ), ); Navigator.of(context).overlay.insert(overlayEntry);
Comment HeroController récupére la liste des Hero dans une page ?
L’arbre des Element
Lorsque nous construisons une application Flutter, nous manipulons uniquement (le plus souvent dans un usage classique) un arbre de widgets, mais il faut savoir qu’il en existe deux autres :
- L’arbre des Element
- L’arbre des Render Object
Je ne vais pas rentrer dans le détail, mais lorsque nous récupérons un BuildContext
nous récupérons en réalité un Element
. Le plus souvent nous récupérons les implémentations StatefulElement et StatelessElement mais il en existe près d’une trentaine :
HeroController utilise l’Element parent d’une page pour récupérer l’ensemble des Hero. Pour parcourir tous les éléments sous-jacents, il suffira d’utiliser la méthode visiteChildElements
sur le BuildContext
.
Voici un exemple pour récupérer tous nos Hero :
Map<Object, Hero> _heroes = {}; void visitHero(BuildContext context) { context.visitChildElements((Element element) { if (element.widget is Hero) { final StatefulElement hero = element; final Hero heroWidget = element.widget; final dynamic tag = heroWidget.tag; final Widget child = heroWidget.child; _heroes[tag] = heroWidget; print("Add new Hero, tag = $tag, type = ${child.runtimeType}"); } else { element.visitChildren(visitHero); } }); }
Les différents Hero seront récupérés, stockés et ajoutés à Navigator.overlay
pour effectuer l’animation.
Maintenant nous savons comment fonctionne Hero place à la pratique !
Commençons simplement par un Hero par dessus un widget Image
.
Sur votre première page, ajoutez le body suivant (en utilisant l’image de votre choix) à votre Scaffold :
Container( padding: const EdgeInsets.only(bottom: 48.0), alignment: Alignment.bottomCenter, child: Hero( tag: "poulpy", child: Image.asset( "assets/poulpy.webp", width: 100, height: 100, color: Colors.red, ), ), )
Remarquez que le widget Image sait gérer les images au format webp 😁
Puis sur la deuxième page, insérez ce body :
Container( alignment: Alignment.center, child: Hero( tag: "poulpy", child: Image.asset( "assets/poulpy.webp", width: 300, height: 300, color: Colors.red, ), ), )
Dans la fonction de votre choix (au clic sur un FloatingActionButton
par exemple), effectuez une redirection entre vos écrans grâce à Navigator.push
:
final route = MaterialPageRoute(builder: (_) => Page2()); Navigator.push(context, route);
Vous devriez avoir un résultat similaire à ceci :
La transition de notre mascotte “Pouply” s’effectue correctement. Mais lorsqu’on utilise les gestures de navigation (notamment sur iOS) nous remarquons que la transition ne s’effectue pas :
Pour prendre en compte les gestures, il faut spécifier la valeur true
à la propriété transitionOnUserGestures
sur chaque widget Hero :
Hero( tag: "poulpy", transitionOnUserGestures: true, child: ... )
Vous obtiendrez le résultat suivant :
FlightShuttleBuilder
Ce builder a pour but d’appliquer une animation sur un widget Hero pendant une transition entre deux pages. La callback retourne différents BuildContext permettant de récupérer leurs widgets respectifs et d’une animation correspond à l’interpolation. Voici une façon simple d’animer une rotation en utilisant le FlightShuttleBuilder:
Hero( tag: "poulpy", // ... flightShuttleBuilder: (BuildContext flightContext, Animation<double> animation, HeroFlightDirection flightDirection, BuildContext fromHeroContext, BuildContext toHeroContext) { final Hero toHero = toHeroContext.widget; return RotationTransition( turns: animation, child: toHero.child, ); }, )
Afin d’obtenir le résultat suivant :
Vous pouvez définir un flightShuttleBuilder
à la fois dans le widget Hero de départ et de destination.
Il sera donc possible d’avoir deux animations différentes selon le sens de votre transition sur votre navigateur (push ou pop).
CreateRectTween
Cette propriété va permettre de changer la courbure de la trajectoire lors du survole. Si aucune valeur n’est renseignée, alors une courbure par défaut sera appliquée. Celle-ci est définit dans le widget MaterialApp directement dans l’instanciation du HeroController
:
class _MaterialAppState extends State<MaterialApp> { HeroController _heroController; @override void initState() { super.initState(); _heroController = HeroController(createRectTween: _createRectTween); _updateNavigator(); } // ... RectTween _createRectTween(Rect begin, Rect end) { return MaterialRectArcTween(begin: begin, end: end); } }
Pour appliquer une courbure personnalisée dans le widget Hero, veuillez appliquer le code suivant :
Hero( tag: "poulpy", createRectTween: (begin, end) { return BounceInRectTween(begin, end); }, // ... ) class BounceInRectTween extends RectTween { final Rect begin; final Rect end; BounceInRectTween(this.begin, this.end); @override Rect lerp(double t) { return RectTween(begin: begin, end: end).transform(Curves.bounceIn.transform(t)); } }
BounceInRectTween
est un RectTween qui aura le rôle de définir un Rect
pendant la transition. Les valeurs d’interpolation seront comprises 0.0 et 1.0.
Dans le code ci-dessus, j’utilise l’instance statique bounceIn
de la classe Curves
qui contient plusieurs d’interpolateurs prédéfinis.
N’hésitez-pas à consulter l’ensemble des Curve prédéfinis.
Il est bon de savoir qu’il est possible de créer votre propre Curve si jamais les différentes instances dans la classe Curves ne vous conviennent pas.
Exemple pour créer un Curve
personnalisé :
class DecelerateCurve extends Curve { @override double transformInternal(double t) { t = 1.0 - t; return 1.0 - t * t; } }
Pour finir, vous obtiendrez le rendu suivant en utilisant la classe BounceInRectTween
créée précédemment :
Utilisé à des endroits clés, Hero donnera du sens à certaines de vos transitions.
Grâce à Hero, vous n’aurez plus d’excuse pour ne pas amener un peu d’animation dans vos transitions ! Les animations sont des guides de navigation, mais il faut les utiliser à bon escient.
Série Flutter of the month
- Tips #1 : Personnaliser la shape d’une BottomSheet
- Tips #2 : Hero – le super widget
- Tips #3 : Responsive widgets
- Tips #4 : Codemagic déployer vers Firebase App Distribution
- Tips #5 : Exploiter votre code coverage avec Codecov.io
- Tips #6 : Embarquez à bord du Zeplin, destination Flutter
- Tips #7 : Merry Christmas
- Tips #8 : Firebase Test Lab