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:

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:

  1. Create a BarChartPainter that takes a list of values
  2. Draw rectangles for each bar, scaled to fit the canvas
  3. Use different colors for each bar
  4. Add shouldRepaint logic 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.