How to create a simple progress gauge using Flutter CustomPaint: Part 1

In Flutter, the CustomPaint widget is a powerful and flexible widget that allows you to create custom graphics and perform custom paintings on the screen. It gives you complete control over the appearance of the widget by letting you define how to paint its contents.

If you want to create a progress gauge, there are several packages out there you can use. However, most of the time you only need one type of progress gauge, while the packages include more. Thus, unused code will be included in your app. For that reason, creating the progress gauge from scratch would be a good option. It also grants you more flexibility to customize your widget.

This article will show you how to create a simple progress gauge using Flutter custom painter.

What is a CustomPaint

In Flutter, the CustomPaint widget is a powerful and flexible widget that allows you to create custom graphics and perform custom paintings on the screen. It gives you complete control over the widget's appearance by letting you define how to paint its contents.

To use the CustomPaint widget, you need to provide a CustomPainter object that defines the painting behavior. The CustomPainter class is an abstract class that you can extend to create your own custom painter.

Now, let's start to build the custom progress gauge.

Preparation

First, let's create a new project called my_simple_gauge (or up to you). Run this command on the terminal:

flutter create my_simple_gauge
terminal

Let's change the main.dart to be like this:

import 'package:flutter/material.dart';
import 'package:my_simple_gauge/custom_progress_gauge.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('My Simple Gauge'),
        ),
        body: const CustomProgressGauge(progress: 80),
      ),
    );
  }
}
main.dart

As you can see, in the code above, the body contains the CustomProgressGauge widget. Let's define the widget:

import 'package:flutter/material.dart';
import 'package:my_simple_gauge/custom_progress_painter.dart';

class CustomProgressGauge extends StatelessWidget {
  const CustomProgressGauge({super.key, this.progress = 0});
  final num progress;

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: CustomProgressPainter(progress),
      child: Container(
        height: 450,
      ),
    );
  }
}
custom_progress_gauge.dart

The CustomPaint widget receives two named parameters, painter, and child. We need to provide a class that extends CustomPainter as a parameter for the painter parameter. The child parameter value is the widget that will be displayed on top of the custom painting.

By default, the CustomPaint will take the size of its child. If it's not defined, the size parameter should be defined. In this case, we defined a child widget Container and set the height 450.

Creating the CustomProgressPainter class

As mentioned above, we need to provide a class that extends the CustomPainter class as a painter parameter. Let's create one!

class CustomProgressPainter extends CustomPainter {
  CustomProgressPainter(this.progress);
  num progress;
  
  @override
  void paint(Canvas canvas, Size size) {
    // Define your custom painting logic here
    // Use the provided canvas object to paint on the screen
  }

  @override
  bool shouldRepaint(MyCustomPainter oldDelegate) {
    // Return true if the painter needs to repaint
    // This is typically based on some condition or if the data that affects the painting has changed
    return false; // Set it to true if you always want to repaint
  }
}
custom_progress_painter.dart

‌As you can see, the class overrides two methods paint and shouldRepaint . The paint method is the place to draw our progress gauge using the provided canvas object. The shouldRepaint method should return true if we want the painter to repaint, or false if otherwise. It is called every time the class is instantiated.

In the code above, just like the CustomProgressGauge class, we make the class to accept one parameter, progress, which will indicate the progress of our gauge.

Working with the canvas object.

To draw a progress gauge, we need to create a paint object, then draw the gauge on the canvas object. Think of it like pen and paper, where the Paint object is the pen and the canvas object is the paper.

To create the Paint object, let's add the following code to the paint method:

  @override
  void paint(Canvas canvas, Size size) {
    final strokeWidth = size.width / 30.0;

    final paint = Paint()
      ..color = Colors.black12
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;
 }
custom_progress_painter.dart

The Paint object we created above describes the features of our pen. Using the color property, we set the color of the ink. The strokeWidth property set the thickness of our paint. In this case, it is the canvas width divided by 30.0 (you can change it to any number that you think is suitable).

The style property defines how the drawing looks. There are two options PaintingStyle.stroke and PaintingStyle.fill . If you want to know the difference, see the illustration below :

In this case, we want to use PaintingStyle.stroke, it because we don't need any fill inside our shape.

Let's move to another property strokeCap . The strokeCap property is used to make the tip of a line rounded, square, or butt. Again, let's see the difference using the illustration below:

How different stroke cap looks in Flutter

Notice that StrokeCap.square and StrokeCap.butt look similar. The difference is, the StrokeCap.butt extends the past end of the line by half of the stroke width. Anyway, in this case, we will use StrokeCap.round for all parts of the article.

Drawing the Arcs: The Background Arc

Our custom progress gauge consists of two arcs, the background arc which displays the maximum value of our progress gauge, and the foreground arc which displays the current progress. Here is the updated paint function.

  @override
  void paint(Canvas canvas, Size size) {
  
    final strokeWidth = size.width / 30.0;
    final circleCenter = Offset(size.width / 2, size.height / 4);
    final circleRadius = (size.width - strokeWidth) / 2.5;

    // Background Arc
    
    final paint = Paint()
      ..color = Colors.black12
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;

    double startAngle = pi / 1.5;
    double sweepAngle = pi / 0.6;

    canvas.drawArc(Rect.fromCircle(center: circleCenter, radius: circleRadius),
        startAngle, sweepAngle, false, paint);
 }
custom_progress_painter.dart

In the code above we draw the Arc into the canvas using a drawArc function. The function requires some parameters including:

  1. a Rect object: is used to define the bounding rectangle within which the arc is drawn. The Rect specifies the size and position of the oval that contains the arc.
  2. startAngle : The starting angle of the arc in radians, where 0 represents the rightmost point of the oval and positive angles rotate the arc counterclockwise.
  3. sweepAngle : The angular length of the arc in radians. A positive value creates a counterclockwise arc, while a negative value creates a clockwise arc.
  4. useCenter : A boolean value that determines whether to connect the arc to the center of the oval (true) or leave it as an open arc (false).
  5. The Paint object that we created before.

Here is how the widget looks so far:

You can read this article for more detail on how to create an arc with CustomPaint.

Drawing the Arcs: The Foreground Arc

The foreground arc is simply the same arc as the background arc, placed in front of the background color. The difference is, we use different colors as the background arc and also make the sweepAngle value to be percentage x background arc sweep angle. Add the following code below the background arc.

  num progress;
  
  @override
  void paint(Canvas canvas, Size size) {
  	........
    
    // Foreground Arc
    
    final arcPaint = Paint()
      ..color = Colors.blue
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;

    canvas.drawArc(Rect.fromCircle(center: circleCenter, radius: circleRadius),
        startAngle, (progress / 100) * sweepAngle, false, arcPaint);
  }
custom_progress_painter.dart

As you can see in the code above, the sweepAngle of the foreground arc is (percentage / 100) x sweepAngle. If you run the code, you will see the foreground arc like so:

Adding text to the center of the gauge.

Now, let's do some finishing touch, we will add the percentage text to the center of the gauge. Add a centered text to the CustomPaint widget that we've created before.

CustomPaint(
      painter: CustomProgressPainter(progress),
      child: const SizedBox(
        height: 450,
        child: Center(
          child: Text("$progress%",
              style: TextStyle(
                  color: Colors.black,
                  fontSize: 36,
                  fontWeight: FontWeight.bold)),
        ),
      ),
    )

In the CustomPaint widget above, we can remove the Container and use SizedBox instead,  since we only have height property in it. We use the Center widget to center the text in it. Here is what the gauge looks like now:

That's it. We have created our progress gauge. You can see this repository for the complete code: https://github.com/ediasep/flutter-simple-gauge.

In the next part, we will learn how to add custom animation to it. Stay tuned!