Kodokon kodokon.com

Animations : implicites et explicites sous le capot

Comprenez la mécanique interne des animations implicites et pilotez finement vos propres AnimationController.

10 min · 3 questions

Ouvrir cette leçon dans Kodokon

AnimatedContainer appartient à la famille des animations implicites : vous changez une valeur cible, le framework anime la transition. Sous le capot, chaque ImplicitlyAnimatedWidget possède un AnimationController caché dans son state. Quand une nouvelle configuration arrive, didUpdateWidget parcourt les propriétés via forEachTween et re-cible chaque tween : la borne begin devient la valeur interpolée courante, la borne end la nouvelle cible, puis le contrôleur repart de zéro. Conséquence précieuse : changer la cible en pleine animation ne provoque aucun saut visuel - l'animation repart exactement de là où elle était.

DART
import 'package:flutter/material.dart';

class SelectableCard extends StatefulWidget {
  const SelectableCard({super.key});

  @override
  State<SelectableCard> createState() =>
      _SelectableCardState();
}

class _SelectableCardState extends State<SelectableCard> {
  bool _selected = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () =>
          setState(() => _selected = !_selected),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeOutCubic,
        width: _selected ? 220 : 160,
        height: 96,
        decoration: BoxDecoration(
          color: _selected
              ? Colors.indigo
              : Colors.indigo.shade200,
          borderRadius: BorderRadius.circular(
            _selected ? 28 : 12,
          ),
        ),
      ),
    );
  }
}
Taille, couleur et rayon animés d'un seul setState.

Les implicites couvrent les propriétés prévues par le framework, et TweenAnimationBuilder généralise le procédé à n'importe quelle valeur interpolable. Dès que vous devez piloter l'animation - pause, inversion, répétition, réaction au statut - passez à l'AnimationController explicite. Le paramètre vsync attend un TickerProvider : le mixin crée un Ticker, un callback invoqué à chaque frame par le SchedulerBinding. Subtilité méconnue : TickerMode met en sourdine les tickers d'un sous-arbre masqué (par exemple sous une route recouverte). Le temps continue de s'écouler, mais plus aucun callback ne tourne - zéro CPU gaspillé pour des animations invisibles.

DART
import 'package:flutter/material.dart';

class RevealPanel extends StatefulWidget {
  const RevealPanel({super.key, required this.child});

  final Widget child;

  @override
  State<RevealPanel> createState() => _RevealPanelState();
}

class _RevealPanelState extends State<RevealPanel>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller =
      AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 400),
    reverseDuration: const Duration(milliseconds: 200),
  );

  late final Animation<double> _opacity = _controller
      .drive(CurveTween(curve: Curves.easeOutCubic));

  void toggle() {
    switch (_controller.status) {
      case AnimationStatus.forward:
      case AnimationStatus.completed:
        _controller.reverse();
      case AnimationStatus.reverse:
      case AnimationStatus.dismissed:
        _controller.forward();
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: toggle,
      child: FadeTransition(
        opacity: _opacity,
        child: widget.child,
      ),
    );
  }
}
reverseDuration : l'aller et le retour ont chacun leur tempo.

Un contrôleur n'est qu'une Animation<double> allant de 0 à 1 : composez-la au lieu de la consommer brute. controller.drive(CurveTween(...)) produit une animation dérivée sans état supplémentaire. Pour appliquer la valeur à l'arbre, préférez les widgets Transition (FadeTransition, RotationTransition) : ils écoutent l'animation directement au niveau du render object, sans reconstruire un seul widget par frame. Quand la transformation est arbitraire, AnimatedBuilder fait l'affaire - à condition de passer le sous-arbre statique dans child : il est construit une seule fois puis réinjecté dans builder à chaque frame. C'est l'optimisation la plus rentable des animations explicites.

DART
import 'dart:math' as math;

import 'package:flutter/material.dart';

class Orbit extends StatefulWidget {
  const Orbit({super.key});

  @override
  State<Orbit> createState() => _OrbitState();
}

class _OrbitState extends State<Orbit>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller =
      AnimationController(
    vsync: this,
    duration: const Duration(seconds: 4),
  )..repeat();

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      child: const Icon(Icons.satellite_alt, size: 32),
      builder: (context, child) {
        final angle = _controller.value * 2 * math.pi;
        return Transform.translate(
          offset: Offset(
            math.cos(angle) * 80,
            math.sin(angle) * 80,
          ),
          child: child,
        );
      },
    );
  }
}
L'icône passée en child n'est construite qu'une seule fois.

Quiz de validation

Vérifiez que vous avez bien retenu les points clés de cette leçon.

  1. Un AnimatedContainer reçoit une nouvelle couleur cible alors que l'animation précédente est à mi-course. Que se passe-t-il ?
    • Le tween est re-ciblé : il repart de la valeur courante vers la nouvelle cible, sans saut visuel
    • L'animation en cours se termine d'abord, puis la nouvelle démarre
    • Le widget saute immédiatement à la nouvelle couleur
  2. À quoi sert exactement le paramètre vsync d'un AnimationController ?
    • Il fournit un Ticker synchronisé sur les frames, que TickerMode peut mettre en sourdine quand le sous-arbre est masqué
    • Il synchronise le contrôleur avec les autres animations de l'app
    • Il limite l'animation à 30 images par seconde
    • Il active l'accélération matérielle du GPU
  3. Pourquoi passer le sous-arbre statique via le paramètre child d'AnimatedBuilder ?
    • Il est construit une seule fois puis réinjecté dans builder, au lieu d'être reconstruit à chaque frame
    • C'est obligatoire : builder ne peut pas retourner de nouveaux widgets
    • Cela libère automatiquement le contrôleur à la fin