SKILL.md
$27
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.,
AnimationControllerwithAnimatedBuilderorAnimatedWidget).
- 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
Tweens driven by a singleAnimationControllerusingIntervalcurves).
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
durationproperty.
- (Optional) Define the
curveproperty 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(orTickerProviderStateMixinfor multiple controllers) to theStateclass.
- Initialize an
AnimationControllerininitState(), providingvsync: thisand aduration.
- Define a
Tweenand chain it to the controller using.animate().
- Wrap the target UI in an
AnimatedBuilder(preferred for complex trees) or subclassAnimatedWidget.
- Pass the
Animationobject to theAnimatedBuilder'sanimationproperty.
- Control playback using
controller.forward(),controller.reverse(), orcontroller.repeat().
- Call
controller.dispose()in thedispose()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
Herowidget.
- Assign a unique, data-driven
tagto the sourceHero.
- Wrap the destination widget in a
Herowidget.
- Assign the exact same
tagto the destinationHero.
- Ensure the widget trees inside both
Herowidgets 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.,onPanEndprovidingDragEndDetails).
- Convert the pixel velocity to the coordinate space of the animating property.
- Instantiate a
SpringSimulationwith 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()));