
Animations are what separate good apps from great apps. They provide visual feedback, guide user attention, and create delightful experiences that users remember. Flutter's animation system is incredibly powerful, but mastering it requires understanding the deeper concepts.
This guide will take you beyond basic animations into advanced territory. You'll learn to create custom physics simulations, complex transitions, and animations that feel natural and engaging. These are the techniques used by top-tier apps to create memorable experiences.
CUSTOM ANIMATION CONTROLLERS
Animation controllers are the heart of Flutter animations. Understanding how to create and orchestrate multiple controllers gives you unlimited creative possibilities.
class AdvancedAnimationWidget extends StatefulWidget { @override _AdvancedAnimationWidgetState createState() => _AdvancedAnimationWidgetState(); } class _AdvancedAnimationWidgetState extends State<AdvancedAnimationWidget> with TickerProviderStateMixin { late AnimationController _slideController; late AnimationController _fadeController; late AnimationController _scaleController; late Animation<Offset> _slideAnimation; late Animation<double> _fadeAnimation; late Animation<double> _scaleAnimation; @override void initState() { super.initState(); // Create multiple controllers with different durations _slideController = AnimationController( duration: const Duration(milliseconds: 800), vsync: this, ); _fadeController = AnimationController( duration: const Duration(milliseconds: 600), vsync: this, ); _scaleController = AnimationController( duration: const Duration(milliseconds: 1000), vsync: this, ); // Create curved animations _slideAnimation = Tween<Offset>( begin: const Offset(0, 1), end: Offset.zero, ).animate(CurvedAnimation( parent: _slideController, curve: Curves.elasticOut, )); _fadeAnimation = Tween<double>( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _fadeController, curve: Curves.easeInOut, )); _scaleAnimation = Tween<double>( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _scaleController, curve: Curves.bounceOut, )); } void _startSequentialAnimation() async { // Start animations in sequence with delays _slideController.forward(); await Future.delayed(const Duration(milliseconds: 200)); _fadeController.forward(); await Future.delayed(const Duration(milliseconds: 200)); _scaleController.forward(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: Listenable.merge([_slideController, _fadeController, _scaleController]), builder: (context, child) { return SlideTransition( position: _slideAnimation, child: FadeTransition( opacity: _fadeAnimation, child: ScaleTransition( scale: _scaleAnimation, child: Container( width: 200, height: 200, decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(20), ), child: const Center( child: Text('Animated!', style: TextStyle(color: Colors.white, fontSize: 24)), ), ), ), ), ); }, ); } }
🎯 Key Concepts:
- • Use TickerProviderStateMixin for multiple controllers
- • Chain animations with delayed execution
- • Combine different animation types for complex effects
- • Always dispose controllers to prevent memory leaks
PHYSICS-BASED ANIMATIONS
Physics simulations create the most natural-feeling animations. Flutter provides springs, gravity, and friction simulations that make your UI feel responsive and alive.
class SpringAnimationWidget extends StatefulWidget { @override _SpringAnimationWidgetState createState() => _SpringAnimationWidgetState(); } class _SpringAnimationWidgetState extends State<SpringAnimationWidget> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<double> _animation; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 2), vsync: this, ); // Create a spring simulation final spring = SpringDescription( mass: 1.0, // Mass of the object stiffness: 500.0, // Spring stiffness damping: 20.0, // Damping coefficient ); final springSimulation = SpringSimulation(spring, 0.0, 1.0, 0.0); _animation = _controller.drive( CurveTween(curve: Curve.from(springSimulation)), ); } void _startSpringAnimation() { _controller.reset(); _controller.forward(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _animation, builder: (context, child) { return Transform.translate( offset: Offset(0, -100 * _animation.value), child: Container( width: 100, height: 100, decoration: const BoxDecoration( color: Colors.red, shape: BoxShape.circle, ), ), ); }, ); } } // Custom physics simulation class BouncingBallSimulation extends Simulation { final double gravity; final double bounciness; final double initialVelocity; BouncingBallSimulation({ required this.gravity, required this.bounciness, required this.initialVelocity, }); @override double x(double time) { // Calculate position based on physics return initialVelocity * time + 0.5 * gravity * time * time; } @override double dx(double time) { // Calculate velocity return initialVelocity + gravity * time; } @override bool isDone(double time) { return false; // Continue indefinitely } }
⚡ Physics Tips:
- • Lower damping = more bouncy, higher damping = more controlled
- • Adjust mass to change how "heavy" animations feel
- • Use gravity simulation for realistic falling effects
- • Combine multiple physics for complex behaviors
HERO ANIMATIONS & TRANSITIONS
Hero animations create seamless transitions between screens. Master custom hero animations to create unique navigation experiences that guide users through your app flow.
// Custom Hero Animation class CustomHeroAnimation extends StatelessWidget { final String heroTag; final Widget child; final Duration duration; const CustomHeroAnimation({ required this.heroTag, required this.child, this.duration = const Duration(milliseconds: 500), }); @override Widget build(BuildContext context) { return Hero( tag: heroTag, flightShuttleBuilder: ( BuildContext flightContext, Animation<double> animation, HeroFlightDirection flightDirection, BuildContext fromHeroContext, BuildContext toHeroContext, ) { return AnimatedBuilder( animation: animation, builder: (context, child) { return Transform.scale( scale: 1.0 + (animation.value * 0.2), child: Transform.rotate( angle: animation.value * 2 * math.pi, child: Material( type: MaterialType.transparency, child: toHeroContext.widget, ), ), ); }, ); }, child: child, ); } } // Advanced Page Transition class SlideUpPageRoute<T> extends PageRoute<T> { final Widget child; final Duration duration; SlideUpPageRoute({ required this.child, this.duration = const Duration(milliseconds: 300), }); @override Color? get barrierColor => null; @override String? get barrierLabel => null; @override Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { return child; } @override Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { final slideAnimation = Tween<Offset>( begin: const Offset(0, 1), end: Offset.zero, ).animate(CurvedAnimation( parent: animation, curve: Curves.easeOutCubic, )); final fadeAnimation = Tween<double>( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: animation, curve: const Interval(0.3, 1.0), )); return SlideTransition( position: slideAnimation, child: FadeTransition( opacity: fadeAnimation, child: child, ), ); } @override Duration get transitionDuration => duration; @override bool get maintainState => true; }
🦸 Hero Best Practices:
- • Use unique hero tags to avoid conflicts
- • Custom flight shuttle builders for unique transitions
- • Consider performance impact of complex hero animations
- • Test hero animations on different screen sizes
CUSTOM PAINT ANIMATIONS
Custom painters let you create any animation imaginable. From morphing shapes to particle systems, custom paint gives you pixel-level control over your animations.
class AnimatedWavePainter extends CustomPainter { final double animationValue; final Color waveColor; AnimatedWavePainter({ required this.animationValue, required this.waveColor, }); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = waveColor ..style = PaintingStyle.fill; final path = Path(); final waveHeight = 20.0; final waveLength = size.width / 2; path.moveTo(0, size.height / 2); for (double x = 0; x <= size.width; x += 1) { final y = size.height / 2 + waveHeight * math.sin((x / waveLength) * 2 * math.pi + animationValue * 2 * math.pi); path.lineTo(x, y); } path.lineTo(size.width, size.height); path.lineTo(0, size.height); path.close(); canvas.drawPath(path, paint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return oldDelegate is AnimatedWavePainter && oldDelegate.animationValue != animationValue; } } class WaveAnimationWidget extends StatefulWidget { @override _WaveAnimationWidgetState createState() => _WaveAnimationWidgetState(); } class _WaveAnimationWidgetState extends State<WaveAnimationWidget> with SingleTickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 3), vsync: this, )..repeat(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (context, child) { return CustomPaint( painter: AnimatedWavePainter( animationValue: _controller.value, waveColor: Colors.blue, ), size: const Size(300, 200), ); }, ); } }
🎨 Custom Paint Tips:
- • Optimize shouldRepaint for better performance
- • Use Path.combine for complex shape operations
- • Cache expensive calculations outside paint method
- • Consider using shaders for advanced effects
ANIMATION PERFORMANCE SECRETS
✅ DO THIS
- • Use const constructors for child widgets
- • Optimize with RepaintBoundary
- • Use AnimatedBuilder for specific rebuilds
- • Dispose controllers properly
- • Use Transform instead of changing layout
❌ AVOID THIS
- • Animating layout properties directly
- • Creating controllers in build method
- • Complex calculations in paint method
- • Too many simultaneous animations
- • Forgetting to dispose resources