Flutter Animations and Custom Painters: Creating Beautiful UI Effects
•16 min read
Learn how to create stunning animations and custom-drawn widgets in Flutter using the Animation framework and CustomPainter class. This guide covers everything from basic animations to complex custom paintings.
1. Basic Animations
Implicit Animations
class AnimatedContainerExample extends StatelessWidget { @override Widget build(BuildContext context) { return AnimatedContainer( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, width: isExpanded ? 200.0 : 100.0, height: isExpanded ? 200.0 : 100.0, decoration: BoxDecoration( color: isExpanded ? Colors.blue : Colors.red, borderRadius: BorderRadius.circular(isExpanded ? 20.0 : 10.0), ), child: Center(child: Text('Tap to animate')), ); } }
Explicit Animations
class RotatingLogo extends StatefulWidget { @override _RotatingLogoState createState() => _RotatingLogoState(); } class _RotatingLogoState extends State<RotatingLogo> 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, end: 2 * pi, ).animate(CurvedAnimation( parent: _controller, curve: Curves.easeInOut, )); _controller.repeat(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _animation, builder: (context, child) { return Transform.rotate( angle: _animation.value, child: FlutterLogo(size: 100), ); }, ); } @override void dispose() { _controller.dispose(); super.dispose(); } }
2. Custom Painters
Basic Shape Drawing
class CirclePainter extends CustomPainter { final Color color; final double radius; CirclePainter({ required this.color, required this.radius, }); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = color ..style = PaintingStyle.fill; canvas.drawCircle( Offset(size.width / 2, size.height / 2), radius, paint, ); } @override bool shouldRepaint(CirclePainter oldDelegate) { return oldDelegate.color != color || oldDelegate.radius != radius; } } // Usage CustomPaint( painter: CirclePainter( color: Colors.blue, radius: 50.0, ), size: Size(100, 100), )
Complex Shapes and Paths
class WavePainter extends CustomPainter { final Color color; final double amplitude; final double frequency; final double phase; WavePainter({ required this.color, this.amplitude = 20.0, this.frequency = 0.05, this.phase = 0.0, }); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = color ..style = PaintingStyle.stroke ..strokeWidth = 2.0; final path = Path(); path.moveTo(0, size.height / 2); for (double x = 0; x < size.width; x++) { final y = size.height / 2 + amplitude * sin(frequency * x + phase); path.lineTo(x, y); } canvas.drawPath(path, paint); } @override bool shouldRepaint(WavePainter oldDelegate) { return oldDelegate.color != color || oldDelegate.amplitude != amplitude || oldDelegate.frequency != frequency || oldDelegate.phase != phase; } }
Animated Custom Painter
class AnimatedWaveWidget extends StatefulWidget { @override _AnimatedWaveWidgetState createState() => _AnimatedWaveWidgetState(); } class _AnimatedWaveWidgetState extends State<AnimatedWaveWidget> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<double> _phaseAnimation; @override void initState() { super.initState(); _controller = AnimationController( duration: Duration(seconds: 2), vsync: this, ); _phaseAnimation = Tween<double>( begin: 0, end: 2 * pi, ).animate(_controller); _controller.repeat(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _phaseAnimation, builder: (context, child) { return CustomPaint( painter: WavePainter( color: Colors.blue, phase: _phaseAnimation.value, ), size: Size(300, 200), ); }, ); } @override void dispose() { _controller.dispose(); super.dispose(); } }
3. Advanced Animation Techniques
Staggered Animations
class StaggeredAnimationExample extends StatefulWidget { @override _StaggeredAnimationExampleState createState() => _StaggeredAnimationExampleState(); } class _StaggeredAnimationExampleState extends State<StaggeredAnimationExample> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation<double> _fadeAnimation; late Animation<double> _slideAnimation; late Animation<double> _scaleAnimation; @override void initState() { super.initState(); _controller = AnimationController( duration: Duration(seconds: 2), vsync: this, ); _fadeAnimation = Tween<double>( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _controller, curve: Interval(0.0, 0.3, curve: Curves.easeIn), )); _slideAnimation = Tween<double>( begin: -100.0, end: 0.0, ).animate(CurvedAnimation( parent: _controller, curve: Interval(0.3, 0.6, curve: Curves.easeOut), )); _scaleAnimation = Tween<double>( begin: 0.5, end: 1.0, ).animate(CurvedAnimation( parent: _controller, curve: Interval(0.6, 1.0, curve: Curves.elasticOut), )); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (context, child) { return Opacity( opacity: _fadeAnimation.value, child: Transform.translate( offset: Offset(_slideAnimation.value, 0.0), child: Transform.scale( scale: _scaleAnimation.value, child: Container( width: 200, height: 200, color: Colors.blue, child: Center( child: Text('Staggered Animation'), ), ), ), ), ); }, ); } @override void dispose() { _controller.dispose(); super.dispose(); } }
Hero Animations
// First screen class FirstScreen extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute(builder: (context) => SecondScreen()), ); }, child: Hero( tag: 'imageHero', child: Image.asset( 'assets/image.jpg', width: 100, height: 100, ), ), ); } } // Second screen class SecondScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Hero( tag: 'imageHero', child: Image.asset( 'assets/image.jpg', width: 300, height: 300, ), ), ), ); } }
4. Custom Painter Effects
Gradient Progress Indicator
class GradientProgressPainter extends CustomPainter { final double progress; final List<Color> gradientColors; GradientProgressPainter({ required this.progress, required this.gradientColors, }); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..strokeWidth = 10.0 ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; // Background track paint.color = Colors.grey.withOpacity(0.2); canvas.drawArc( Offset.zero & size, -pi / 2, 2 * pi, false, paint, ); // Progress gradient paint.shader = SweepGradient( colors: gradientColors, startAngle: -pi / 2, endAngle: 3 * pi / 2, ).createShader(Offset.zero & size); canvas.drawArc( Offset.zero & size, -pi / 2, 2 * pi * progress, false, paint, ); } @override bool shouldRepaint(GradientProgressPainter oldDelegate) { return oldDelegate.progress != progress; } }
Particle System
class Particle { Offset position; Offset velocity; double size; Color color; double life; Particle({ required this.position, required this.velocity, required this.size, required this.color, required this.life, }); void update() { position += velocity; life -= 0.01; } } class ParticleSystemPainter extends CustomPainter { final List<Particle> particles; ParticleSystemPainter(this.particles); @override void paint(Canvas canvas, Size size) { final paint = Paint()..style = PaintingStyle.fill; for (final particle in particles) { paint.color = particle.color.withOpacity(particle.life); canvas.drawCircle( particle.position, particle.size * particle.life, paint, ); } } @override bool shouldRepaint(ParticleSystemPainter oldDelegate) => true; } class ParticleSystemWidget extends StatefulWidget { @override _ParticleSystemWidgetState createState() => _ParticleSystemWidgetState(); } class _ParticleSystemWidgetState extends State<ParticleSystemWidget> with SingleTickerProviderStateMixin { late AnimationController _controller; final List<Particle> _particles = []; final Random _random = Random(); @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: Duration(seconds: 1), )..addListener(_updateParticles); _controller.repeat(); } void _updateParticles() { _particles.removeWhere((particle) => particle.life <= 0); if (_particles.length < 100) { _particles.add( Particle( position: Offset( _random.nextDouble() * 300, _random.nextDouble() * 300, ), velocity: Offset( _random.nextDouble() * 2 - 1, _random.nextDouble() * 2 - 1, ), size: _random.nextDouble() * 10 + 5, color: Colors.blue, life: 1.0, ), ); } for (final particle in _particles) { particle.update(); } setState(() {}); } @override Widget build(BuildContext context) { return CustomPaint( painter: ParticleSystemPainter(_particles), size: Size(300, 300), ); } @override void dispose() { _controller.dispose(); super.dispose(); } }
Best Practices
- Performance Optimization
- Use
RepaintBoundary
to isolate animations - Implement
shouldRepaint
correctly - Minimize canvas operations
- Use hardware acceleration when possible
- Use
class OptimizedAnimation extends StatelessWidget { @override Widget build(BuildContext context) { return RepaintBoundary( child: CustomPaint( painter: MyOptimizedPainter(), ), ); } }
- Animation Composition
- Break complex animations into smaller parts
- Reuse animation controllers when possible
- Use animation curves for natural motion
class ComposedAnimation extends StatelessWidget { final Animation<double> animation; ComposedAnimation({required this.animation}); @override Widget build(BuildContext context) { return Stack( children: [ SlideTransition( position: Tween<Offset>( begin: Offset(-1, 0), end: Offset.zero, ).animate(animation), child: FadeTransition( opacity: animation, child: ScaleTransition( scale: animation, child: YourWidget(), ), ), ), ], ); } }
- Custom Painter Organization
- Separate drawing logic into methods
- Use constants for magic numbers
- Document complex drawing operations
class OrganizedPainter extends CustomPainter { static const double STROKE_WIDTH = 2.0; static const double CORNER_RADIUS = 10.0; @override void paint(Canvas canvas, Size size) { _drawBackground(canvas, size); _drawForeground(canvas, size); _drawDetails(canvas, size); } void _drawBackground(Canvas canvas, Size size) { // Background drawing logic } void _drawForeground(Canvas canvas, Size size) { // Foreground drawing logic } void _drawDetails(Canvas canvas, Size size) { // Details drawing logic } @override bool shouldRepaint(OrganizedPainter oldDelegate) => false; }
Conclusion
Creating beautiful animations and custom-drawn widgets in Flutter requires:
-
Understanding Animation Types
- Implicit vs Explicit animations
- Staggered animations
- Hero transitions
-
Custom Painter Mastery
- Basic shapes and paths
- Complex effects
- Performance optimization
-
Best Practices
- Code organization
- Performance considerations
- Reusability
Remember to:
- Keep animations smooth and natural
- Optimize performance
- Follow material design guidelines
- Test on different devices
- Handle edge cases
By following these practices, you can create engaging and performant animations that enhance your Flutter application's user experience.