Mastering Flutter Custom Painters: Create Beautiful Custom Graphics
•12 min read
Flutter's CustomPainter is a powerful tool for creating custom graphics, animations, and unique UI elements. This guide will help you master CustomPainter through practical examples and advanced techniques.
Understanding CustomPainter
CustomPainter is a class that provides a canvas on which you can draw various shapes, paths, and patterns using Flutter's low-level drawing APIs.
Basic Structure
class MyCustomPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { // Drawing code goes here } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return false; // Return true if the painting should be updated } }
Basic Shapes and Lines
1. Drawing Lines
class LinesPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.blue ..strokeWidth = 4 ..strokeCap = StrokeCap.round; canvas.drawLine( Offset(0, size.height / 2), Offset(size.width, size.height / 2), paint, ); // Dashed line final dashWidth = 10.0; final dashSpace = 5.0; double startX = 0; while (startX < size.width) { canvas.drawLine( Offset(startX, size.height / 4), Offset(startX + dashWidth, size.height / 4), paint, ); startX += dashWidth + dashSpace; } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; }
2. Drawing Shapes
class ShapesPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.red ..style = PaintingStyle.stroke ..strokeWidth = 2; // Rectangle canvas.drawRect( Rect.fromLTWH(10, 10, 100, 100), paint, ); // Circle canvas.drawCircle( Offset(size.width / 2, size.height / 2), 50, paint..color = Colors.blue, ); // Oval canvas.drawOval( Rect.fromCenter( center: Offset(size.width * 0.7, size.height * 0.7), width: 100, height: 50, ), paint..color = Colors.green, ); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; }
Advanced Drawing Techniques
1. Custom Paths
class StarPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.yellow ..style = PaintingStyle.fill; final path = Path(); final center = Offset(size.width / 2, size.height / 2); final radius = size.width < size.height ? size.width / 2 : size.height / 2; for (var i = 0; i < 5; i++) { final angle = (i * 72) * pi / 180; final point = Offset( center.dx + radius * cos(angle), center.dy + radius * sin(angle), ); if (i == 0) { path.moveTo(point.dx, point.dy); } else { path.lineTo(point.dx, point.dy); } } path.close(); canvas.drawPath(path, paint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; }
2. Gradients and Shadows
class GradientPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() ..shader = LinearGradient( colors: [Colors.blue, Colors.green], begin: Alignment.topLeft, end: Alignment.bottomRight, ).createShader(Rect.fromLTWH(0, 0, size.width, size.height)); // Draw rectangle with gradient canvas.drawRect( Rect.fromLTWH(0, 0, size.width, size.height), paint, ); // Draw circle with shadow final shadowPaint = Paint() ..color = Colors.blue ..maskFilter = MaskFilter.blur(BlurStyle.normal, 8); canvas.drawCircle( Offset(size.width / 2, size.height / 2), 50, shadowPaint, ); canvas.drawCircle( Offset(size.width / 2, size.height / 2), 50, Paint()..color = Colors.white, ); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; }
Animated Custom Painters
1. Basic Animation
class AnimatedCirclePainter extends CustomPainter { final double progress; AnimatedCirclePainter(this.progress); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.blue ..style = PaintingStyle.stroke ..strokeWidth = 4; final center = Offset(size.width / 2, size.height / 2); final radius = min(size.width, size.height) / 2; canvas.drawArc( Rect.fromCircle(center: center, radius: radius), -pi / 2, 2 * pi * progress, false, paint, ); } @override bool shouldRepaint(covariant AnimatedCirclePainter oldDelegate) { return oldDelegate.progress != progress; } } // Usage in StatefulWidget class AnimatedCircle extends StatefulWidget { @override _AnimatedCircleState createState() => _AnimatedCircleState(); } class _AnimatedCircleState extends State<AnimatedCircle> with SingleTickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: Duration(seconds: 2), )..repeat(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (context, child) { return CustomPaint( painter: AnimatedCirclePainter(_controller.value), size: Size(200, 200), ); }, ); } @override void dispose() { _controller.dispose(); super.dispose(); } }
2. Complex Animation
class WavePainter extends CustomPainter { final Animation<double> animation; WavePainter(this.animation); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.blue.withOpacity(0.3) ..style = PaintingStyle.fill; final path = Path(); path.moveTo(0, size.height); for (var i = 0.0; i <= size.width; i++) { path.lineTo( i, size.height / 2 + sin((i / size.width * 2 * pi) + (animation.value * 2 * pi)) * 20, ); } path.lineTo(size.width, size.height); path.close(); canvas.drawPath(path, paint); } @override bool shouldRepaint(covariant WavePainter oldDelegate) { return oldDelegate.animation.value != animation.value; } }
Practical Examples
1. Custom Progress Indicator
class CircularProgressPainter extends CustomPainter { final double progress; final Color color; final double strokeWidth; CircularProgressPainter({ required this.progress, this.color = Colors.blue, this.strokeWidth = 10, }); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = color.withOpacity(0.2) ..strokeWidth = strokeWidth ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; canvas.drawCircle( Offset(size.width / 2, size.height / 2), min(size.width, size.height) / 2, paint, ); paint.color = color; canvas.drawArc( Rect.fromCircle( center: Offset(size.width / 2, size.height / 2), radius: min(size.width, size.height) / 2, ), -pi / 2, 2 * pi * progress, false, paint, ); } @override bool shouldRepaint(covariant CircularProgressPainter oldDelegate) { return oldDelegate.progress != progress || oldDelegate.color != color || oldDelegate.strokeWidth != strokeWidth; } }
2. Custom Chart
class BarChartPainter extends CustomPainter { final List<double> data; final double maxValue; BarChartPainter(this.data) : maxValue = data.reduce(max); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.blue ..style = PaintingStyle.fill; final barWidth = size.width / data.length; for (var i = 0; i < data.length; i++) { final barHeight = (data[i] / maxValue) * size.height; canvas.drawRect( Rect.fromLTWH( i * barWidth, size.height - barHeight, barWidth * 0.8, barHeight, ), paint, ); } } @override bool shouldRepaint(covariant BarChartPainter oldDelegate) { return oldDelegate.data != data; } }
Best Practices
- Performance Optimization
class OptimizedPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { // Cache paint objects final paint = Paint(); // Use clipping for complex shapes canvas.clipRect(Rect.fromLTWH(0, 0, size.width, size.height)); // Batch similar operations final path = Path(); // Add multiple shapes to the same path canvas.drawPath(path, paint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { // Only repaint when necessary return false; } }
- Reusable Components
class ShapeUtils { static Path createStarPath(Offset center, double radius, int points) { final path = Path(); final angle = (2 * pi) / points; for (var i = 0; i < points; i++) { final x = center.dx + radius * cos(i * angle - pi / 2); final y = center.dy + radius * sin(i * angle - pi / 2); if (i == 0) { path.moveTo(x, y); } else { path.lineTo(x, y); } } path.close(); return path; } }
Conclusion
CustomPainter is a powerful tool for creating unique and engaging graphics in Flutter. Key takeaways:
- Start with basic shapes and gradually move to complex paths
- Use animations to bring your graphics to life
- Optimize performance by caching paint objects and using efficient drawing methods
- Create reusable components for common patterns
- Consider device pixel ratio for crisp graphics
- Test on different screen sizes
Remember to:
- Keep your paint code organized and modular
- Use appropriate paint styles and stroke properties
- Handle different screen sizes properly
- Optimize performance with shouldRepaint
- Document complex drawing logic
With these techniques and best practices, you can create beautiful and performant custom graphics in your Flutter applications.