Custom Painters: Drawing Custom Graphics in Flutter
Custom Painters: Drawing Custom Graphics in Flutter
What You’ll Learn
How to use CustomPaint and CustomPainter to draw custom graphics, shapes, and visual effects that go beyond standard widgets. This unlocks the ability to create charts, diagrams, custom progress indicators, and unique UI elements.
Why Custom Painters Matter
Sometimes Flutter’s built-in widgets aren’t enough—you need to draw something completely custom. Maybe you’re building a data visualization, a signature pad, a drawing app, or unique animated effects. CustomPaint gives you direct access to the canvas, letting you draw anything you can imagine using low-level graphics primitives.
The Basics: Canvas and Paint
Every custom drawing involves two key objects:
- Canvas: The surface you draw on, providing methods like
drawCircle(),drawLine(),drawPath(), etc. - Paint: Defines how to draw—colors, stroke width, fill style, etc.
Example: Creating a Simple Custom Painter
Let’s draw a simple progress arc—like a circular progress indicator with custom styling:
import 'package:flutter/material.dart';
import 'dart:math' as math;
class CircularProgressPainter extends CustomPainter {
final double progress; // 0.0 to 1.0
final Color color;
final double strokeWidth;
CircularProgressPainter({
required this.progress,
required this.color,
this.strokeWidth = 8.0,
});
@override
void paint(Canvas canvas, Size size) {
// Background arc (gray)
final backgroundPaint = Paint()
..color = Colors.grey.shade300
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
// Progress arc (colored)
final progressPaint = Paint()
..color = color
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
final center = Offset(size.width / 2, size.height / 2);
final radius = math.min(size.width, size.height) / 2 - strokeWidth / 2;
// Draw background circle
canvas.drawCircle(center, radius, backgroundPaint);
// Draw progress arc
const startAngle = -math.pi / 2; // Start at top
final sweepAngle = 2 * math.pi * progress; // Progress amount
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
false, // Don't use center (would create a pie chart)
progressPaint,
);
}
@override
bool shouldRepaint(CircularProgressPainter oldDelegate) {
// Repaint if progress or color changes
return oldDelegate.progress != progress ||
oldDelegate.color != color;
}
}
// Usage in a widget
class CustomProgressIndicator extends StatelessWidget {
final double progress;
const CustomProgressIndicator({required this.progress});
@override
Widget build(BuildContext context) {
return CustomPaint(
size: const Size(100, 100),
painter: CircularProgressPainter(
progress: progress,
color: Colors.blue,
),
);
}
}
// Example usage
CustomProgressIndicator(progress: 0.65) // 65% progress
Key Methods to Know
Drawing Shapes:
canvas.drawCircle(center, radius, paint);
canvas.drawRect(rect, paint);
canvas.drawLine(p1, p2, paint);
canvas.drawPath(path, paint);
Paint Styles:
paint.style = PaintingStyle.fill; // Filled shape
paint.style = PaintingStyle.stroke; // Outline only
paint.strokeWidth = 4.0;
paint.strokeCap = StrokeCap.round; // Round line ends
Complex Paths:
final path = Path()
..moveTo(x1, y1)
..lineTo(x2, y2)
..quadraticBezierTo(cx, cy, x3, y3)
..close();
canvas.drawPath(path, paint);
Try It Yourself
Create a custom graph widget that draws a simple bar chart:
- Create a
BarChartPainterthat takes a list of values - Draw rectangles for each bar, scaled to fit the canvas
- Use different colors for each bar
- Add
shouldRepaintlogic that checks if the data changed
Bonus challenge: Add animation by wrapping your CustomPaint in an AnimatedBuilder and animating the bar heights from 0 to their target values.
Tip of the Day
Performance matters with CustomPaint. The shouldRepaint method is crucial—returning true unnecessarily causes expensive repaints. Only repaint when visual properties actually change.
For complex drawings, consider using CustomPainter’s hitTest method to make your custom drawings interactive and respond to tap/click events.
When debugging custom painters, wrap your CustomPaint in a RepaintBoundary to isolate repaints and use the Flutter DevTools performance overlay to spot unnecessary repaints.