Creating a Custom Progress Indicator in Flutter

Aniket Raj
7 min readAug 1, 2023

--

Hey there! Welcome to this exciting tutorial where we’ll dive into creating a captivating Custom Progress Indicator from scratch.

To accomplish this, we’ll be harnessing the powerful capabilities of CustomPaint Widget. The CustomPaint widget in Flutter enables you to create custom drawings and visuals by using a CustomPainter class. For more details about the CustomPaint refer here.

Our journey to create this Custom Progress Indicator will consist of two key parts:

1. Creating a Liquid Progress Indicator

In this section, we will be making liquid-like animation so let’s start.

Creating a LiquidPainter subclass

To start building the Liquid Progress Indicator, we’ll create a CustomPainter subclass named LiquidPainter. This class will extend CustomPainter and will be responsible for drawing and animating our indicator. Let's take a closer look at the LiquidPainter class:

The LiquidPainter subclass takes 2 parameters value

  1. value: current progress value
  2. maxValue: The maximum value the progress can reach.

Now, let’s see the calculations made within this class:

We are calculating the diameter and radius based on the shortest side with the help of the size parameter.

double diameter = min(size.height, size.width);
double radius = diameter / 2;

// Defining coordinate points. The wave starts from the bottom and ends at the top as the value changes.
double pointX = 0;
double pointY = diameter - ((diameter + 10) * (value / maxValue)); // 10 is an extra offset added to fill the circle completely

Path path = Path();
path.moveTo(pointX, pointY);

We are defining pointX which points to zero initially and pointY which depends on the value, pointY will start from the bottommost (pointY = diameter) and will end at the topmost, attaining zero as shown in the below graphics.

Now, we are going to add a wave-like effect as shown in the below graphics. To achieve a liquid-like animation, we are going to plot the sine wave-like path. We will use the equation y = A * sin(ωt + φ) + C.

Here’s how we have used the formula to trace the path with the help of path.lineTo:

    // Amplitude: the height of the sine wave
double amplitude = 10;

// Period: the time taken to complete one full cycle of the sine wave.
// f = 1/p, the more the value of the period, the higher the frequency.
double period = value / maxValue;

// Phase Shift: the horizontal shift of the sine wave along the x-axis.
double phaseShift = value * pi;

// Plotting the sine wave by connecting various paths till it reaches the diameter.
// Using this formula: y = A * sin(ωt + φ) + C
for (double i = 0; i <= diameter; i++) {
path.lineTo(
i + pointX,
pointY + amplitude * sin((i * 2 * period * pi / diameter) + phaseShift),
);
}

Explanation of the calculations made:

  • Amplitude (A): In the code, the variable amplitude is set to 10, which determines the height of the sine wave.
  • Period: The variable period is calculated as value / maxValue, which controls the time taken to complete one full cycle of the sine wave.
  • Time (t): The variable i represents the current time or position along the x-axis while plotting the sine wave. i varies from 0 to diameter.
  • Angular Frequency (ω): expression 2 * period * pi / diameter calculates the angular frequency of the sine wave. It's multiplied by i to determine how much the wave has progressed horizontally based on the current position.
  • Phase Shift(φ): The variable phaseShift is calculated as value * pi, it determines the horizontal shift of the sine wave.
  • Vertical Shift ©: The variable pointY is calculated based on the value and maxValue, adjusting the y-coordinate of the starting point of the sine wave.

Note : (i * 2 * period * pi / diameter) here we are dividing i by diameterto ensure that the sine wave will complete one full cycle from 0 to 2π as i ranges from 0 to diameter.

After this, we’ll close the path by creating two vertical lines at the right and left ends of the sine wave.

    // Plotting a vertical line which connects the right end of the sine wave.
path.lineTo(pointX + diameter, diameter);
// Plotting a vertical line which connects the left end of the sine wave.
path.lineTo(pointX, diameter);
// Closing the path.
path.close();

Finally, we are using SweepGradient in the paint to fill with the gradient as it progresses. The canvas is clipped to an oval shape, and then we draw the liquid-like animation using the calculated path and paint.

Paint paint = Paint()
..shader = const SweepGradient(
colors: [
Color(0xffFF7A01),
Color(0xffFF0069),
Color(0xff7639FB),
],
startAngle: pi / 2,
endAngle: 5 * pi / 2,
tileMode: TileMode.clamp,
stops: [
0.25,
0.35,
0.5,
]).createShader(Rect.fromCircle(center: Offset(diameter, diameter), radius: radius))
..style = PaintingStyle.fill;

// Clipping rectangular-shaped path to Oval.
Path circleClip = Path()..addOval(Rect.fromCenter(center: Offset(radius, radius), width: diameter, height: diameter));
canvas.clipPath(circleClip, doAntiAlias: true);
canvas.drawPath(path, paint);

Now that we have our LiquidPainter ready, we can continue with the second part.

2. Creating a Circular Progress Bar:

This component will showcase progress with a gradient color over a grey-colored circular track.

Creating a CustomPainter subclass for circular Progress Bar

To start building the circular progress bar, we’ll create a CustomPainter subclass named RadialProgressPainter. Let's take a closer look at the RadialProgressPainter class:

The RadialProgressPainter the class takes four parameters

  1. value: current progress value
  2. backgroundGradientColors: a list of colors for the gradient to show the progress
  3. minValue: minimum value of the progress
  4. maxValue: maximum value of the progress

Next, we’ll calculate the diameter, radius, and center coordinates of the circular progress bar based on the available size parameter. The strokeWidth defines how thick the lines will be when drawing circular shapes.

    // circle's diameter // taking min side as diameter
final double diameter = min(size.height, size.width);
// Radius
final double radius = diameter / 2;
// Center cordinate
final double centerX = radius;
final double centerY = radius;

const double strokeWidth = 6;

We use SweepGradientin the progressPaint to create the gradient effect on the progress. You can use other gradient types like LinearGradient and RadialGradient as per your design choice.

// Paint for the progress with gradient colors.
final Paint progressPaint = Paint()
..shader = SweepGradient(
colors: backgroundGradientColors,
startAngle: -pi / 2,
endAngle: 3 * pi / 2,
tileMode: TileMode.repeated,
).createShader(Rect.fromCircle(center: Offset(centerX, centerY), radius: radius))
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;

// Paint for the progress track.
final Paint progressTrackPaint = Paint()
..color = Colors.white.withOpacity(0.2)
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;

Finally, we calculate the start and sweep angles to draw the progress arc on the canvas. The progress starts from the top (-pi/2 ) and revolves a complete cycle according to the value. We then draw both the progress track and progress arc on the canvas.

    // Calculate the start and sweep angles to draw the progress arc.
double startAngle = -pi / 2;
double sweepAngle = 2 * pi * value / maxValue;

// Drawing track.
canvas.drawCircle(Offset(centerX, centerY), radius, progressTrackPaint);

// Drawing progress.
canvas.drawArc(
Rect.fromCircle(center: Offset(centerX, centerY), radius: radius),
startAngle,
sweepAngle,
false,
progressPaint,
);

Now that we have our RadialProgressPainter ready.

Putting it all together

Now, you can use LiquidPainter and RadialProgressPainter in your Flutter widget to display the Liquid Progress Indicator and Circular Progress Bar, and customize them as per your requirements.

For our example, we’ll make use of the following list of colors as the gradient:

List<Color> gradientColors = const [
Color(0xffFF0069),
Color(0xffFED602),
Color(0xff7639FB),
Color(0xffD500C5),
Color(0xffFF7A01),
Color(0xffFF0069),
];

Here we are creating DemoPage a stateful widget that displays a custom liquid progress indicator and a circular progress bar along with a start/stop button.

  • In the initState(), we initialize an AnimationController with a maximum duration of 10 seconds. The controller is responsible for animating the progress values.
  • The build() method displays the progress value as text, the custom liquid progress indicator and circular progress bar using CustomPaint widgets, and the start/stop button that controls the animation. when the start/stop button is tapped, we toggle the animation by calling _controller.reset() and _controller.forward() accordingly, and update the isPlaying variable to control the animation state.

Congratulations! With that, we have successfully completed the creation of our custom progress indicator. 👏

🎉 Thank you for reading this exciting demo! 🙏 You can find the source code on this GitHub repository.

If you loved it, give it a clap! 👏

you can connect with me on LinkedIn and Twitter for more amazing content! 🚀

Keep coding and stay curious! 🚀 Happy Fluttering! 😊

--

--

Aniket Raj

📱 Mobile Developer | Flutter Developer | Android | IOS