Skip to main content

Command Palette

Search for a command to run...

Drawing Dot Leaders in Flutter

Updated
4 min read
Drawing Dot Leaders in Flutter

This is part of a series of articles I'm writing chronicling the build of an app using Flutter. I'm working with a great designer who has made an outstanding app design, but with that design he's managed to throw a bunch of wrenches of various sizes at me, which I've been figuring out how to fix one by one.

The problem...

... is that I need to draw dots between the beginning and end of a row. This row can be of any length, but dots need to be spaced evenly and to fill the full space between the start and finish:

The work...

The easiest way to do this is to use a custom painter. What we need to do is figure out where the dots should be drawn in relation to the width and height of the box given to the custom painter to paint. Because of the way that circles are drawn in Flutter, I need to calculate everything based on the centre of the points. For now, that looks like this:

final offset = radius + spacing + radius;
final vPos = size.height / 2;
var hPos = radius;

To do the actual drawing, I'll be subclassing CustomPainter and using a CustomPaint; if you haven't run across those yet I recommend taking a look at the documentation. To do the drawing, I will be using the canvas and size parameters passed to the paint function of the CustomPainter subclass.

I need to draw each of the points, then move to the next, while ensuring that I don't go past the right of the box given to us to draw in. I also create a Paint object loaded up with whichever colour I want to draw the dots with. For this implementation I've drawn the points from the left of the available space towards the right until the end of the available space. This is what the code to do that looks like:

final paint = Paint()..color = color;
do {
  canvas.drawCircle(Offset(hPos, vPos), radius, paint);
  hPos += offset;
} while (hPos + radius <= width);

This all needs to be set up in a CustomPainter, and then passed to a CustomPaint; here's the bare basic setup but made configurable:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomPaint(painter: DotsPainter())
  }
}

class DotsPainter extends CustomPainter {
  final double radius;
  final Color color;
  final double spacing;

  const DotsPainter({
    required this.radius,
    required this.color,
    required this.spacing,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()..color = color;
    final width = size.width;
    final vPos = size.height / 2;
    final offset = radius + spacing + radius;

    var hPos = radius;
    do {
      canvas.drawCircle(Offset(hPos, vPos), radius, paint);
      hPos += offset;
    } while (hPos + radius <= width);
  }

  @override
  bool shouldRepaint(covariant DotsPainter oldDelegate) =>
      oldDelegate.spacing != spacing || oldDelegate.color != color || oldDelegate.radius != radius;
}

The shouldRepaint function has an implementation that will ensure that any changes to the radius/colour/spacing ensures the painter will redraw. This does everything needed to draw the example given at the start with the correct colouring & sizing.

The end result...

For a full example, tap the icon below. You can open the code used to generate this preview, which includes some extra set-up code and hardcodes the colours & sizes used.

This solves the problem to my satisfaction and recreates the example shown at the start. I've made use of the configurable colouring & sizing to make the dots show up better for the sake of this example.

However, as I keep building out the app I've realized something - drawing the points from the left isn't always sufficient. For example, if you have a list of items each with a different length of text on the left, the dots will be drawn according to that size which will result in them not matching up vertically. Sigh. I'll write a post soon showing how I've fixed that!

Making Keepsta, a custom-designed flutter app

Part 1 of 1

In this series, I'm deep-diving into a bunch of the problems that came up while developing a very customized Flutter app.