SWASTIJ CONSULTANCY

Flutter Elliptical Progress Bar: Code & Calculations

Learn how to create a custom elliptical progress bar in Flutter with step-by-step explanations of the math, logic, and drawing calculations.

Published On 2023-10-15

Flutter Elliptical Progress Bar: Code & Calculations

By Swasti Jain

INTRODUCTION

Progress bars are everywhere — from showing app loading states to tracking goals. But while circular or linear progress bars are common in Flutter, an elliptical progress bar isn’t available out of the box.

In this post, we’ll explore how to create one from scratch. We’ll break down the ellipse into smaller parts, understand the math behind it, and walk through the logic step by step until we have a working elliptical progress bar in Flutter.

1. DIVIDING THE ELLIPSE INTO SECTIONS

When creating an elliptical progress bar, it helps to simplify the shape into smaller parts. Instead of dealing with the full ellipse at once, we break it down into segments. The progress is then shown by filling these segments step by step to represent the current value.

In this approach, the ellipse’s path is divided into six sections:

  • First line AB
  • First Arc with center X
  • Second line DE
  • Third line EF
  • Second Arc with center Y
  • Fourth line HA
Blog image

sectioned-ellipse

2. CALCULATING THE START AND END POINTS

Blog image

screen coordinates

Important before you proceed - For the canvas paint, all coordinate
points are calculated keeping the origin on the leftmost screen

For example, a point at a distance of X from the left edge of the
screen will have an x-ordinate as X. Similarly, a point at a distance
of Y from the top edge of the screen will have a y-ordinate as Y.

Let the width of the ellipse container be W and the height be H
Let the thickness of the ellipse be t.

We will be drawing the white path as the progress path. It is the center of the progress loop we need. Giving stroke width t/2 to the purple path will make it fill the ellipse completely.

Blog image

sectioned ellipse with maths

1. LINE AB

Then coordinates of the start point A of line AB = (W/2, t/2) ………..(1)

2. FIRST ARC BCD

Let L be the endpoint on the edge of the first line of the ellipse,
L is at the top edge of the screen, hence Ly = 0
Let the center of this arc be X
Radius r(XB) = XL - BL = H/2 - t/2
x-ordinate of X = NM - XM = NM - XC - CM
= W - r - t/2
Y-ordinate of X = H/2
Therefore (Xx, Xy) = (W - r - t/2, H/2) …………………(2)

3. Second line DE

Endpoint E = (A, H - t/2)
E = ( W/2, H - t/2 ) …………………(3)

4. For third line EF,

X-ordinate of F and Y are the same
i.e. Fx = Yx
Yx = NG + GY
therefore
Endpoint Fx = NG + GY = r + t/2
Fy = H - t/2
F = (r + t/2, H - t/2) …………………(4)

5. Second arc FGH

Center Y = ( r + t/2, H/2 ) …………………(5)

6. Fourth line HA

To endpoint A = (W/2, t/2), as calculated in (1)

The ellipse is divided into 6 segments as elaborated above; the progress value needs to be divided into 6 parts to represent an accurate completion fraction.

Therefore
progress = progressValue / 6

Let progress for each segment be calculated in an array called sections

1int x = (progress / 100).floor();
2var sections = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
3
4for (int i = 0; i < x; i++) {
5  sections[i] = 1;
6}
7
8if (x <= 5) {
9  sections[x] = (progress % 100) / 100;
10}

VISUALISATION OF THE ABOVE CALCULATION

For a progressValue of 60,
progress = progressValue * 6 = 360
x represents the segments that will be filled fully
x = Floor(360/100) = 3

sections = [ 1, 1, 1, 0.6, 0, 0 ]

This represents that all the parts need to be filled in the calculated order as follows:

Blog image

numbered sections of ellipse

First line AB : sections[0] = 1 means 100% (full)
First Arc BCD : sections[1] = 1 means 100% (full)
Second line DE : sections[2] = 1 means 100% (full)
Third line EF : sections[3] = 0.6 means 60% (partial)
Second Arc FGH : sections[4] = 0 means 0% (none)
Fourth line HA : sections[5] = 0 means 0% (none)

Blog image

sub sections of ellipse

Calculating the lengths of the lines
len1 = Bx - Ax;
len2 = Dx - Ex
= Bx - Ex ( since Bx = Dx as can be seen from fig)
len3 = Ex - Fx ;
len4 = Ax - Hx;

3. PAINTING


Now according to the sections we have calculated and the x value, we paint the canvas. We will paint only the progress amount, which will be calculated as -
Length of the part \* section value of the part

Visualization -> For a segment having a section value of 0.6, only 60% of the total length of that segment needs to be filled hence we will paint to only 60% of the total length

CASE 1: x = 0 -> only first segment is to be painted

Blog image

case 1

1if (x == 0) {
2  final path = Path()
3    ..moveTo(startPointX, thickness! / 2)
4    ..lineTo(
5      startPointX + len1 * sections[0],
6      thickness! / 2,
7    ); // line1
8}
1canvas.drawPath(path, paint);

CASE 2: x = 1

first segment + first arc is to be painted
The full arc is made by painting an arc of 180 degrees. ( -90 to 90 in canvas paint )
Hence progress to be painted for the arc can be calculated as follows -
Section value of the arc * 180

Note: math.pi / 180 is multiplied for conversion from radian into
degrees
Blog image

case 2

1else if (x == 1) {
2  final path = Path()
3    ..moveTo(startPointX, thickness! / 2)
4    ..lineTo(firstArcStartX, thickness! / 2) // line1
5    ..arcTo(
6      Rect.fromCircle(center: firstArcCenter, radius: r),
7      startAngleRad,
8      sections[1] * 180 * (math.pi / 180.0),
9      true,
10    );
11
12  canvas.drawPath(path, paint);
13}

CASE 3: x = 2

first segment + first arc + second line is to be painted

Blog image

case 3

1else if (x == 2) {
2  final path = Path()
3    ..moveTo(startPointX, thickness! / 2)
4    ..lineTo(firstArcStartX, thickness! / 2) // line1
5    ..arcTo(
6      Rect.fromCircle(center: firstArcCenter, radius: r),
7      startAngleRad,
8      sections[1] * 180 * (math.pi / 180.0),
9      true,
10    )
11    ..lineTo(
12      firstArcStartX - len2 * sections[2],
13      size.height - thickness! / 2,
14    ); // line2
15
16  canvas.drawPath(path, paint);
17}

CASE 4: x = 3

first line + first arc + second line + third line

Blog image

case 4

1else if (x == 3) {
2  final path = Path()
3    ..moveTo(startPointX, thickness! / 2)
4    ..lineTo(firstArcStartX, thickness! / 2) // line1
5    ..arcTo(
6      Rect.fromCircle(center: firstArcCenter, radius: r),
7      startAngleRad,
8      sections[1] * 180 * (math.pi / 180.0),
9      true,
10    )
11    ..lineTo(
12      firstArcStartX - len2 * sections[2],
13      size.height - thickness! / 2,
14    ) // line2
15    ..lineTo(
16      secondLineEndX - len3 * sections[3],
17      size.height - thickness! / 2,
18    ); // line3
19
20  canvas.drawPath(path, paint);
21}

CASE 5: x = 4

first line + first arc + second line + third line + second arc

Blog image

case 5

1else if (x == 4) {
2  final path = Path()
3    ..moveTo(startPointX, thickness! / 2)
4    ..lineTo(firstArcStartX, thickness! / 2) // line1
5    ..arcTo(
6      Rect.fromCircle(center: firstArcCenter, radius: r),
7      startAngleRad,
8      sections[1] * 180 * (math.pi / 180.0),
9      true,
10    )
11    ..lineTo(
12      firstArcStartX - len2 * sections[2],
13      size.height - thickness! / 2,
14    ) // line2
15    ..lineTo(
16      secondLineEndX - len3 * sections[3],
17      size.height - thickness! / 2,
18    ) // line3
19    ..arcTo(
20      Rect.fromCircle(center: secondArcCenter, radius: r),
21      startAngleRad + piInRadian,
22      sections[4] * 180 * (math.pi / 180.0),
23      true,
24    );
25
26  canvas.drawPath(path, paint);
27}

CASE 6: x = 5

first line + first arc + second line + third line + second arc + fourth line

Blog image

case 6

1else {
2  final path = Path()
3    ..moveTo(startPointX, thickness! / 2)
4    ..lineTo(firstArcStartX, thickness! / 2) // line1
5    ..arcTo(
6      Rect.fromCircle(center: firstArcCenter, radius: r),
7      startAngleRad,
8      sections[1] * 180 * (math.pi / 180.0),
9      true,
10    )
11    ..lineTo(
12      firstArcStartX - len2 * sections[2],
13      size.height - thickness! / 2,
14    ) // line2
15    ..lineTo(
16      secondLineEndX - len3 * sections[3],
17      size.height - thickness! / 2,
18    ) // line3
19    ..arcTo(
20      Rect.fromCircle(center: secondArcCenter, radius: r),
21      startAngleRad + piInRadian,
22      sections[4] * 180 * (math.pi / 180.0),
23      true,
24    )
25    ..lineTo(
26      thirdLineEndX + len4 * sections[5],
27      thickness! / 2,
28    ); // line4
29
30  canvas.drawPath(path, paint);
31}

Building an elliptical progress bar in Flutter becomes much simpler once you break the ellipse into smaller segments and map progress across them. By combining a bit of math with Flutter’s Canvas and Path, you can create a fully custom progress indicator.


This method not only helps visualize progress in a unique way but also gives you the foundation to design other creative, custom UI elements in Flutter.
Thanks for reading!