Comprenez la mécanique interne des animations implicites et pilotez finement vos propres AnimationController.
Ouvrir cette leçon dans KodokonAnimatedContainer 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.
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,
),
),
),
);
}
}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.
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,
),
);
}
}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.
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,
);
},
);
}
}