Creating a Custom Progress Indicator in Flutter
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
value
: current progress valuemaxValue
: 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 asvalue
/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 todiameter.
- Angular Frequency (ω): expression
2 * period * pi / diameter
calculates the angular frequency of the sine wave. It's multiplied byi
to determine how much the wave has progressed horizontally based on the current position. - Phase Shift(φ): The variable
phaseShift
is calculated asvalue * pi
, it determines the horizontal shift of the sine wave. - Vertical Shift ©: The variable
pointY
is calculated based on thevalue
andmaxValue
, adjusting the y-coordinate of the starting point of the sine wave.
Note : (i * 2 * period * pi / diameter)
here we are dividing i
by diameter
to 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
value
: current progress valuebackgroundGradientColors
: a list of colors for the gradient to show the progressminValue
: minimum value of the progressmaxValue
: 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 SweepGradient
in 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 anAnimationController
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 usingCustomPaint
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 theisPlaying
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! 😊