Back to Posts

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

  1. 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;
  }
}
  1. 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.