vgv-animations

Par verygoodopensource · vgv-ai-flutter-plugin

Bonnes pratiques pour les animations Flutter avec le framework d'animation intégré. À utiliser lors de la création, de la modification ou de la révision d'animations, de transitions, de mouvements ou de widgets animés. Couvre les animations implicites, les animations explicites, les transitions de page et les tokens de mouvement Material 3.

npx skills add https://github.com/verygoodopensource/vgv-ai-flutter-plugin --skill vgv-animations

Animations

Meilleures pratiques pour les animations Flutter en utilisant le framework d'animation intégré et les directives de mouvement Material 3. Aucune bibliothèque d'animation tierce (Lottie, Rive, etc.).

Standards Fondamentaux

Appliquez ces standards à TOUS les travaux d'animation :

  • Clarifiez l'intention visuelle quand la demande est ambiguë — quand le développeur dit « ajoute une animation » ou « rends-le plus fluide » sans spécifier la propriété, le déclencheur, la durée ou la courbe, demandez confirmation avant d'écrire du code. Si le développeur fournit des spécifications claires (par ex. « fondu de 300 ms ease-in sur la carte à son apparition »), procédez directement
  • Utilisez l'approche d'animation la plus simple qui fonctionne — suivez l'arbre de décision ci-dessous ; n'utilisez jamais AnimationController quand une animation implicite suffira
  • Utilisez les jetons de mouvement Material 3 pour la durée et l'easing — ne codez jamais en dur des valeurs Duration ou Curve arbitraires
  • Extractez les constantes d'animation — les durées, courbes et décalages vont dans des constantes nommées ou une classe AppMotion centralisée, pas en ligne
  • Disposez les contrôleurs — chaque AnimationController doit être disposé dans la méthode dispose() du State
  • Utilisez SingleTickerProviderStateMixin pour un contrôleur — utilisez TickerProviderStateMixin seulement si le widget possède plusieurs contrôleurs
  • Gardez les sous-arbres animés petits — enrobez uniquement les widgets qui changent dans le constructeur d'animation, pas des arbres de widgets entiers
  • N'animez jamais les propriétés déclenchant la mise en page dans une boucle serrée — animer width/height sur les mises en page complexes cause des reconstructions coûteuses ; préférez Transform ou Opacity qui opèrent sur la couche de composition

Arbre de Décision pour les Animations

Choisissez l'approche la plus simple qui répond au besoin :

Le widget se reconstruit-il quand la valeur change ?
  |
  OUI --> Le framework fournit-il un widget AnimatedFoo ?
  |         |
  |         OUI --> Utilisez le widget implicite AnimatedFoo
  |         |       (AnimatedContainer, AnimatedOpacity, AnimatedAlign, etc.)
  |         |
  |         NON --> Utilisez TweenAnimationBuilder
  |
  NON --> Avez-vous besoin d'un contrôle fin ?
            (répéter, inverser, séquencer, écouter le statut)
            |
            OUI --> Utilisez AnimationController + AnimatedBuilder
            |
            NON --> Utilisez TweenAnimationBuilder

Règle générale : si l'animation est « définir une cible et la laisser s'animer jusqu'à là », utilisez l'implicite. Si l'animation doit jouer/mettre en pause/inverser/répéter sur commande, utilisez l'explicite.


Jetons de Mouvement Material 3

Utilisez les classes Durations et Easing intégrées de Flutter — ne codez jamais en dur Duration(milliseconds: ...) ou utilisez Curves.* pour le nouveau code. Les constantes du framework s'alignent sur la spécification de mouvement Material 3 ; consultez la documentation des classes Durations et Easing de Flutter pour la liste complète des jetons.

Constantes de Mouvement Centralisées

Introduisez une classe AppMotion quand le projet utilise des animations sur plusieurs fonctionnalités. Pour une seule animation dans l'app, les jetons M3 en ligne suffisent.

abstract class AppMotion {
  // Transitions standard
  static const Duration standardDuration = Durations.medium2;
  static const Curve standardCurve = Easing.standard;

  // Transitions de page
  static const Duration pageDuration = Durations.medium4;
  static const Curve pageEnterCurve = Easing.emphasizedDecelerate;
  static const Curve pageExitCurve = Easing.emphasizedAccelerate;

  // Fondus
  static const Duration fadeDuration = Durations.short3;
  static const Curve fadeCurve = Easing.standard;
}

Animations Implicites

Utilisez les animations implicites quand le widget se reconstruit avec de nouvelles valeurs cibles. Le framework interpole automatiquement. Flutter fournit des widgets AnimatedFoo intégrés (AnimatedContainer, AnimatedOpacity, AnimatedSlide, AnimatedSwitcher, etc.) — utilisez celui qui correspond à la propriété animée. Quand aucun widget intégré n'existe, utilisez TweenAnimationBuilder.


TweenAnimationBuilder

Utilisez TweenAnimationBuilder quand aucun widget AnimatedFoo intégré n'existe pour votre propriété, mais que vous voulez toujours une animation de style implicite « définir et oublier ».

TweenAnimationBuilder<double>(
  tween: Tween(begin: 0, end: isActive ? 1.0 : 0.0),
  duration: Durations.medium2,
  curve: Easing.standard,
  builder: (context, value, child) {
    return Transform.scale(
      scale: 0.8 + (0.2 * value),
      child: Opacity(
        opacity: value,
        child: child,
      ),
    );
  },
  child: child, // child n'est pas reconstruit — optimisation
)

Le paramètre child est critique : passez les widgets qui ne dépendent pas de la valeur animée pour éviter les reconstructions inutiles.


Animations Explicites

Utilisez les animations explicites quand vous avez besoin de contrôler la lecture : jouer, mettre en pause, inverser, répéter ou écouter le statut d'animation.

Configuration de AnimationController

class _MyWidgetState extends State<MyWidget>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> _fadeAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Durations.medium2,
      vsync: this,
    );
    _fadeAnimation = CurvedAnimation(
      parent: _controller,
      curve: Easing.standard,
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _fadeAnimation,
      builder: (context, child) {
        return Opacity(
          opacity: _fadeAnimation.value,
          child: child,
        );
      },
      child: child, // enfant statique — pas reconstruit chaque frame
    );
  }
}

Voir references/explicit-animations.md pour les motifs didUpdateWidget, l'injection de contrôleur pour des tests faciles, et les directives des widgets de transition vs AnimatedBuilder.

Animations Chaînées avec Intervalles

Utilisez Interval dans CurvedAnimation pour séquencer des animations sur un seul contrôleur :

late final Animation<double> _fadeAnimation = CurvedAnimation(
  parent: _controller,
  curve: const Interval(0.0, 0.5, curve: Easing.standard),
);

late final Animation<Offset> _slideAnimation = Tween<Offset>(
  begin: const Offset(0, 0.25),
  end: Offset.zero,
).animate(
  CurvedAnimation(
    parent: _controller,
    curve: const Interval(0.2, 0.8, curve: Easing.emphasized),
  ),
);

Voir references/staggered-animations.md pour des exemples complets d'animations d'entrée décalées et d'éléments de liste décalés. Voir references/looping-animations.md pour les motifs d'animations répétées et de pulsation.


Transitions de Page

Les transitions de page personnalisées s'intègrent avec GoRouter via CustomTransitionPage dans GoRouteData.buildPage.

@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
  return CustomTransitionPage(
    key: state.pageKey,
    child: const DetailsPage(),
    transitionDuration: Durations.medium4,
    reverseTransitionDuration: Durations.medium4,
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return FadeTransition(
        opacity: CurvedAnimation(
          parent: animation,
          curve: Easing.emphasizedDecelerate,
        ),
        child: child,
      );
    },
  );
}

Voir references/page-transitions.md pour une classe d'aide AppPageTransitions réutilisable avec des transitions fondu, fondu-glissement et glissement-montée, et utilisation avec GoRouteData.

Animations Hero

Utilisez Hero pour les transitions d'éléments partagés entre les routes. Le framework gère l'animation automatiquement.

// Écran source
Hero(
  tag: 'product-image-${product.id}',
  child: Image.network(product.imageUrl),
)

// Écran destination
Hero(
  tag: 'product-image-${product.id}',
  child: Image.network(product.imageUrl),
)

Règles pour Hero :

  • Les étiquettes doivent être uniques dans chaque route — utilisez des identifiants significatifs, pas des indices
  • La source et la destination doivent être visibles pendant la transition — Hero ne fonctionne pas avec les listes lazy qui suppriment le widget source
  • Enrobez uniquement l'élément visuel — pas la carte ou tuile entière

Performance

À Faire

  • Animez Transform et Opacity — ceux-ci opèrent sur la couche de composition et ignorent la mise en page/peinture
  • Utilisez le paramètre child dans AnimatedBuilder et TweenAnimationBuilder pour éviter de reconstruire les widgets statiques chaque frame
  • Utilisez RepaintBoundary autour des widgets animés dans les mises en page complexes pour isoler les repeints

À Éviter

  • N'animez pas width, height ou padding sur les mises en page complexes — déclenche des recalculs de mise en page coûteux chaque frame
  • N'enrobez pas des écrans entiers dans AnimatedBuilder — enrobez uniquement le sous-arbre qui change
  • Ne créez pas plusieurs instances de AnimationController pour des animations partageant la même temporisation — utilisez Interval sur un seul contrôleur

Anti-Motifs

Valeurs magiques codées en dur

// Mauvais — valeurs arbitraires sans sens sémantique
AnimatedContainer(
  duration: Duration(milliseconds: 375),
  curve: Curves.easeInOutCubic,
  // ...
)

// Bon — jetons M3 avec intention claire
AnimatedContainer(
  duration: Durations.medium2,
  curve: Easing.standard,
  // ...
)

Disposition de contrôleur manquante

// Mauvais — fuite mémoire
@override
void dispose() {
  super.dispose();
}

// Bon — disposez avant super.dispose()
@override
void dispose() {
  _controller.dispose();
  super.dispose();
}

Reconstruction des enfants statiques chaque frame

// Mauvais — sous-arbre entier reconstruit 60 fois/seconde
AnimatedBuilder(
  animation: _controller,
  builder: (context, child) {
    return Opacity(
      opacity: _controller.value,
      child: const ExpensiveWidget(), // reconstruit chaque frame
    );
  },
)

// Bon — enfant statique passé en paramètre
AnimatedBuilder(
  animation: _controller,
  builder: (context, child) {
    return Opacity(
      opacity: _controller.value,
      child: child,
    );
  },
  child: const ExpensiveWidget(), // construit une fois
)

Utiliser l'explicite quand l'implicite suffit

// Mauvais — complexité inutile pour une animation simple de valeur cible
class _FadeWidgetState extends State<FadeWidget>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  // ... 20+ lignes de boilerplate

// Bon — un widget, zéro boilerplate
AnimatedOpacity(
  duration: Durations.short3,
  curve: Easing.standard,
  opacity: isVisible ? 1.0 : 0.0,
  child: child,
)

Référence Rapide

Approche Quand l'utiliser
AnimatedFoo Un widget intégré existe pour la propriété
TweenAnimationBuilder Propriété personnalisée, aucun contrôle de lecture nécessaire
AnimationController Besoin de jouer/mettre en pause/inverser/répéter/statut
Hero Transition d'élément partagé entre routes
CustomTransitionPage Transition de page GoRouter personnalisée
Mixin Quand l'utiliser
SingleTickerProviderStateMixin Le widget possède exactement un contrôleur
TickerProviderStateMixin Le widget possède plusieurs contrôleurs

Ressources Supplémentaires

Skills similaires