Explicit Animations Reference
Explicit animations provide full control using AnimationController, Tween, and animation status listeners.
Core Components
AnimationController
The heart of explicit animations. Manages animation lifecycle, value, and timing.
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this, // Required TickerProvider
lowerBound: 0.0, // Optional: default 0.0
upperBound: 1.0, // Optional: default 1.0
);
}
@override
void dispose() {
_controller.dispose(); // ALWAYS dispose!
super.dispose();
}Lifecycle methods:
// Start animation forward
_controller.forward();
// Start animation backward
_controller.reverse();
// Stop animation
_controller.stop();
// Repeat animation
_controller.repeat();
// Animate to specific value
await _controller.animateTo(0.5);
// Reset to beginning
_controller.reset();
// Animate with physics simulation
_controller.fling(velocity: 2.0);
// Animate with custom simulation
_controller.animateWith(SpringSimulation(...));Listening to changes:
_controller.addListener(() {
// Value changed - rebuild needed
setState(() {});
});
_controller.addStatusListener((status) {
// Status changed
switch (status) {
case AnimationStatus.dismissed:
print('Animation dismissed (at 0)');
break;
case AnimationStatus.forward:
print('Animation moving forward');
break;
case AnimationStatus.reverse:
print('Animation moving backward');
break;
case AnimationStatus.completed:
print('Animation completed (at 1)');
break;
}
});Tween
Interpolates between begin and end values.
// Simple tween
animation = Tween<double>(begin: 0, end: 300).animate(_controller);
// Color tween
animation = ColorTween(
begin: Colors.red,
end: Colors.blue,
).animate(_controller);
// Border radius tween
animation = BorderRadiusTween(
begin: BorderRadius.circular(4),
end: BorderRadius.circular(75),
).animate(_controller);
// Rect tween
animation = RectTween(
begin: Rect.fromLTWH(0, 0, 100, 100),
end: Rect.fromLTWH(100, 100, 200, 200),
).animate(_controller);Common Tweens:
Tween<T>- Generic tween (use for most types)ColorTween- Color interpolationRectTween- Rectangle interpolationIntTween- Integer interpolationSizeTween- Size interpolationOffsetTween- Offset interpolationBorderRadiusTween- Border radius interpolation
CurvedAnimation
Applies a curve to transform animation values.
animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
reverseCurve: Curves.easeIn, // Optional: different curve for reverse
);With Interval:
animation = CurvedAnimation(
parent: _controller,
curve: const Interval(
0.25, // Start at 25% of controller
0.75, // End at 75% of controller
curve: Curves.easeInOut,
),
);ReverseAnimation
Reverses the parent animation.
final forwardAnimation = Tween<double>(begin: 0, end: 1).animate(_controller);
final reverseAnimation = ReverseAnimation(forwardAnimation);Patterns
AnimatedWidget Pattern
Best for reusable animated widgets. Automatically handles rebuilds.
class AnimatedLogo extends AnimatedWidget {
const AnimatedLogo({super.key, required Animation<double> animation})
: super(listenable: animation);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Container(
height: animation.value,
width: animation.value,
child: const FlutterLogo(),
),
);
}
}
// Usage
@override
Widget build(BuildContext context) {
return AnimatedLogo(animation: _animation);
}AnimatedBuilder Pattern
Best for complex widgets. Separates animation logic from widget logic.
class GrowTransition extends StatelessWidget {
const GrowTransition({
required this.child,
required this.animation,
super.key,
});
final Widget child;
final Animation<double> animation;
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: animation,
builder: (context, child) {
return SizedBox(
height: animation.value,
width: animation.value,
child: child,
);
},
child: child,
),
);
}
}
// Usage
@override
Widget build(BuildContext context) {
return GrowTransition(
animation: _animation,
child: const LogoWidget(),
);
}Why pass child twice? The child parameter to AnimatedBuilder is passed to the builder function, allowing the child to be built once and reused, rather than rebuilt every animation frame.
Multiple Animations Pattern
Animate multiple properties from single controller.
class MultiPropertyAnimation extends AnimatedWidget {
const MultiPropertyAnimation({super.key, required Animation<double> animation})
: super(listenable: animation);
static final _opacityTween = Tween<double>(begin: 0.1, end: 1);
static final _sizeTween = Tween<double>(begin: 0, end: 300);
static final _colorTween = ColorTween(
begin: Colors.red,
end: Colors.blue,
);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Opacity(
opacity: _opacityTween.evaluate(animation),
child: Container(
height: _sizeTween.evaluate(animation),
width: _sizeTween.evaluate(animation),
decoration: BoxDecoration(
color: _colorTween.evaluate(animation),
borderRadius: BorderRadius.circular(12),
),
child: const FlutterLogo(),
),
),
);
}
}Built-in Transitions
Flutter provides pre-built transitions for common use cases.
FadeTransition
FadeTransition(
opacity: _animation,
child: const FlutterLogo(),
)ScaleTransition
ScaleTransition(
scale: _animation,
child: const FlutterLogo(),
)SlideTransition
SlideTransition(
position: Tween<Offset>(
begin: Offset(0, 1),
end: Offset.zero,
).animate(_animation),
child: const FlutterLogo(),
)SizeTransition
SizeTransition(
sizeFactor: _animation,
axis: Axis.vertical,
child: const FlutterLogo(),
)RotationTransition
RotationTransition(
turns: _animation,
child: const FlutterLogo(),
)DecoratedBoxTransition
DecoratedBoxTransition(
decoration: DecorationTween(
begin: BoxDecoration(color: Colors.red),
end: BoxDecoration(color: Colors.blue),
).animate(_animation),
child: const FlutterLogo(),
)PositionedTransition
Stack(
children: [
PositionedTransition(
rect: RelativeRectTween(
begin: RelativeRect.fill(calculateRect(context, startPosition)),
end: RelativeRect.fill(calculateRect(context, endPosition)),
).animate(_animation),
child: const FlutterLogo(),
),
],
)Animation Status Handling
Basic Status Loop
_animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});One-way Animation
_animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.stop(); // Or navigate, show dialog, etc.
}
});Reset and Repeat
_animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reset();
_controller.forward();
}
});Performance Best Practices
DO
- Always dispose AnimationController
- Use
AnimatedWidgetorAnimatedBuilderinstead ofsetState()in listeners - Minimize widget rebuilds during animation
- Use static Tweens when possible (avoid recreating)
- Profile with Flutter DevTools
- Use
timeDilationfor debugging
DON'T
- Forget to dispose controllers (memory leak)
- Call
setState()in animation listeners unnecessarily - Create complex widget trees inside animation builds
- Animate too many widgets simultaneously
- Use animation values directly in expensive operations
Performance Pattern: RepaintBoundary
RepaintBoundary(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return ExpensiveWidget();
},
),
)Debugging
Slow Animations
void main() {
timeDilation = 10.0; // 10x slower
runApp(MyApp());
}Print Animation Values
_controller.addListener(() {
print('Animation value: ${_controller.value}');
});
_animation.addStatusListener((status) {
print('Animation status: $status');
});Flutter DevTools Performance Overlay
- Run app in debug mode
- Press 'p' to toggle performance overlay
- Look for "GPU UI" and "GPU Raster" graphs
- Aim for 60fps (16.67ms per frame)
Common Patterns
Fade In Then Slide Up
_animation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
),
);
_slideAnimation = Tween<Offset>(
begin: Offset(0, 0.5),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.3, 1.0, curve: Curves.easeOut),
),
);Bounce Effect
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.elasticOut,
);Shake Effect
class ShakeCurve extends Curve {
@override
double transform(double t) {
return sin(t * pi * 2);
}
}
_animation = CurvedAnimation(
parent: _controller,
curve: ShakeCurve(),
);Parallax Scrolling
class ParallaxWidget extends StatelessWidget {
const ParallaxWidget({
required this.animation,
required this.child,
super.key,
});
final Animation<double> animation;
final Widget child;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Transform.translate(
offset: Offset(0, animation.value * 50),
child: child,
);
},
child: child,
);
}
}Comparison: Implicit vs Explicit
| Feature | Implicit | Explicit |
|---|---|---|
| Controller | None needed | AnimationController required |
| Setup | Simple | More complex |
| Control | Limited (duration, curve) | Full (lifecycle, status, physics) |
| Multiple Properties | AnimatedContainer only | Unlimited |
| Performance | Good | Excellent (with patterns) |
| Reusability | Widget-based | Component-based |
| State Monitoring | Limited (onEnd) | Full (status listeners) |
When to Use Explicit Animations
Use explicit animations when:
- Need full control over animation lifecycle
- Animating multiple properties
- Need to react to animation state changes
- Creating reusable animation components
- Need complex timing or sequencing
- Performance is critical
- Want physics-based or custom animations
Use implicit animations when:
- Simple, one-off animations
- Animation triggered by state change
- No need for fine-grained control
- Want simple, declarative code