Learning about CustomPaint in Flutter
Tags: flutter, ui, beginners, learning
Published: 2024-06-23 23:09:54

Something like a week ago I started to research about "how I can manually draw things in the Flutter canvas" and found information about `CustomPaint` and `CustomPainter`, which seems to be the main Flutter mechanism to allow devs draw specific things.

I'm not sure if there're more options or alternatives to achieve that, so for now let's just talk about my experience with Flutter's `CustomPaint` widget.

## The Flutter art workshop, an analog perspective

Since the very first moment I started to learn Flutter, I inmediately noticed that it is, essentially, just an OpenGL canvas with a bunch of abstractions. I personally don't like that, not because of the abstractions themselves, but because using an OpenGL canvas doesn't feel... Native... Like, it's really easy to notice that.

And you may think: "Well, yeah, but if you think about that, every user interface is just an intangible abstraction, so... Who cares?". And yeah, you're right and I also agree with that, it's just a personal opinion and I've been learning how that approach for making user interfaces brings some benefits.

With that in mind, I ended up to the conclusion that Flutter it's likely an art workshop: it gives you canvas and a bunch of different brushes and tools to draw interfaces in a fast, simple and easy way. With this perspective, using an OpenGL canvas makes much more sense and helps bit to understand how `CustomPaint` works and it's more easy to explain it.

## Alright, let's move on a bit more

So, you are working on a Flutter project and there's something specific you want to do, but Flutter doesn't have a widget for that and there's only one solution: create it by yourself. Well, the way to do that is by using `CustomPaint`, a widget that gives you direct access to the canvas, allowing you to draw things, but with one requirement: you need to give it a `CustomPainter`, which is basically Flutter telling you "Fine, you can draw here, but you must give an specialized artist for this very specific job!".

## Creating a `CustomPainter`

At this point, you may be wondering what's a `CustomPainter` and how to create them, because if you try giving a literal `CustomPainter` to a `CustomPaint`, you'll get an error. That's because `CustomPainter` is an abstract class and we have to create a class that `extends` from `CustomPainter` instead. So, let's make it:

```dart
final class WavePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}
```

Now, let me explain this:
 - Our class is named `WavePainter` because we're going to make a simple audio waveform widget (as in the banner of this article)
 - All classes that `extends` from `CustomPainter` must override 2 methods:
   - `paint`: this one is where we're going to draw
   - `shouldRepaint`: and this one handles if `paint` should be called again

Pretty simple, right? The `paint` methods recieves a `Canvas` and a `Size` object for the drawing stuff and `shouldRepaint` recieves an old version of our `CustomPainter` to check for changes and, if so, return true to tell Flutter to re`paint` the widget, but we usually do this only if our widget has mutable data that is expected to change during the lifetime of our app.

## Starting to draw

Alright, so our `CustomPainter` looks good so far, we can already give it to a `CustomPaint`, however, it will not draw anything because we haven't code what to draw yet, so let's start by creating a _brush_ for our drawing:

```dart
final class WavePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // The Paint class is essentially a brush, so...
    final brush = Paint()
      // We set the color,
      ..color = Color(0xFFFAFAFA)
      // the thickness of the line
      ..strokeWidth = 3
      // and how we want them to end
      ..strokeCap = StrokeCap.round;
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}
```

At this point, you should check out the [API reference for the Paint](https://api.flutter.dev/flutter/dart-ui/Paint-class.html) class to know what else you can do with it. Now, our brush is ready to use, so it's time to finish our drawing:

```dart
final class WavePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final brush = Paint()
      ..color = Colors.white
      ..strokeWidth = 3
      ..strokeCap = StrokeCap.round;

    var shift = 0.0;
    final verticalCenter = size.height / 2;
    final values = List<double>.generate(100, (_) {
      return Random().nextDouble() * verticalCenter;
    });

    for (var i = 0; i < values.length && shift < size.width; i++) {
      canvas.drawLine(
        Offset(shift, verticalCenter - values[i]),
        Offset(shift, verticalCenter + values[i]),
        brush
      );

      shift += 6;
    }
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}
```

If we put that inside of a `CustomPaint`, we should get something like this:

![Figure 1: preview of the final result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i7admi4xkf213jz4d2p8.png)

## Understanding what's going on

Now, again, I'm gonna explain these changes a bit more, but first, we need to understand how exactly positioning works in this case, which is pretty simple because we can understand it by just looking at this image:

![Figure 2: how positioning works on the given canvas](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/91b4qor2j5223vszv53m.jpg)

As you can see, we start drawing at the top left corner of the given canvas, so if we don't want to draw outside of it, we should use positive numbers. That said, we will want to find the vertical center of that canvas and that's exactly what we did with `size.height / 2`, we're gonna use that number for both: positioning our bars and limit it's size.

By default, all custom paints gets a canvas of 300x300, but we can change that by putting our custom paint into another widget that could help to set a different size. In this case, I created a canvas of 400x100 by putting my custom paint inside of a `Container`, so our vertical center is 50 (unless we change the height of the `Container`).

That said, how we're gonna use that number as the limit of our bars? Well, that's pretty simple and we already did it in this line:

```dart
return Random().nextDouble() * verticalCenter;
```

You see, since `nextDouble` will always return a number between 0.0 and 1.0, mutiplying that result by the vertical center will never exceed half the height of the canvas, so it's perfectly fine to do that. Now this section in the code we wrote makes more sense:

```dart
canvas.drawLine(
  Offset(shift, verticalCenter - values[i]),
  Offset(shift, verticalCenter + values[i]),
  brush
);
```

`drawLine` requires an starting point and an end point for drawing lines, so we're doing this:

![Figure 4: helper for vertical positioning](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ftcg183wlwm48kbymgpt.jpg)

Pretty simple, right? Now, you might be thinking: "What about `shift`? What it does?". Well, `shift` is even more easy: it controls the horizontal position of our bars. Since we're not only putting them side by side, but also leaving a small space between them, we set `shift` to the width of the line (3) + space we want to have between bars (also 3) and we just keep incrementing it after every use.

Finally, to prevent our drawing to go out of the size of the given canvas, we put an additional condition to our `for` loop: `shift < size.width`. With that simple condition, no matter the width of the given canvas or how much data we have.

## Final words

To be honest, learning how to do this was a really cool thing, I enjoyed the process and had a fun moment so far, despite I wasn't able to implement it in the project I'm working on.

Here is the full code for this article: https://gist.github.com/Miqueas/b66297d8de4a29000e9cb4d3f9cdc3f5

Anyways, I hope you enjoyed this too, feel free to share, have a nice day and see you another day 👋.