Implementing Flutter Animations
Contents
-
Core Concepts
-
Animation Strategies
-
Workflows
-
Implementing Implicit Animations
-
Implementing Explicit Animations
-
Implementing Hero Transitions
-
Implementing Physics-Based Animations
-
Examples
Core Concepts
Manage Flutter animations using the core typed Animation system. Do not manually calculate frames; rely on the framework's ticker and interpolation classes.
-
Animation<T> : Treat this as an abstract representation of a value that changes over time. It holds state (completed, dismissed) and notifies listeners, but knows nothing about the UI.
-
AnimationController : Instantiate this to drive the animation. It generates values (typically 0.0 to 1.0) tied to the screen refresh rate. Always provide a vsync (usually via SingleTickerProviderStateMixin ) to prevent offscreen resource consumption. Always dispose() controllers to prevent memory leaks.
-
Tween<T> : Define a stateless mapping from an input range (usually 0.0-1.0) to an output type (e.g., Color , Offset , double ). Chain tweens with curves using .animate() .
-
Curve : Apply non-linear timing (e.g., Curves.easeIn , Curves.bounceOut ) to an animation using a CurvedAnimation or CurveTween .
Animation Strategies
Apply conditional logic to select the correct animation approach:
-
If animating simple property changes (size, color, opacity) without playback control: Use Implicit Animations (e.g., AnimatedContainer , AnimatedOpacity , TweenAnimationBuilder ).
-
If requiring playback control (play, pause, reverse, loop) or coordinating multiple properties: Use Explicit Animations (e.g., AnimationController with AnimatedBuilder or AnimatedWidget ).
-
If animating elements between two distinct routes: Use Hero Animations (Shared Element Transitions).
-
If modeling real-world motion (e.g., snapping back after a drag): Use Physics-Based Animations (e.g., SpringSimulation ).
-
If animating a sequence of overlapping or delayed motions: Use Staggered Animations (multiple Tween s driven by a single AnimationController using Interval curves).
Workflows
Implementing Implicit Animations
Use this workflow for "fire-and-forget" state-driven animations.
-
Task Progress:
-
Identify the target properties to animate (e.g., width, color).
-
Replace the static widget (e.g., Container ) with its animated counterpart (e.g., AnimatedContainer ).
-
Define the duration property.
-
(Optional) Define the curve property for non-linear motion.
-
Trigger the animation by updating the properties inside a setState() call.
-
Run validator -> review UI for jank -> adjust duration/curve if necessary.
Implementing Explicit Animations
Use this workflow when you need granular control over the animation lifecycle.
-
Task Progress:
-
Add SingleTickerProviderStateMixin (or TickerProviderStateMixin for multiple controllers) to the State class.
-
Initialize an AnimationController in initState() , providing vsync: this and a duration .
-
Define a Tween and chain it to the controller using .animate() .
-
Wrap the target UI in an AnimatedBuilder (preferred for complex trees) or subclass AnimatedWidget .
-
Pass the Animation object to the AnimatedBuilder 's animation property.
-
Control playback using controller.forward() , controller.reverse() , or controller.repeat() .
-
Call controller.dispose() in the dispose() method.
-
Run validator -> check for memory leaks -> ensure dispose() is called.
Implementing Hero Transitions
Use this workflow to fly a widget between two routes.
-
Task Progress:
-
Wrap the source widget in a Hero widget.
-
Assign a unique, data-driven tag to the source Hero .
-
Wrap the destination widget in a Hero widget.
-
Assign the exact same tag to the destination Hero .
-
Ensure the widget trees inside both Hero widgets are visually similar to prevent jarring jumps.
-
Trigger the transition by pushing the destination route via Navigator .
Implementing Physics-Based Animations
Use this workflow for gesture-driven, natural motion.
-
Task Progress:
-
Set up an AnimationController (do not set a fixed duration).
-
Capture gesture velocity using a GestureDetector (e.g., onPanEnd providing DragEndDetails ).
-
Convert the pixel velocity to the coordinate space of the animating property.
-
Instantiate a SpringSimulation with mass, stiffness, damping, and the calculated velocity.
-
Drive the controller using controller.animateWith(simulation) .
Examples
class StaggeredAnimationDemo extends StatefulWidget { @override State<StaggeredAnimationDemo> createState() => _StaggeredAnimationDemoState(); }
class _StaggeredAnimationDemoState extends State<StaggeredAnimationDemo> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<double> _widthAnimation; late Animation<Color?> _colorAnimation;
@override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 2), vsync: this, );
// Staggered width animation (0.0 to 0.5 interval)
_widthAnimation = Tween<double>(begin: 50.0, end: 200.0).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
),
);
// Staggered color animation (0.5 to 1.0 interval)
_colorAnimation = ColorTween(begin: Colors.blue, end: Colors.red).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.5, 1.0, curve: Curves.easeOut),
),
);
_controller.forward();
}
@override void dispose() { _controller.dispose(); // CRITICAL: Prevent memory leaks super.dispose(); }
@override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (context, child) { return Container( width: _widthAnimation.value, height: 50.0, color: _colorAnimation.value, ); }, ); } }
Route createCustomRoute(Widget destination) { return PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => destination, transitionsBuilder: (context, animation, secondaryAnimation, child) { const begin = Offset(0.0, 1.0); // Start from bottom const end = Offset.zero; const curve = Curves.easeOut;
final tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
final offsetAnimation = animation.drive(tween);
return SlideTransition(
position: offsetAnimation,
child: child,
);
},
); }
// Usage: Navigator.of(context).push(createCustomRoute(const NextPage()));