Working with Canvas for Drawing Custom Graphics in Flutter

Flutter provides a powerful and flexible framework for building cross-platform applications, and one of its standout features is the CustomPaint widget, which allows developers to create and render custom graphics using a Canvas. Mastering the use of Canvas opens up a world of possibilities for creating unique UI elements, interactive charts, and complex animations. This blog post will explore how to effectively work with Canvas in Flutter, providing comprehensive examples and best practices to elevate your Flutter development skills.

What is the Canvas in Flutter?

In Flutter, the Canvas is a surface that you can draw on, similar to a real-world canvas. It provides a variety of methods for drawing shapes, text, images, and paths. The CustomPaint widget is used to integrate custom drawing into the Flutter UI.

Why Use the Canvas?

  • Custom UI Elements: Create unique and tailored UI components that aren’t available out-of-the-box.
  • Interactive Graphics: Implement interactive charts, graphs, and visual elements.
  • Complex Animations: Animate custom shapes and paths to create engaging visual effects.
  • Performance Optimization: Achieve optimal rendering performance for custom graphics by controlling exactly how pixels are drawn.

How to Use Canvas in Flutter

Using the Canvas in Flutter involves a few key steps:

Step 1: Create a CustomPainter Class

First, you need to create a class that extends CustomPainter. This class will handle the drawing logic.

import 'package:flutter/material.dart';

class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // Drawing logic goes here
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}
  • paint: This method is where you write the code to draw on the canvas.
  • shouldRepaint: This method determines whether the painter needs to be redrawn. Return true if the painter’s appearance depends on external state that has changed; otherwise, return false.

Step 2: Use the CustomPaint Widget

Next, use the CustomPaint widget in your UI, providing an instance of your CustomPainter class.

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: MyPainter(),
      child: Container(), // Your widget content
    );
  }
}

The child of the CustomPaint widget is optional. It can be any widget that you want to draw over or under the custom paint.

Drawing Basic Shapes

Here’s how to draw basic shapes using the Canvas:

Drawing a Circle

class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.fill;

    final center = Offset(size.width / 2, size.height / 2);
    final radius = size.width / 4;

    canvas.drawCircle(center, radius, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

Drawing a Rectangle

class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = 5;

    final rect = Rect.fromLTWH(
      size.width / 4,
      size.height / 4,
      size.width / 2,
      size.height / 2,
    );

    canvas.drawRect(rect, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

Drawing a Line

class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.green
      ..strokeWidth = 5;

    final start = Offset(0, 0);
    final end = Offset(size.width, size.height);

    canvas.drawLine(start, end, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

Drawing Paths

Drawing paths allows you to create complex shapes by combining multiple lines and curves.

class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.purple
      ..style = PaintingStyle.stroke
      ..strokeWidth = 5;

    final path = Path();
    path.moveTo(size.width / 2, size.height / 4); // Starting point
    path.quadraticBezierTo(
      size.width / 4,
      size.height / 2,
      size.width / 2,
      size.height * 3 / 4,
    ); // Curve to the middle-bottom
    path.quadraticBezierTo(
      size.width * 3 / 4,
      size.height / 2,
      size.width / 2,
      size.height / 4,
    ); // Curve back to the starting point
    path.close();

    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

Explanation:

  • path.moveTo(x, y): Sets the starting point of the path.
  • path.quadraticBezierTo(x1, y1, x2, y2): Adds a quadratic Bezier curve from the current point to (x2, y2), using (x1, y1) as the control point.
  • path.close(): Closes the path by drawing a line back to the starting point.

Drawing Text

You can also draw text on the canvas using a TextPainter.

import 'dart:ui' as ui; // Import dart:ui to use TextStyle

class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final textStyle = ui.TextStyle(
      color: Colors.black,
      fontSize: 24,
    );

    final textSpan = ui.TextSpan(
      text: 'Hello, Canvas!',
      style: textStyle,
    );

    final textPainter = TextPainter(
      text: textSpan,
      textAlign: TextAlign.center,
      textDirection: TextDirection.ltr,
    );

    textPainter.layout(
      minWidth: 0,
      maxWidth: size.width,
    );

    final x = size.width / 2 - textPainter.width / 2;
    final y = size.height / 2 - textPainter.height / 2;

    textPainter.paint(canvas, Offset(x, y));
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

Drawing Images

Drawing images requires you to load the image first, then draw it on the canvas.

import 'dart:ui' as ui;

import 'package:flutter/services.dart';

class MyPainter extends CustomPainter {
  ui.Image? image;

  MyPainter({required this.image});

  @override
  void paint(Canvas canvas, Size size) {
    if (image != null) {
      final rect = Rect.fromLTWH(0, 0, size.width, size.height);
      canvas.drawImageRect(
        image!,
        Rect.fromLTWH(0, 0, image!.width.toDouble(), image!.height.toDouble()),
        rect,
        Paint(),
      );
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true; // Repaint when the image changes
  }
}

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State {
  ui.Image? image;

  @override
  void initState() {
    super.initState();
    _loadImage();
  }

  Future _loadImage() async {
    final data = await rootBundle.load('assets/my_image.png');
    final bytes = data.buffer.asUint8List();
    final image = await decodeImageFromList(bytes);
    setState(() {
      this.image = image;
    });
  }

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: MyPainter(image: image),
      child: Container(),
    );
  }
}

Key points:

  • Load the image using rootBundle.load and decodeImageFromList.
  • Draw the image using canvas.drawImageRect.

Advanced Techniques

Beyond basic shapes, paths, text and images, the Canvas offers a variety of advanced drawing techniques that you can use in Flutter.

Transforms

The Canvas offers various transform functions that can manipulate the coordinate space. These transforms include:

  • translate(double dx, double dy)
  • rotate(double radians)
  • scale(double sx, [double sy])
  • transform(Float64List matrix4)

An example use case is:


@override
void paint(Canvas canvas, Size size) {
    // Save the current canvas state
    canvas.save();
    
    // Translate to the center of the canvas
    canvas.translate(size.width / 2, size.height / 2);

    // Rotate the canvas by 45 degrees
    canvas.rotate(45 * Math.pi / 180);

    // Draw a rectangle
    Rect rect = Rect.fromPoints(Offset(-50, -50), Offset(50, 50));
    canvas.drawRect(rect, Paint()..color = Colors.blue);

    // Restore the canvas to its original state
    canvas.restore();
}

This code would save the default canvas state, apply a translation to move the origin to the center, rotate the canvas by 45 degrees, and then draw a blue square. Afterward, it restores the canvas state to its previous setting.

Clipping

Clipping in Flutter Canvas is an operation that restricts drawing to a certain region. Anything outside this region is not painted. Flutter provides different methods to define clipping regions:

  • clipRect: Creates a rectangular clipping region.
  • clipRRect: Creates a rounded rectangular clipping region.
  • clipPath: Creates a clipping region based on a path.

Here’s an example that makes use of clipping with canvas:


class ClippingPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // Define a rectangular clip
    final Rect rect = Rect.fromLTRB(size.width * 0.1, size.height * 0.1, size.width * 0.9, size.height * 0.9);
    
    // Clip the canvas to the defined rectangle
    canvas.clipRect(rect);
    
    // Draw a background that covers the entire canvas
    canvas.drawColor(Colors.yellow, BlendMode.src);
    
    // Draw a circle (it will only be visible within the clipped region)
    final paint = Paint()..color = Colors.red;
    canvas.drawCircle(Offset(size.width / 2, size.height / 2), size.width / 3, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

Gradients

Gradients in Flutter allow you to fill shapes with a smooth transition between two or more colors. Gradients can be linear, radial, or sweep. The two most common are LinearGradient and RadialGradient


import 'package:flutter/material.dart';
import 'dart:math' as Math;

class GradientPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // Define a linear gradient
    final LinearGradient gradient = LinearGradient(
      begin: Alignment.topLeft,
      end: Alignment.bottomRight,
      colors: [Colors.red, Colors.blue],
    );

    // Create a Paint object and assign the shader
    final Paint paint = Paint()
      ..shader = gradient.createShader(Rect.fromLTRB(0, 0, size.width, size.height));

    // Draw a rectangle that covers the entire canvas with the gradient
    canvas.drawRect(Rect.fromLTRB(0, 0, size.width, size.height), paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

When painting a shape, instead of providing a flat color, the shader is defined using gradient from a LinearGradient that has starting point (Alignment.topLeft) of red color, and end (Alignment.bottomRight) blue.

Shadows and Blurs

You can also apply shadow and blur effects when using Canvas for Drawing Custom Graphics in Flutter. For more detailed techniques, this external reference is recommended: Shadows in Flutter Documentation.

Performance Considerations

  • Optimize shouldRepaint: Ensure that the shouldRepaint method returns true only when necessary. Unnecessary repaints can lead to poor performance.
  • Use RepaintBoundary: Wrap complex custom paints with a RepaintBoundary widget to isolate the repainting process. This prevents the entire screen from being redrawn when only a small part changes.
  • Cache Expensive Operations: Cache expensive calculations, images, and paths to avoid recomputing them on every frame.

Conclusion

Using the Canvas in Flutter provides immense flexibility for creating custom graphics and UI elements. By mastering basic shapes, paths, text, images, and advanced techniques like transforms, gradients, and shadows, you can create stunning visual effects and interactive experiences in your Flutter applications. Remember to optimize for performance to ensure smooth and efficient rendering.