paint-brush
Création d'une animation échelonnée dans Flutter : un guide des micro-interactionspar@nikkieke
460 lectures
460 lectures

Création d'une animation échelonnée dans Flutter : un guide des micro-interactions

par 19m2024/06/04
Read on Terminal Reader

Trop long; Pour lire

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. Dans Flutter, vous pouvez créer des micro-interactions en créant des animations subtiles à l'aide de l'animation implicite ou explicite. Dans cet article, nous allons créer une animation échelonnée qui anime les enfants dans un widget de colonne lorsque cette page est glissée.
featured image - Création d'une animation échelonnée dans Flutter : un guide des micro-interactions
undefined HackerNoon profile picture
0-item

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 SmoothPageIndicator utilisé dans l'extrait de code ci-dessus peut être trouvé sur pub.dev . Ici, le SmoothPageIndicator est utilisé pour afficher la page actuelle en vue. Vous pouvez ajouter le package à votre pubspec.yaml comme ceci 👇🏽

 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 AnimateWidget . Il aura plusieurs paramètres pour contrôler la façon dont l'animation va se dérouler.

 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' AnimateWidget comme indiqué dans l'extrait ci-dessus, nous pouvons contrôler avec succès :

  • Durée de l'animation.
  • Le pageController , ce qui lui permet de déclencher l'animation lorsque la page est glissée.
  • La quantité de décalage horizontal de l’élément animé.


Ensuite, dans AnimateWiget , nous définirons les éléments suivants :

  • AnimationController : Utilisé pour contrôler la séquence d'animation.

  • 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é AnimationController pour contrôler la séquence d'animation, c'est pour cette raison que nous avons introduit le SingleTickerProviderStateMixin .


  • Dans la méthode init state , nous avons initialisé AnimationController , puis déclenché l'animation à l'aide animationController.forward , lors de l'entrée de page et du balayage de page.


  • Nous avons utilisé le Timer pour contrôler le déclenchement de l'animation en fonction du délai.


  • Dans la méthode dispose , nous avons nettoyé les ressources.


  • Nous avons utilisé AutomaticKeepAliveClientMixin pour préserver l'état du widget. Ceci permet d'empêcher la suppression du AnimationController , lorsqu'une page est glissée et n'est plus visible.


  • Dans la fonction getDelay , 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.


Dans la méthode Build, nous renvoyons un AnimatedBuilder . Dans ce générateur animé, nous renverrons une fonction widget appelée _slideAnimation qui renvoie un widget Transform.translate . Dans la fonction _slideAnimation , nous avons la fonction offsetAnimation .


La fonction offsetAnimation renvoie la propriété Animation qui est utilisée dans le widget Transform.translate . Le widget Transform.translate anime le widget enfant, en utilisant la valeur de l'animation.

 @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 AnimateWidget dans un widget de colonne, nous allons créer une classe avec une méthode statique appelée toStaggeredList qui renvoie une liste. Dans cette méthode, nous transmettons tous les paramètres nécessaires, y compris une liste children . Le paramètre children est l’endroit où nous transmettrons la liste des éléments que nous allons animer.


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 AnimateWidget , nous transmettons les paramètres requis pour animer avec succès chaque enfant de la liste. Grâce à la méthode AnimateList.toStaggeredList , nous pouvons désormais l'implémenter sur les deux pages sur lesquelles nous travaillons.

 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 AnimateList.toStaggeredList 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 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