Une animation échelonnée se compose d'animations séquentielles ou superposées. L'animation peut être purement séquentielle, avec un changement se produisant après le suivant, ou elle peut se chevaucher partiellement ou complètement. Lorsque l'animation est séquentielle, les éléments sont animés séquentiellement avec un léger décalage entre les heures de début de chaque élément. Cela crée un effet en cascade ou en ondulation, dans lequel l'animation semble se déplacer à travers les éléments par étapes plutôt que d'un seul coup. Les animations échelonnées sont considérées comme un type de micro-interaction car elles améliorent l'expérience utilisateur en fournissant un retour subtil et interactif qui guide l'utilisateur à travers une interface. Dans Flutter, vous pouvez créer des micro-interactions en créant des animations subtiles à l'aide de l'animation implicite ou explicite. Pour le contexte, les animations implicites sont conçues pour être simples et faciles à utiliser car les détails de l'animation sont abstraits tandis que les animations explicites sont plus adaptées aux animations complexes car elles offrent un contrôle complet du processus d'animation. Ceci est particulièrement utilisé lorsque vous avez besoin d’avoir un contrôle plus précis sur l’animation. Dans cet article, nous approfondirons le concept de micro-interactions, puis pour un cas d'utilisation de micro-interaction, nous utiliserons une animation explicite pour créer une animation échelonnée qui anime les enfants contenus dans un widget colonne. Quel est le concept de micro-interactions ? Les bons produits sont des produits qui offrent de bonnes fonctionnalités et des détails. Les fonctionnalités attirent les gens vers vos produits, mais les détails les retiennent. Ces détails permettent à votre application de se démarquer des autres. Grâce aux micro-interactions, vous pouvez créer ces détails en fournissant des commentaires agréables à vos utilisateurs. Le concept de micro-interactions repose sur l’idée que de petits détails peuvent avoir un impact important sur l’expérience utilisateur globale. Les micro-interactions peuvent être utilisées pour remplir des fonctions essentielles telles que la communication de commentaires ou du résultat d’une action. Voici des exemples de micro-interactions : Animations des boutons : le bouton change de couleur ou de taille lorsqu'il est survolé ou enfoncé. Indicateurs de chargement : Animations qui indiquent à un utilisateur qu'un processus est en cours. Gestes de balayage : animations qui répondent aux gestes de balayage. Transitions de navigation : animations fluides lors de la transition entre les écrans. Micro-interactions dans des applications réelles Ci-dessous, nous pouvons voir des applications réelles de micro-interactions, ces animations subtiles sont créées dans Flutter, pour améliorer l'expérience utilisateur. 👇🏽 Les références de conception de ces applications proviennent de Dribbble Comment créer une animation échelonnée dans Flutter Dans cet exemple, nous allons créer une animation échelonnée qui anime les enfants dans un widget de colonne lorsque cette page est balayée. Celui-ci sera construit en utilisant l'approche d'animation explicite car dans ce cas, nous devrons avoir un contrôle total sur la façon dont nous voulons que l'animation s'exécute. Voici à quoi ressemblera l'animation une fois terminée 👇🏽 Prérequis Pour tirer le meilleur parti de ce tutoriel, vous devez disposer des éléments suivants : Une compréhension de base de la façon dont les animations sont créées dans Flutter Une bonne maîtrise des fondamentaux de Flutter & Dart Un éditeur de code, soit VScode ou Android Studio Un émulateur ou un appareil sur lequel s'appuyer Une fois que vous avez vérifié tous les prérequis du projet, plongeons-nous dans le vif du sujet. Tout d’abord, nous allons construire l’écran principal qui contient ces deux pages. Ces deux pages seront enveloppées dans un widget de visualisation de page qui contrôle quelle page est affichée lors du balayage. De plus, en bas de l'écran principal, nous avons un indicateur qui nous montre la page sur laquelle nous nous trouvons actuellement. class MainScreen extends StatefulWidget { const MainScreen({super.key}); @override State<MainScreen> createState() => _MainScreenState(); } class _MainScreenState extends State<MainScreen>{ final controller = PageController(keepPage: true); @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: PageView( controller: controller, children: [ Page1(pageController: controller,), Page2(pageController: controller,), ], ), ), bottomNavigationBar: Container( alignment: Alignment.topCenter, padding: const EdgeInsets.only(top: 10), height: 30, child: SmoothPageIndicator( controller: controller, count: 2, effect: const JumpingDotEffect( dotHeight: 10, dotWidth: 10, activeDotColor: Colors.grey, dotColor: Colors.black12, ), ), ) ); } } Le utilisé dans l'extrait de code ci-dessus peut être trouvé sur . Ici, le est utilisé pour afficher la page actuelle en vue. Vous pouvez ajouter le package à votre comme ceci 👇🏽 SmoothPageIndicator pub.dev SmoothPageIndicator pubspec.yaml dependencies: flutter: sdk: flutter smooth_page_indicator: ^1.1.0 Dans les deux pages, nous aurons un widget colonne avec plusieurs widgets cartes vides. Ces widgets de cartes vides serviront à remplir la colonne afin que nous puissions pleinement voir et apprécier l'animation décalée. Nous allons créer le widget de carte vide en tant que composant réutilisable afin de ne pas avoir à le créer à partir de zéro à chaque endroit où nous l'utilisons. class EmptyCard extends StatelessWidget { const EmptyCard({super.key, required this.width, required this.height}); final double width; final double height; @override Widget build(BuildContext context) { return Container( height: height, width: width, decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(10)), color: Colors.blue.shade200, boxShadow: const [ BoxShadow( color: Colors.black12, blurRadius: 4, offset: Offset(0,4), ) ] ), ); } } Une fois tous ces codes passe-partout éliminés, nous allons maintenant commencer à créer l’animation. Tout d’abord, nous allons créer un nouveau widget avec état que nous appellerons . Il aura plusieurs paramètres pour contrôler la façon dont l'animation va se dérouler. AnimateWidget class AnimateWidget extends StatefulWidget { const AnimateWidget({super.key, required this.duration, required this.position, required this.horizontalOffset, required this.child, required this.controller, }); final Duration duration; final int position; final double? horizontalOffset; final Widget child; final PageController controller; @override State<AnimateWidget> createState() => _AnimateWidgetState(); } Avec les paramètres d' comme indiqué dans l'extrait ci-dessus, nous pouvons contrôler avec succès : AnimateWidget Durée de l'animation. Le , ce qui lui permet de déclencher l'animation lorsque la page est glissée. pageController La quantité de décalage horizontal de l’élément animé. Ensuite, dans , nous définirons les éléments suivants : AnimateWiget : Utilisé pour contrôler la séquence d'animation. AnimationController Page actuelle : sera utilisé pour contenir les données de la page actuelle. Timer : Sera utilisé pour déclencher l’animation après un certain délai ; c'est ce qui provoquera l'effet en cascade d'animation échelonnée. class _AnimateWidgetState extends State<AnimateWidget> with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin{ @override bool get wantKeepAlive => true; late AnimationController animationController; int currentPage = 0; Timer? _timer; @override void initState() { super.initState(); animationController = AnimationController(vsync: this, duration: widget.duration); _timer = Timer(getDelay(), animationController.forward); widget.controller.addListener(() { currentPage = widget.controller.page!.round(); if(currentPage == widget.controller.page){ _timer = Timer(getDelay(), animationController.forward); } }); } @override void dispose() { _timer?.cancel(); animationController.dispose(); super.dispose(); } Duration getDelay(){ var delayInMilliSec = widget.duration.inMilliseconds ~/ 6; int getStaggeredListDuration(){ return widget.position * delayInMilliSec; } return Duration(milliseconds: getStaggeredListDuration()); } En décomposant l'extrait de code ci-dessus, vous remarquerez que : Nous avons utilisé pour contrôler la séquence d'animation, c'est pour cette raison que nous avons introduit le . AnimationController SingleTickerProviderStateMixin Dans la méthode , nous avons initialisé , puis déclenché l'animation à l'aide , lors de l'entrée de page et du balayage de page. init state AnimationController animationController.forward Nous avons utilisé le Timer pour contrôler le déclenchement de l'animation en fonction du délai. Dans la méthode , nous avons nettoyé les ressources. dispose Nous avons utilisé pour préserver l'état du widget. Ceci permet d'empêcher la suppression du , lorsqu'une page est glissée et n'est plus visible. AutomaticKeepAliveClientMixin AnimationController Dans la fonction , nous avons calculé le délai avant le déclenchement de l'animation pour chaque élément. Pour y parvenir, nous avons divisé la durée en millisecondes par 6 et approximé les résultats, puis l'avons multiplié par la position. Il s'agit de la position (dans ce cas, l'index) de l'élément. getDelay Dans la méthode Build, nous renvoyons un . Dans ce générateur animé, nous renverrons une fonction widget appelée qui renvoie un widget . Dans la fonction , nous avons la fonction . AnimatedBuilder _slideAnimation Transform.translate _slideAnimation offsetAnimation La fonction renvoie la propriété Animation qui est utilisée dans le widget . Le widget anime le widget enfant, en utilisant la valeur de l'animation. offsetAnimation Transform.translate Transform.translate @override Widget build(BuildContext context) { super.build(context); return AnimatedBuilder( animation: animationController, builder: (context, child){ return _slideAnimation(animationController); } ); } Widget _slideAnimation(Animation<double> animationController){ Animation<double> offsetAnimation(double offset, Animation<double> animationController) { return Tween<double>(begin: offset, end: 0.0).animate( CurvedAnimation( parent: animationController, curve: const Interval(0.0, 1.0, curve: Curves.ease), ), ); } return Transform.translate( offset: Offset( widget.horizontalOffset == 0.0 ? 0.0 : offsetAnimation(widget.horizontalOffset!, animationController).value, 0.0, ), child: widget.child, ); } Ceci est le code complet de la classe 👇🏽 AnimateWidget class AnimateWidget extends StatefulWidget { const AnimateWidget({super.key, required this.duration, required this.position, required this.horizontalOffset, required this.child, required this.controller, }); final Duration duration; final int position; final double? horizontalOffset; final Widget child; final PageController controller; @override State<AnimateWidget> createState() => _AnimateWidgetState(); } class _AnimateWidgetState extends State<AnimateWidget> with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin{ @override bool get wantKeepAlive => true; late AnimationController animationController; int currentPage = 0; Timer? _timer; @override void initState() { super.initState(); animationController = AnimationController(vsync: this, duration: widget.duration); _timer = Timer(getDelay(), animationController.forward); widget.controller.addListener(() { currentPage = widget.controller.page!.round(); if(currentPage == widget.controller.page){ _timer = Timer(getDelay(), animationController.forward); } }); } @override void dispose() { _timer?.cancel(); animationController.dispose(); super.dispose(); } Duration getDelay(){ var delayInMilliSec = widget.duration.inMilliseconds ~/ 6; int getStaggeredListDuration(){ return widget.position * delayInMilliSec; } return Duration(milliseconds: getStaggeredListDuration()); } @override Widget build(BuildContext context) { super.build(context); return AnimatedBuilder( animation: animationController, builder: (context, child){ return _slideAnimation(animationController); } ); } Widget _slideAnimation(Animation<double> animationController){ Animation<double> offsetAnimation(double offset, Animation<double> animationController) { return Tween<double>(begin: offset, end: 0.0).animate( CurvedAnimation( parent: animationController, curve: const Interval(0.0, 1.0, curve: Curves.ease), ), ); } return Transform.translate( offset: Offset( widget.horizontalOffset == 0.0 ? 0.0 : offsetAnimation(widget.horizontalOffset!, animationController).value, 0.0, ), child: widget.child, ); } } Ensuite, pour utiliser cette classe dans un widget de colonne, nous allons créer une classe avec une méthode statique appelée qui renvoie une liste. Dans cette méthode, nous transmettons tous les paramètres nécessaires, y compris une liste . Le paramètre est l’endroit où nous transmettrons la liste des éléments que nous allons animer. AnimateWidget toStaggeredList children children Ensuite, nous cartographierons les enfants, en enveloppant chaque enfant avec le . AnimateWidget class AnimateList{ static List<Widget>toStaggeredList({ required Duration duration, double? horizontalOffset, required PageController controller, required List<Widget>children, })=> children .asMap() .map((index, widget){ return MapEntry( index, AnimateWidget( duration: duration, position: index, horizontalOffset: horizontalOffset, controller: controller, child: widget, ) ); }) .values .toList(); } Dans , nous transmettons les paramètres requis pour animer avec succès chaque enfant de la liste. Grâce à la méthode , nous pouvons désormais l'implémenter sur les deux pages sur lesquelles nous travaillons. AnimateWidget AnimateList.toStaggeredList class Page1 extends StatelessWidget { const Page1({super.key, required this.pageController}); final PageController pageController; @override Widget build(BuildContext context) { return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( children: AnimateList.toStaggeredList( duration: const Duration(milliseconds: 375), controller: pageController, horizontalOffset: MediaQuery.of(context).size.width / 2, children: [ const EmptyCard(width: 250, height: 50,), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 180, height: 80,), ), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 270, height: 50,), ), const Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ EmptyCard(height: 50, width: 70), EmptyCard(height: 50, width: 70), EmptyCard(height: 50, width: 70), ], ), ), const EmptyCard(width: 250, height: 50,), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 180, height: 80,), ), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 270, height: 50,), ), const Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ EmptyCard(height: 50, width: 70), EmptyCard(height: 50, width: 70), EmptyCard(height: 50, width: 70), ], ), ), ], ), ), ); } } class Page2 extends StatelessWidget { const Page2({super.key, required this.pageController}); final PageController pageController; @override Widget build(BuildContext context) { return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( children: AnimateList.toStaggeredList( duration: const Duration(milliseconds: 375), controller: pageController, horizontalOffset: MediaQuery.of(context).size.width / 2, children: [ const EmptyCard(width: 220, height: 70,), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 300, height: 70,), ), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 200, height: 50,), ), const Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ EmptyCard(height: 70, width: 70), EmptyCard(height: 70, width: 70), ], ), ), const EmptyCard(width: 220, height: 70,), const Padding( padding: EdgeInsets.only(top: 20), child: EmptyCard(width: 300, height: 70,), ), const Padding( padding: EdgeInsets.symmetric(vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ EmptyCard(height: 70, width: 70), EmptyCard(height: 70, width: 70), ], ), ), ], ), ), ); } } Dans les enfants du widget de colonne, nous passerons et transmettrons les paramètres nécessaires, y compris les widgets qui doivent être affichés dans la colonne. Avec cela, nous avons réussi à créer une animation décalée déclenchée lors du balayage. Vous pouvez consulter le code complet . AnimateList.toStaggeredList ici Voici notre résultat final : Conclusion Nous sommes arrivés à la fin de ce tutoriel. À ce stade, nous avons abordé le concept de micro-interactions et son impact sur l'expérience utilisateur. Nous avons également suivi le processus de création d'une animation échelonnée des éléments dans un widget de colonne déclenché lors du balayage de la page. Il existe de nombreux types de micro-interactions que vous pouvez ajouter à votre application pour améliorer l'expérience utilisateur de votre projet ; vous pouvez expérimenter en créant des animations plus interactives dans Flutter. Si vous avez trouvé cet article utile, vous pouvez le soutenir en laissant un like ou un commentaire. Vous pouvez également me suivre pour des articles plus connexes. Les références Forfait d’animation échelonnée Flutter