Custom Painting in Flutter: Draw Your Own Widgets
Custom Painting in Flutter: Draw Your Own Widgets
What You’ll Learn
Learn how to use CustomPaint and CustomPainter to create custom graphics and visual elements beyond Flutter’s built-in widgets—perfect for charts, diagrams, and unique UI elements in desktop apps.
Why Custom Painting Matters
Sometimes standard Flutter widgets aren’t enough. Maybe you need a custom progress indicator, a hand-drawn chart, or a unique shape. That’s where CustomPaint comes in. It gives you direct access to the canvas to draw exactly what you need.
The Basics
Custom painting involves two key components:
- CustomPaint: A widget that provides a canvas
- CustomPainter: A class where you define what to draw
Example: Drawing a Simple Progress Arc
Let’s create a circular progress indicator with a custom style:
import 'package:flutter/material.dart';
import 'dart:math';
class ArcProgressPainter extends CustomPainter {
final double progress; // 0.0 to 1.0
final Color color;
ArcProgressPainter({required this.progress, required this.color});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = min(size.width, size.height) / 2;
// Background circle
final bgPaint = Paint()
..color = Colors.grey.shade200
..style = PaintingStyle.stroke
..strokeWidth = 8;
canvas.drawCircle(center, radius, bgPaint);
// Progress arc
final progressPaint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 8
..strokeCap = StrokeCap.round;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
-pi / 2, // Start at top
2 * pi * progress, // Sweep angle
false,
progressPaint,
);
}
@override
bool shouldRepaint(ArcProgressPainter oldDelegate) {
return oldDelegate.progress != progress || oldDelegate.color != color;
}
}
// Usage
class ProgressWidget extends StatelessWidget {
final double progress;
const ProgressWidget({Key? key, required this.progress}) : super(key: key);
@override
Widget build(BuildContext context) {
return CustomPaint(
size: const Size(100, 100),
painter: ArcProgressPainter(
progress: progress,
color: Colors.blue,
),
);
}
}
Key Methods Explained
- paint(): This is where the drawing happens. You get a
CanvasandSizeto work with. - shouldRepaint(): Return
truewhen your painter needs to redraw. This optimizes performance.
Common Canvas Methods
canvas.drawCircle(center, radius, paint);
canvas.drawRect(rect, paint);
canvas.drawLine(p1, p2, paint);
canvas.drawPath(path, paint);
canvas.drawArc(rect, startAngle, sweepAngle, useCenter, paint);
Try It Yourself
Enhance the progress indicator:
- Add a percentage text in the center using
TextPainter - Animate the progress from 0 to 1 using
AnimationController - Make it respond to mouse hover on desktop (change color/size)
Hint for text:
final textPainter = TextPainter(
text: TextSpan(text: '${(progress * 100).toInt()}%'),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(canvas, /* position */);
Tip of the Day
When working with CustomPaint, use RepaintBoundary to isolate expensive paint operations and improve performance. Wrap your CustomPaint widget:
RepaintBoundary(
child: CustomPaint(painter: MyPainter()),
)
This prevents unnecessary repaints of other parts of your widget tree when only your custom painting changes—especially valuable for desktop apps with complex UIs!