Custom Widgets & Custom Painting

Canvas Primitives: Lines, Rectangles & Circles

16 min Lesson 4 of 12

Canvas Primitives: Lines, Rectangles & Circles

Every custom-painted Flutter widget begins with the same building blocks: the Canvas object and the Paint object. The Canvas exposes a set of drawing primitives — drawLine, drawRect, drawOval, and drawArc — that let you compose anything from simple separators to full chart widgets. In this lesson you will master each primitive and the Paint properties that control how they appear.

The Paint Object

Before calling any draw method you must configure a Paint instance. Paint carries all visual properties for a drawing operation. The most important properties are:

  • color — the color used for stroke or fill.
  • stylePaintingStyle.stroke draws an outline; PaintingStyle.fill floods the interior.
  • strokeWidth — thickness of the stroke in logical pixels (only meaningful when style is stroke).
  • isAntiAlias — smooths diagonal edges; defaults to true and should almost always remain so.
  • strokeCap — how line ends are rendered: StrokeCap.butt, round, or square.
Note: A Paint object is mutable, so you can reuse one instance by changing its properties between draw calls. For clarity, many developers create a separate Paint for each logical style (e.g. _fillPaint, _strokePaint).

drawLine — Straight Lines

canvas.drawLine(Offset p1, Offset p2, Paint paint) draws a straight segment from p1 to p2. The Paint must use PaintingStyle.stroke; fill has no effect on a one-dimensional line. Use strokeWidth to control thickness and strokeCap to control whether the endpoints are flat, rounded, or square-extended.

Drawing Lines with Different Caps

class LineDemoPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = const Color(0xFF1565C0)
      ..style = PaintingStyle.stroke
      ..strokeWidth = 6
      ..isAntiAlias = true;

    // Butt cap — line ends exactly at the point
    paint.strokeCap = StrokeCap.butt;
    canvas.drawLine(
      const Offset(20, 40),
      Offset(size.width - 20, 40),
      paint,
    );

    // Round cap — semicircle extends beyond the endpoint
    paint.strokeCap = StrokeCap.round;
    paint.color = const Color(0xFFE53935);
    canvas.drawLine(
      const Offset(20, 90),
      Offset(size.width - 20, 90),
      paint,
    );

    // Square cap — rectangle extends by half strokeWidth
    paint.strokeCap = StrokeCap.square;
    paint.color = const Color(0xFF2E7D32);
    canvas.drawLine(
      const Offset(20, 140),
      Offset(size.width - 20, 140),
      paint,
    );
  }

  @override
  bool shouldRepaint(covariant CustomPainter old) => false;
}

drawRect — Rectangles

canvas.drawRect(Rect rect, Paint paint) draws a rectangle defined by a Rect. Flutter provides several named constructors on Rect that are more expressive than raw coordinates:

  • Rect.fromLTWH(left, top, width, height) — position plus size.
  • Rect.fromLTRB(left, top, right, bottom) — two corners.
  • Rect.fromCenter(center: offset, width: w, height: h) — centred on a point.
Tip: When you need rounded corners, replace drawRect with drawRRect and pass an RRect.fromRectAndRadius. The API is identical except for the extra radius argument.

drawOval and drawCircle

canvas.drawOval(Rect rect, Paint paint) inscribes an ellipse inside the given Rect. When the rect is a square the result is a perfect circle. For convenience Flutter also provides canvas.drawCircle(Offset center, double radius, Paint paint) which avoids constructing a Rect entirely.

drawArc — Arcs and Pie Slices

canvas.drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint) draws an arc inscribed in rect. Angles are in radians (not degrees). The zero-radian position is the 3 o'clock position on the circle. When useCenter is true, lines are drawn from the arc endpoints to the center — creating a pie-slice shape.

Rectangles, Circles, and an Arc in One Painter

import 'dart:math' as math;

class ShapesDemoPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final fillPaint = Paint()
      ..style = PaintingStyle.fill
      ..isAntiAlias = true;

    final strokePaint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 3
      ..isAntiAlias = true;

    // Filled rectangle
    fillPaint.color = const Color(0xFFBBDEFB);
    strokePaint.color = const Color(0xFF1565C0);
    final rect = Rect.fromLTWH(20, 20, 120, 80);
    canvas.drawRect(rect, fillPaint);
    canvas.drawRect(rect, strokePaint);

    // Filled circle
    fillPaint.color = const Color(0xFFC8E6C9);
    strokePaint.color = const Color(0xFF2E7D32);
    canvas.drawCircle(
      Offset(size.width / 2, 60),
      45,
      fillPaint,
    );
    canvas.drawCircle(
      Offset(size.width / 2, 60),
      45,
      strokePaint,
    );

    // Pie-slice arc (90 degrees = pi/2 radians)
    fillPaint.color = const Color(0xFFFFCDD2);
    strokePaint.color = const Color(0xFFB71C1C);
    final arcRect = Rect.fromCenter(
      center: Offset(size.width - 80, 60),
      width: 90,
      height: 90,
    );
    canvas.drawArc(arcRect, 0, math.pi / 2, true, fillPaint);
    canvas.drawArc(arcRect, 0, math.pi / 2, true, strokePaint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter old) => false;
}

Coordinate System and Size

The Canvas coordinate system has its origin (0, 0) at the top-left corner of the widget. The x-axis increases to the right; the y-axis increases downward. The size parameter passed to paint() is the logical pixel size of the widget as determined by the layout system. Always use size.width and size.height instead of hard-coded numbers so your painter adapts to any screen.

Warning: Painting outside the [0,0] — [size.width, size.height] bounds is technically allowed but will visually overflow the widget boundary. Clipping can be applied with canvas.clipRect to enforce containment.

Combining Fill and Stroke

A single draw call only applies one Paint. To render both a filled interior and a visible border you must call the draw method twice — once with a fill Paint and once with a stroke Paint — for the same geometry. Draw the fill first so the stroke appears on top.

Summary

This lesson covered the foundational Canvas primitives:

  • drawLine — straight segments; controlled by strokeWidth and strokeCap.
  • drawRect — rectangles defined by a Rect; supports both fill and stroke.
  • drawOval / drawCircle — ellipses and circles.
  • drawArc — arcs with optional pie-slice lines to center; angles in radians.
  • Paint — the single object controlling color, style, strokeWidth, isAntiAlias, and strokeCap for every draw call.

In the next lesson you will learn how to apply transformations (translate, rotate, scale) to the Canvas to build more complex compositions without manually computing every coordinate.