Advanced Flutter Animations and Transitions
•17 min read
Flutter provides a powerful animation system that can create beautiful and engaging user experiences. In this post, we'll explore advanced animation techniques and transitions that can take your Flutter apps to the next level.
1. Custom Animation Controllers
Create complex animations with custom controllers.
class CustomAnimation extends StatefulWidget { @override _CustomAnimationState createState() => _CustomAnimationState(); } class _CustomAnimationState extends State<CustomAnimation> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<double> _animation; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 2), vsync: this, ); _animation = CurvedAnimation( parent: _controller, curve: Curves.easeInOut, ); _controller.repeat(reverse: true); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _animation, builder: (context, child) { return Transform.scale( scale: 1.0 + (_animation.value * 0.2), child: Container( width: 100, height: 100, color: Colors.blue, ), ); }, ); } @override void dispose() { _controller.dispose(); super.dispose(); } }
2. Staggered Animations
Create sequences of animations that follow each other.
class StaggeredAnimation extends StatelessWidget { final Animation<double> controller; final Animation<double> opacity; final Animation<double> width; final Animation<double> height; final Animation<EdgeInsets> padding; final Animation<BorderRadius> borderRadius; final Animation<Color> color; StaggeredAnimation({Key? key, required this.controller}) : opacity = Tween<double>( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: controller, curve: Interval(0.0, 0.1, curve: Curves.ease), )), width = Tween<double>( begin: 50.0, end: 150.0, ).animate(CurvedAnimation( parent: controller, curve: Interval(0.1, 0.3, curve: Curves.ease), )), height = Tween<double>( begin: 50.0, end: 150.0, ).animate(CurvedAnimation( parent: controller, curve: Interval(0.3, 0.6, curve: Curves.ease), )), padding = EdgeInsetsTween( begin: const EdgeInsets.only(bottom: 16), end: const EdgeInsets.only(bottom: 75), ).animate(CurvedAnimation( parent: controller, curve: Interval(0.6, 0.8, curve: Curves.ease), )), borderRadius = BorderRadiusTween( begin: BorderRadius.circular(4), end: BorderRadius.circular(75), ).animate(CurvedAnimation( parent: controller, curve: Interval(0.8, 1.0, curve: Curves.ease), )), color = ColorTween( begin: Colors.blue, end: Colors.orange, ).animate(CurvedAnimation( parent: controller, curve: Interval(0.0, 1.0, curve: Curves.ease), )), super(key: key); @override Widget build(BuildContext context) { return AnimatedBuilder( animation: controller, builder: (context, child) { return Container( padding: padding.value, alignment: Alignment.bottomCenter, child: Opacity( opacity: opacity.value, child: Container( width: width.value, height: height.value, decoration: BoxDecoration( color: color.value, border: Border.all( color: Colors.white, width: 3.0, ), borderRadius: borderRadius.value, ), ), ), ); }, ); } }
3. Page Route Transitions
Create custom page transitions for navigation.
class CustomPageRoute extends PageRouteBuilder { final Widget child; final AxisDirection direction; CustomPageRoute({ required this.child, this.direction = AxisDirection.right, }) : super( transitionDuration: const Duration(milliseconds: 500), reverseTransitionDuration: const Duration(milliseconds: 500), pageBuilder: (context, animation, secondaryAnimation) => child, ); @override Widget buildTransitions( BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child, ) { return SlideTransition( position: Tween<Offset>( begin: _getOffset(), end: Offset.zero, ).animate(CurvedAnimation( parent: animation, curve: Curves.easeOut, )), child: child, ); } Offset _getOffset() { switch (direction) { case AxisDirection.up: return const Offset(0, 1); case AxisDirection.right: return const Offset(-1, 0); case AxisDirection.down: return const Offset(0, -1); case AxisDirection.left: return const Offset(1, 0); } } }
4. Implicit Animations
Use built-in implicit animations for simple transitions.
class ImplicitAnimation extends StatefulWidget { @override _ImplicitAnimationState createState() => _ImplicitAnimationState(); } class _ImplicitAnimationState extends State<ImplicitAnimation> { bool _isExpanded = false; @override Widget build(BuildContext context) { return GestureDetector( onTap: () => setState(() => _isExpanded = !_isExpanded), child: AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, width: _isExpanded ? 200 : 100, height: _isExpanded ? 200 : 100, decoration: BoxDecoration( color: _isExpanded ? Colors.orange : Colors.blue, borderRadius: BorderRadius.circular(_isExpanded ? 20 : 10), ), child: Center( child: AnimatedDefaultTextStyle( duration: const Duration(milliseconds: 300), style: TextStyle( fontSize: _isExpanded ? 24 : 16, color: Colors.white, ), child: const Text('Tap me!'), ), ), ), ); } }
5. Physics-based Animations
Create realistic animations using physics simulations.
class PhysicsAnimation extends StatefulWidget { @override _PhysicsAnimationState createState() => _PhysicsAnimationState(); } class _PhysicsAnimationState extends State<PhysicsAnimation> with SingleTickerProviderStateMixin { late AnimationController _controller; late SpringSimulation _simulation; double _position = 0.0; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(seconds: 2), ); const spring = SpringDescription( mass: 1.0, stiffness: 500.0, damping: 10.0, ); _simulation = SpringSimulation(spring, 0.0, 1.0, 0.0); _controller.addListener(() { setState(() { _position = _simulation.x(_controller.value); }); }); } void _startAnimation() { _controller.animateWith(_simulation); } @override Widget build(BuildContext context) { return GestureDetector( onTap: _startAnimation, child: Transform.translate( offset: Offset(0, 100 * _position), child: Container( width: 50, height: 50, decoration: BoxDecoration( color: Colors.blue, shape: BoxShape.circle, ), ), ), ); } @override void dispose() { _controller.dispose(); super.dispose(); } }
6. Hero Animations with Custom Transitions
Create custom hero animations with shared elements.
class CustomHeroAnimation extends StatelessWidget { @override Widget build(BuildContext context) { return Hero( tag: 'hero-tag', 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: child, ); }, child: Container( width: 100, height: 100, color: Colors.blue, ), ); }, child: Container( width: 100, height: 100, color: Colors.blue, ), ); } }
7. Custom Tween Animations
class ColorSequenceTween extends Tween<Color?> { final List<Color> colors; final List<double> stops; ColorSequenceTween({ required this.colors, required this.stops, }) : assert(colors.length == stops.length), super(begin: colors.first, end: colors.last); @override Color? lerp(double t) { if (t <= stops.first) return colors.first; if (t >= stops.last) return colors.last; for (int i = 0; i < stops.length - 1; i++) { if (t >= stops[i] && t <= stops[i + 1]) { final localT = (t - stops[i]) / (stops[i + 1] - stops[i]); return Color.lerp(colors[i], colors[i + 1], localT); } } return colors.last; } } class CustomTweenAnimation extends StatefulWidget { @override _CustomTweenAnimationState createState() => _CustomTweenAnimationState(); } class _CustomTweenAnimationState extends State<CustomTweenAnimation> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<Color?> _colorAnimation; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 3), vsync: this, ); _colorAnimation = ColorSequenceTween( colors: [ Colors.blue, Colors.purple, Colors.red, Colors.orange, Colors.yellow, ], stops: [0.0, 0.25, 0.5, 0.75, 1.0], ).animate(_controller); _controller.repeat(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _colorAnimation, builder: (context, child) { return Container( width: 200, height: 200, decoration: BoxDecoration( color: _colorAnimation.value, borderRadius: BorderRadius.circular(20), ), ); }, ); } @override void dispose() { _controller.dispose(); super.dispose(); } }
8. Performance Optimization
8.1 RepaintBoundary Usage
class OptimizedAnimation extends StatelessWidget { @override Widget build(BuildContext context) { return RepaintBoundary( child: CustomPaint( painter: AnimationPainter(), ), ); } } class AnimationPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { // Complex painting operations } @override bool shouldRepaint(AnimationPainter oldDelegate) { // Only repaint when necessary return false; } }
8.2 Caching Animations
class CachedAnimation extends StatefulWidget { @override _CachedAnimationState createState() => _CachedAnimationState(); } class _CachedAnimationState extends State<CachedAnimation> with SingleTickerProviderStateMixin { late AnimationController _controller; final List<Widget> _cachedFrames = []; static const int totalFrames = 60; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 1), vsync: this, ); // Pre-compute animation frames for (int i = 0; i < totalFrames; i++) { final t = i / (totalFrames - 1); _cachedFrames.add( Transform.rotate( angle: 2 * pi * t, child: Container( width: 100, height: 100, decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.blue, Colors.purple], stops: [0, t], ), ), ), ), ); } } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (context, child) { final frame = (_controller.value * (totalFrames - 1)).round(); return _cachedFrames[frame]; }, ); } @override void dispose() { _controller.dispose(); super.dispose(); } }
Best Practices for Animations
- Always dispose of animation controllers
- Use
CurvedAnimation
for natural movement - Consider performance implications
- Test animations on different devices
- Provide fallbacks for users who prefer reduced motion
Conclusion
Flutter's animation system is powerful and flexible, allowing you to create engaging user experiences. Remember to:
- Keep animations subtle and purposeful
- Consider performance implications
- Test on various devices
- Follow platform guidelines
- Provide accessibility options
Stay tuned for more advanced animation techniques and real-world examples!