--- name: "flutter-animating-apps" description: "Implements animated effects, transitions, and motion in a Flutter app. Use when adding visual feedback, shared element transitions, or physics-based animations." metadata: model: "models/gemini-3.1-pro-preview" last_modified: "Thu, 12 Mar 2026 22:16:34 GMT" --- # Implementing Flutter Animations ## Contents - [Core Concepts](#core-concepts) - [Animation Strategies](#animation-strategies) - [Workflows](#workflows) - [Implementing Implicit Animations](#implementing-implicit-animations) - [Implementing Explicit Animations](#implementing-explicit-animations) - [Implementing Hero Transitions](#implementing-hero-transitions) - [Implementing Physics-Based Animations](#implementing-physics-based-animations) - [Examples](#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`**: 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`**: 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
Example: Explicit Animation (Staggered with AnimatedBuilder) ```dart class StaggeredAnimationDemo extends StatefulWidget { @override State createState() => _StaggeredAnimationDemoState(); } class _StaggeredAnimationDemoState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _widthAnimation; late Animation _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(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, ); }, ); } } ```
Example: Custom Page Route Transition ```dart 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())); ```