|
| 1 | +--- |
| 2 | +name: "flutter-animation" |
| 3 | +description: "Add animated effects to your Flutter app" |
| 4 | +metadata: |
| 5 | + model: "models/gemini-3.1-pro-preview" |
| 6 | + last_modified: "Mon, 02 Mar 2026 21:40:10 GMT" |
| 7 | + |
| 8 | +--- |
| 9 | +# Flutter Animations Implementation |
| 10 | + |
| 11 | +## Goal |
| 12 | +Implements and manages Flutter animations, selecting the appropriate animation strategy (implicit, explicit, tween, physics, hero, or staggered) based on UI requirements. Assumes a working Flutter environment, stateful/stateless widget competence, and a standard widget tree structure. |
| 13 | + |
| 14 | +## Instructions |
| 15 | + |
| 16 | +### 1. Determine Animation Strategy (Decision Logic) |
| 17 | +Evaluate the UI requirement using the following decision tree to select the correct animation approach: |
| 18 | +1. **Is the animation a simple property change (color, size, alignment) on a single widget?** |
| 19 | + * YES: Use **Implicit Animations** (e.g., `AnimatedContainer`). |
| 20 | + * NO: Proceed to 2. |
| 21 | +2. **Does the animation model real-world movement (springs, gravity, velocity)?** |
| 22 | + * YES: Use **Physics-based animation** (`SpringSimulation`, `animateWith`). |
| 23 | + * NO: Proceed to 3. |
| 24 | +3. **Does the animation involve a widget flying between two different screens/routes?** |
| 25 | + * YES: Use **Hero Animations** (`Hero` widget). |
| 26 | + * NO: Proceed to 4. |
| 27 | +4. **Does the animation involve multiple sequential or overlapping movements?** |
| 28 | + * YES: Use **Staggered Animations** (Single `AnimationController` with multiple `Tween`s and `Interval` curves). |
| 29 | + * NO: Use **Standard Explicit Animations** (`AnimationController`, `Tween`, `AnimatedBuilder` / `AnimatedWidget`). |
| 30 | + |
| 31 | +**STOP AND ASK THE USER:** If the requirement is ambiguous, pause and ask the user to clarify the desired visual effect before writing implementation code. |
| 32 | + |
| 33 | +### 2. Implement Implicit Animations |
| 34 | +For simple transitions between values, use implicit animation widgets. Do not manually manage state or controllers. |
| 35 | + |
| 36 | +```dart |
| 37 | +AnimatedContainer( |
| 38 | + duration: const Duration(milliseconds: 500), |
| 39 | + curve: Curves.bounceIn, |
| 40 | + width: _isExpanded ? 200.0 : 100.0, |
| 41 | + height: _isExpanded ? 200.0 : 100.0, |
| 42 | + decoration: BoxDecoration( |
| 43 | + color: _isExpanded ? Colors.green : Colors.blue, |
| 44 | + borderRadius: BorderRadius.circular(_isExpanded ? 50.0 : 8.0), |
| 45 | + ), |
| 46 | + child: const FlutterLogo(), |
| 47 | +) |
| 48 | +``` |
| 49 | + |
| 50 | +### 3. Implement Explicit Animations (Tween-based) |
| 51 | +When you need to control the animation (play, pause, reverse), use an `AnimationController` with a `Tween`. Separate the transition rendering from the state using `AnimatedBuilder`. |
| 52 | + |
| 53 | +```dart |
| 54 | +class _MyAnimatedWidgetState extends State<MyAnimatedWidget> with SingleTickerProviderStateMixin { |
| 55 | + late AnimationController _controller; |
| 56 | + late Animation<double> _animation; |
| 57 | +
|
| 58 | + @override |
| 59 | + void initState() { |
| 60 | + super.initState(); |
| 61 | + _controller = AnimationController( |
| 62 | + duration: const Duration(seconds: 2), |
| 63 | + vsync: this, |
| 64 | + ); |
| 65 | + |
| 66 | + _animation = Tween<double>(begin: 0, end: 300).animate( |
| 67 | + CurvedAnimation(parent: _controller, curve: Curves.easeOut), |
| 68 | + )..addStatusListener((status) { |
| 69 | + if (status == AnimationStatus.completed) { |
| 70 | + _controller.reverse(); |
| 71 | + } else if (status == AnimationStatus.dismissed) { |
| 72 | + _controller.forward(); |
| 73 | + } |
| 74 | + }); |
| 75 | + |
| 76 | + _controller.forward(); |
| 77 | + } |
| 78 | +
|
| 79 | + @override |
| 80 | + void dispose() { |
| 81 | + _controller.dispose(); // STRICT REQUIREMENT |
| 82 | + super.dispose(); |
| 83 | + } |
| 84 | +
|
| 85 | + @override |
| 86 | + Widget build(BuildContext context) { |
| 87 | + return AnimatedBuilder( |
| 88 | + animation: _animation, |
| 89 | + builder: (context, child) { |
| 90 | + return SizedBox( |
| 91 | + height: _animation.value, |
| 92 | + width: _animation.value, |
| 93 | + child: child, |
| 94 | + ); |
| 95 | + }, |
| 96 | + child: const FlutterLogo(), // Passed as child for performance |
| 97 | + ); |
| 98 | + } |
| 99 | +} |
| 100 | +``` |
| 101 | + |
| 102 | +### 4. Implement Page Route Transitions |
| 103 | +To animate transitions between routes, use `PageRouteBuilder` and chain a `CurveTween` with a `Tween<Offset>`. |
| 104 | + |
| 105 | +```dart |
| 106 | +Route<void> _createRoute() { |
| 107 | + return PageRouteBuilder( |
| 108 | + pageBuilder: (context, animation, secondaryAnimation) => const DestinationPage(), |
| 109 | + transitionsBuilder: (context, animation, secondaryAnimation, child) { |
| 110 | + const begin = Offset(0.0, 1.0); |
| 111 | + const end = Offset.zero; |
| 112 | + const curve = Curves.ease; |
| 113 | +
|
| 114 | + final tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); |
| 115 | +
|
| 116 | + return SlideTransition( |
| 117 | + position: animation.drive(tween), |
| 118 | + child: child, |
| 119 | + ); |
| 120 | + }, |
| 121 | + ); |
| 122 | +} |
| 123 | +``` |
| 124 | + |
| 125 | +### 5. Implement Physics-Based Animations |
| 126 | +For realistic motion (e.g., snapping back after a drag), calculate velocity and apply a `SpringSimulation`. |
| 127 | + |
| 128 | +```dart |
| 129 | +void _runSpringAnimation(Offset pixelsPerSecond, Size size, Alignment dragAlignment) { |
| 130 | + _animation = _controller.drive( |
| 131 | + AlignmentTween(begin: dragAlignment, end: Alignment.center), |
| 132 | + ); |
| 133 | +
|
| 134 | + final unitsPerSecondX = pixelsPerSecond.dx / size.width; |
| 135 | + final unitsPerSecondY = pixelsPerSecond.dy / size.height; |
| 136 | + final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY); |
| 137 | + final unitVelocity = unitsPerSecond.distance; |
| 138 | +
|
| 139 | + const spring = SpringDescription(mass: 1, stiffness: 1, damping: 1); |
| 140 | + final simulation = SpringSimulation(spring, 0, 1, -unitVelocity); |
| 141 | +
|
| 142 | + _controller.animateWith(simulation); |
| 143 | +} |
| 144 | +``` |
| 145 | + |
| 146 | +### 6. Implement Hero Animations (Shared Element) |
| 147 | +To fly a widget between routes, wrap the identical widget tree in both routes with a `Hero` widget using the exact same `tag`. |
| 148 | + |
| 149 | +```dart |
| 150 | +// Source Route |
| 151 | +Hero( |
| 152 | + tag: 'unique-photo-tag', |
| 153 | + child: Image.asset('photo.png', width: 100), |
| 154 | +) |
| 155 | +
|
| 156 | +// Destination Route |
| 157 | +Hero( |
| 158 | + tag: 'unique-photo-tag', |
| 159 | + child: Image.asset('photo.png', width: 300), |
| 160 | +) |
| 161 | +``` |
| 162 | + |
| 163 | +### 7. Implement Staggered Animations |
| 164 | +For sequential or overlapping animations, use a single `AnimationController` and define multiple `Tween`s with `Interval` curves. |
| 165 | + |
| 166 | +```dart |
| 167 | +class StaggerAnimation extends StatelessWidget { |
| 168 | + StaggerAnimation({super.key, required this.controller}) : |
| 169 | + opacity = Tween<double>(begin: 0.0, end: 1.0).animate( |
| 170 | + CurvedAnimation( |
| 171 | + parent: controller, |
| 172 | + curve: const Interval(0.0, 0.100, curve: Curves.ease), |
| 173 | + ), |
| 174 | + ), |
| 175 | + width = Tween<double>(begin: 50.0, end: 150.0).animate( |
| 176 | + CurvedAnimation( |
| 177 | + parent: controller, |
| 178 | + curve: const Interval(0.125, 0.250, curve: Curves.ease), |
| 179 | + ), |
| 180 | + ); |
| 181 | +
|
| 182 | + final AnimationController controller; |
| 183 | + final Animation<double> opacity; |
| 184 | + final Animation<double> width; |
| 185 | +
|
| 186 | + @override |
| 187 | + Widget build(BuildContext context) { |
| 188 | + return AnimatedBuilder( |
| 189 | + animation: controller, |
| 190 | + builder: (context, child) { |
| 191 | + return Opacity( |
| 192 | + opacity: opacity.value, |
| 193 | + child: Container(width: width.value, height: 50, color: Colors.blue), |
| 194 | + ); |
| 195 | + }, |
| 196 | + ); |
| 197 | + } |
| 198 | +} |
| 199 | +``` |
| 200 | + |
| 201 | +### 8. Validate-and-Fix Loop |
| 202 | +After generating animation code, verify the following: |
| 203 | +1. Does the `State` class use `SingleTickerProviderStateMixin` (or `TickerProviderStateMixin` for multiple controllers)? |
| 204 | +2. Is `_controller.dispose()` explicitly called in the `dispose()` method? |
| 205 | +3. If using `AnimatedBuilder`, is the static widget passed to the `child` parameter rather than rebuilt inside the `builder` function? |
| 206 | +If any of these are missing, fix the code immediately before presenting it to the user. |
| 207 | + |
| 208 | +## Constraints |
| 209 | +* **Strict Disposal:** You MUST include `dispose()` methods for all `AnimationController` instances to prevent memory leaks. |
| 210 | +* **No URLs:** Do not include external links or URLs in the output or comments. |
| 211 | +* **Immutability:** Treat `Tween` and `Curve` classes as stateless and immutable. Do not attempt to mutate them after instantiation. |
| 212 | +* **Performance:** Always use `AnimatedBuilder` or `AnimatedWidget` instead of calling `setState()` inside a controller's `addListener` when building complex widget trees. |
| 213 | +* **Hero Tags:** Hero tags MUST be identical and unique per route transition. Do not use generic tags like `'image'` if multiple heroes exist. |
0 commit comments