Building Custom Animations in Flutter
•11 min read
Flutter provides powerful tools for creating custom animations. This article will guide you through building custom animations using AnimationController, Tween, and custom painters.
Understanding Animation Basics
1. AnimationController
class CustomAnimation extends StatefulWidget { @override _CustomAnimationState createState() => _CustomAnimationState(); } class _CustomAnimationState extends State<CustomAnimation> with SingleTickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( duration: Duration(seconds: 2), vsync: this, )..repeat(); } @override void dispose() { _controller.dispose(); super.dispose(); } }
2. Tween and CurvedAnimation
class _CustomAnimationState extends State<CustomAnimation> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<double> _animation; @override void initState() { super.initState(); _controller = AnimationController( duration: Duration(seconds: 2), vsync: this, ); _animation = Tween<double>( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _controller, curve: Curves.easeInOut, )); _controller.repeat(); } }
Creating Custom Animations
1. Custom Painter Animation
class CustomPainterAnimation extends CustomPainter { final Animation<double> animation; CustomPainterAnimation(this.animation) : super(repaint: animation); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.blue ..style = PaintingStyle.fill; final center = Offset(size.width / 2, size.height / 2); final radius = size.width / 4 * animation.value; canvas.drawCircle(center, radius, paint); } @override bool shouldRepaint(CustomPainterAnimation oldDelegate) { return oldDelegate.animation != animation; } }
2. Using Custom Painter
class AnimatedCircle extends StatelessWidget { @override Widget build(BuildContext context) { return CustomPaint( painter: CustomPainterAnimation( Tween<double>(begin: 0, end: 1).animate( AnimationController( duration: Duration(seconds: 2), vsync: this, )..repeat(), ), ), child: Container(), ); } }
Advanced Animation Techniques
1. Staggered Animations
class StaggeredAnimation extends StatefulWidget { @override _StaggeredAnimationState createState() => _StaggeredAnimationState(); } class _StaggeredAnimationState extends State<StaggeredAnimation> with TickerProviderStateMixin { late AnimationController _controller; late List<Animation<double>> _animations; @override void initState() { super.initState(); _controller = AnimationController( duration: Duration(seconds: 2), vsync: this, ); _animations = List.generate(3, (index) { return Tween<double>( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _controller, curve: Interval( index * 0.3, (index + 1) * 0.3, curve: Curves.easeInOut, ), )); }); _controller.forward(); } }
2. Physics-Based Animations
class PhysicsAnimation extends StatefulWidget { @override _PhysicsAnimationState createState() => _PhysicsAnimationState(); } class _PhysicsAnimationState extends State<PhysicsAnimation> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<double> _animation; @override void initState() { super.initState(); _controller = AnimationController( duration: Duration(seconds: 2), vsync: this, ); _animation = Tween<double>( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _controller, curve: Curves.bounceOut, )); _controller.forward(); } }
Performance Optimization
1. Using RepaintBoundary
RepaintBoundary( child: CustomPaint( painter: CustomPainterAnimation(_animation), child: Container(), ), )
2. Optimizing Animation Updates
class OptimizedAnimation extends StatefulWidget { @override _OptimizedAnimationState createState() => _OptimizedAnimationState(); } class _OptimizedAnimationState extends State<OptimizedAnimation> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<double> _animation; @override void initState() { super.initState(); _controller = AnimationController( duration: Duration(seconds: 2), vsync: this, ); _animation = Tween<double>( begin: 0.0, end: 1.0, ).animate(_controller); _controller.addListener(() { if (_controller.isCompleted) { _controller.reverse(); } else if (_controller.isDismissed) { _controller.forward(); } }); _controller.forward(); } }
Best Practices
- Use Appropriate Curves: Choose curves that match the animation's purpose
- Optimize Performance: Use RepaintBoundary for complex animations
- Handle Disposal: Always dispose of AnimationControllers
- Test on Multiple Devices: Ensure smooth performance across devices
- Consider Accessibility: Provide alternative content for users who prefer reduced motion
Example: Custom Loading Animation
class CustomLoadingAnimation extends StatefulWidget { @override _CustomLoadingAnimationState createState() => _CustomLoadingAnimationState(); } class _CustomLoadingAnimationState extends State<CustomLoadingAnimation> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<double> _rotationAnimation; late Animation<double> _scaleAnimation; @override void initState() { super.initState(); _controller = AnimationController( duration: Duration(seconds: 2), vsync: this, ); _rotationAnimation = Tween<double>( begin: 0.0, end: 2 * pi, ).animate(CurvedAnimation( parent: _controller, curve: Curves.linear, )); _scaleAnimation = Tween<double>( begin: 0.5, end: 1.0, ).animate(CurvedAnimation( parent: _controller, curve: Curves.easeInOut, )); _controller.repeat(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (context, child) { return Transform.rotate( angle: _rotationAnimation.value, child: Transform.scale( scale: _scaleAnimation.value, child: Icon(Icons.refresh, size: 48), ), ); }, ); } @override void dispose() { _controller.dispose(); super.dispose(); } }
Conclusion
Building custom animations in Flutter involves:
- Understanding AnimationController and Tween
- Creating custom painters for complex animations
- Implementing staggered and physics-based animations
- Optimizing performance with RepaintBoundary
- Following best practices for smooth animations
-
Performance
- Use
const
widgets where possible - Avoid unnecessary rebuilds
- Use
RepaintBoundary
for complex animations - Optimize animation curves
- Use
-
User Experience
- Keep animations short and purposeful
- Provide clear feedback
- Consider motion sickness
- Respect user preferences
-
Code Organization
- Separate animation logic
- Use mixins for common behaviors
- Implement proper cleanup
- Document animation parameters
Common Animation Patterns
1. Page Transitions
class CustomPageRoute extends PageRouteBuilder { final Widget page; CustomPageRoute({required this.page}) : super( pageBuilder: (context, animation, secondaryAnimation) => page, transitionsBuilder: (context, animation, secondaryAnimation, child) { return FadeTransition( opacity: animation, child: child, ); }, ); } // Usage Navigator.push( context, CustomPageRoute(page: const NextPage()), );
2. Loading Animations
class LoadingAnimation extends StatefulWidget { const LoadingAnimation({super.key}); @override State<LoadingAnimation> createState() => _LoadingAnimationState(); } class _LoadingAnimationState extends State<LoadingAnimation> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<double> _rotationAnimation; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(seconds: 2), ); _rotationAnimation = Tween<double>( begin: 0, end: 2 * math.pi, ).animate(_controller); _controller.repeat(); } @override Widget build(BuildContext context) { return RotationTransition( turns: _rotationAnimation, child: const Icon(Icons.refresh, size: 50), ); } }
3. Interactive Animations
class InteractiveAnimation extends StatefulWidget { const InteractiveAnimation({super.key}); @override State<InteractiveAnimation> createState() => _InteractiveAnimationState(); } class _InteractiveAnimationState extends State<InteractiveAnimation> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<double> _animation; double _dragPosition = 0; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 300), ); _animation = Tween<double>(begin: 0, end: 1).animate(_controller); } @override Widget build(BuildContext context) { return GestureDetector( onPanUpdate: (details) { setState(() { _dragPosition += details.delta.dx; }); }, onPanEnd: (_) { if (_dragPosition > 100) { _controller.forward(); } else { _controller.reverse(); } }, child: Transform.translate( offset: Offset(_dragPosition, 0), child: const FlutterLogo(size: 100), ), ); } }
Testing Animations
void main() { group('Animation Tests', () { testWidgets('animates correctly', (tester) async { await tester.pumpWidget(const MaterialApp( home: BasicAnimation(), )); expect(find.byType(FlutterLogo), findsOneWidget); await tester.pump(const Duration(seconds: 1)); // Verify animation state }); testWidgets('handles user interaction', (tester) async { await tester.pumpWidget(const MaterialApp( home: InteractiveAnimation(), )); await tester.drag(find.byType(FlutterLogo), const Offset(200, 0)); await tester.pumpAndSettle(); // Verify final position }); }); }
Performance Optimization
-
Use RepaintBoundary
RepaintBoundary( child: AnimatedContainer( duration: const Duration(seconds: 1), color: Colors.blue, ), )
-
Optimize Animation Curves
CurvedAnimation( parent: _controller, curve: Curves.easeOutCubic, // More performant than easeOut )
-
Use Transform Instead of Layout Changes
Transform.scale( scale: _animation.value, child: const FlutterLogo(), )
Conclusion
Creating custom animations in Flutter requires understanding of:
- Animation Fundamentals: Controllers, tweens, and curves
- Performance Considerations: Optimization and best practices
- User Experience: Purposeful and smooth animations
- Advanced Techniques: Physics, staggered, and custom animations
- Testing: Verifying animation behavior
Remember to:
- Keep animations purposeful and smooth
- Optimize for performance
- Test thoroughly
- Consider accessibility
- Document animation parameters
By following these guidelines, you can create engaging and performant animations that enhance your Flutter applications.