Drawing Shapes, Text, and Images on the Canvas in Flutter

Flutter’s Canvas provides a powerful interface for drawing custom graphics, text, and images directly onto the screen. The Canvas is the core of Flutter’s custom painting system, allowing developers to create unique and dynamic UIs that go beyond standard widgets. This comprehensive guide will explore how to use the Canvas in Flutter to draw shapes, text, and images, along with practical code examples.

What is Flutter’s Canvas?

The Canvas in Flutter is a component that represents a surface upon which you can draw. It provides a set of methods for drawing primitive shapes, text, and images. These drawings are executed in a specific Paint style, determining how they appear on the screen.

Why Use the Canvas?

  • Custom UI Design: Create unique UI elements beyond standard Flutter widgets.
  • Graphics and Animations: Build custom animations and dynamic graphics.
  • Control and Precision: Precisely control the appearance and behavior of graphical elements.

Setting Up a Custom Painter

To use the Canvas, you typically create a custom painter by extending the CustomPainter class. This class provides a paint method where you perform all your drawing operations.

Step 1: Create a Custom Painter Class

Extend the CustomPainter class and override the paint method:


import 'package:flutter/material.dart';

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

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false; // Return true if the painter needs to be repainted
  }
}

In the paint method, you’ll use the canvas and size parameters to perform your drawings. The shouldRepaint method determines whether the painter needs to be repainted when the widget is rebuilt.

Step 2: Use the Custom Painter in a Widget

Use the CustomPaint widget to incorporate your custom painter into your UI:


import 'package:flutter/material.dart';

class MyCanvasWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: MyCustomPainter(),
      size: Size(300, 300), // Set the size of the canvas
    );
  }
}

Drawing Shapes on the Canvas

The Canvas class offers several methods for drawing various shapes, including rectangles, circles, lines, and more.

Drawing a Rectangle

Use the drawRect method to draw a rectangle:


import 'package:flutter/material.dart';

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

    final rect = Rect.fromPoints(
      Offset(0, 0), // Top-left corner
      Offset(100, 100), // Bottom-right corner
    );

    canvas.drawRect(rect, paint);
  }

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

In this example:

  • A Paint object is created to define the color and style of the rectangle.
  • A Rect object is created using two Offset points, representing the top-left and bottom-right corners of the rectangle.
  • The drawRect method is called to draw the rectangle on the canvas using the specified Rect and Paint objects.

Drawing a Circle

Use the drawCircle method to draw a circle:


import 'package:flutter/material.dart';

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

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

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

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

Here:

  • The center of the circle is calculated based on the size of the canvas.
  • The drawCircle method is called with the center Offset, the radius, and the Paint object.

Drawing a Line

Use the drawLine method to draw a line:


import 'package:flutter/material.dart';

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

    final startPoint = Offset(0, 0);
    final endPoint = Offset(size.width, size.height);

    canvas.drawLine(startPoint, endPoint, paint);
  }

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

In this code:

  • The Paint object is configured with a strokeWidth to define the thickness of the line and a PaintingStyle.stroke to specify that the line should be drawn as an outline.
  • The drawLine method is called with the start and end Offset points and the Paint object.

Drawing Text on the Canvas

The Canvas class also allows you to draw text using the drawParagraph method.


import 'package:flutter/material.dart';
import 'dart:ui' as ui;

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

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

    final paragraphBuilder = ui.ParagraphBuilder(ui.ParagraphStyle(
      textAlign: TextAlign.left,
    ))
      ..pushStyle(textStyle.getTextStyle(color: Colors.black))
      ..addText(textSpan.text!);

    final paragraph = paragraphBuilder.build()
      ..layout(ui.ParagraphConstraints(
        width: size.width,
      ));

    canvas.drawParagraph(paragraph, Offset(0, 0));
  }

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

Explanation:

  • Create a TextStyle to define the appearance of the text (color, font size, etc.).
  • Create a TextSpan containing the text and the text style.
  • Use ui.ParagraphBuilder to create a paragraph with the text span and layout constraints.
  • Call canvas.drawParagraph to draw the text on the canvas.

Drawing Images on the Canvas

To draw images on the Canvas, you need to load the image first and then use the drawImage or drawImageRect methods.

Loading an Image

First, load an image using Image.asset or Image.network:


import 'package:flutter/material.dart';
import 'dart:ui' as ui;

class MyCustomPainter extends CustomPainter {
  ui.Image? image;

  MyCustomPainter(this.image);

  @override
  void paint(Canvas canvas, Size size) {
    if (image != null) {
      canvas.drawImage(image!, Offset(0, 0), Paint());
    }
  }

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

class MyCanvasWidget extends StatefulWidget {
  @override
  _MyCanvasWidgetState createState() => _MyCanvasWidgetState();
}

class _MyCanvasWidgetState extends State {
  ui.Image? image;

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

  Future _loadImage() async {
    final ByteData data = await rootBundle.load('assets/my_image.png'); // Replace with your image path
    final ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
    final ui.FrameInfo frameInfo = await codec.getNextFrame();
    setState(() {
      image = frameInfo.image;
    });
  }

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: MyCustomPainter(image),
      size: Size(300, 300),
    );
  }
}

In this example:

  • The image is loaded in the initState method of a stateful widget.
  • The loaded image is then passed to the MyCustomPainter.

Drawing the Image

Use the drawImage method to draw the entire image on the canvas:


canvas.drawImage(image!, Offset(0, 0), Paint());

Alternatively, use drawImageRect to draw a specific portion of the image:


final srcRect = Rect.fromLTWH(0, 0, 100, 100); // Source rectangle
final destRect = Rect.fromLTWH(0, 0, 200, 200); // Destination rectangle
canvas.drawImageRect(image!, srcRect, destRect, Paint());

In this example:

  • srcRect defines the portion of the image to be drawn.
  • destRect defines the area on the canvas where the image will be drawn, allowing you to scale and position the image.

Best Practices

  • Optimize Repaints: Implement shouldRepaint efficiently to minimize unnecessary redraws.
  • Use Layers: For complex drawings, consider using layers to improve performance.
  • Cache Resources: Load and cache images and other resources to avoid redundant loading.
  • Consider Performance: Complex drawings can be resource-intensive, so profile your code and optimize where necessary.

Conclusion

Flutter’s Canvas provides a powerful way to create custom UIs by drawing shapes, text, and images directly on the screen. By extending the CustomPainter class and utilizing the drawing methods, developers can craft unique and dynamic interfaces. Properly utilizing the Canvas can greatly enhance the visual appeal and user experience of your Flutter applications. Remember to optimize performance to ensure smooth and responsive UIs.