React Native percentage based progress circle ( no external library ) Part 1

Saurabh Gour
8 min readJun 30, 2018

--

I have been recently researching around solutions to build a circular percentage view like so in react native.

Figure 0: Circular progress view

I wanted to achieve this in react native alone without the help of any external libraries. One of the libraries out there pretty much does this but has a minor issue reported in its github page. I wanted to see if I could re-create a similar component using react native alone. The contents of this article are pretty much inspired from the plugin mentioned above and this article.

So, let’s get started. Firstly, we will try to create a circle in css for scenarios when the progress is 0 percent, this circle will act as the bottom layer in our css design. For now, the widths I would be taking would be hard coded. Later on I will try to make this component accept its dimensions from outside as props.

Above code is used to create a base square which looks like so:

Figure 1: A simple square

Now, lets add some borders to this square. The border width would determine the thickness of the circular border. One thing to note here is that for react native the border seems to be a part of the width or height of the container, so the borders would occupy space within the square. To add the border width, add the following property to our styles object.

borderWidth: 20

Figure 2: Square with border

Now, come the fun part of converting this square to a circle. To do this, we have to add a border radius of half the width of the square like so:

borderRadius: 100

And voila, css magic converts your square to a circle.

Figure 3: Circle with default border and background color

Now, let’s remove the background color we originally assigned ( since it was only for visualization purpose and give our border a color). Our styles object now looks like this:

And, we have a base circular border that we can use for our percentage wheel.

Figure 4: Circle with grey border and no background color

At this point, we only have the base layer for our design. Now, we will create the second layer which will basically be placed on top of this base layer. To do this, we create another view inside our parent view, assign it the same width and height as that of the parent view and give it a position of absolute. ( since we will be adding more child views, we don’t want flexbox to interfere, hence the position of absolute).

backgroundColor added only for temporary visualization
Figure 5: Second layer is placed above base layer but has some alignment issue :(

As you can see from the figure, we have successfully placed the second layer above the first layer, but it seems to have some alignment issue( I am not entirely sure why this issue occurs since the square dimensions are same ). This can be fixed by adding justifyContent and alignItems flexbox properties to our container element like so:

Now, our second layer square completely overlays the base circle.

Figure 6: Square overlays circle

At this point, we will assign same border as we assigned to our base circle and remove the background color that we added for visualization.

Figure 7: Bordered square overlays circle.

Now, you can clearly see the square overlaying the circle. The next portion is taken from this article. Now, we will make the left and bottom borders as transparent whereas top and right borders will be assigned a color that would indicate the progress.

The result of this is as follows:

Figure 8: Square with top, right border colored, left and bottom transparent

Due to the transparent color property we assign to the border, the bottom layer circle is now partially visible. Now, let’s add the border radius property as we did to the parent container to convert the square borders into a semi circle. We simply add:

borderRadius: 100 to the progressLayer, now our output is:

Figure 9: Second layers semi-circle overlays bottom circle.

Awesome !! Now we have a semi circle in the second layer which exactly overlays the circle of the bottom layer. The only important thing left to do now is rotate the semicircle to indicate progress. Since, we have a semi-circle, we will be able to only indicate progress from 0 to 50 percent using it. Let’s first try to get that done and then we’ll see how to add progress from 50 to 100 percent. By default the origin for any element is located at its centre. For our case this is the centre of the square view which also happens to be the centre of the circle. This is great news for us since react native does not support the css transform-origin property. ( And co-incidentally we want to rotate the second layers circle around its centre). Okay, so let’s rotate our circle by 45 degrees:

transform:[{rotateZ: ‘45deg’}]

The Z-axis passes through the centre of the circle, and you can imagine it as the axis that would come outside the screen of your device. So, you can guess how the rotation of 45 degrees around this axis would affect our circle.

Figure 10: Second layer circle with a rotation of 45 degrees.

This looks pretty good now, and seems like a progress circle occupying a percentage of 50. But, what about cases where we want the percentage to be less than that. Let’s say 25 %. If we change the rotation to -45 deg like so:

transform:[{rotateZ: ‘-45deg’}]

we have unwanted coloring toward the top left side of the circle as highlighted below:

Figure 11: When percentage is less than 50, we have unwanted coloring in the left semi-circle.

If we wanted to represent 0 percent, we would end up having the entire left portion of the semi-circle colored which we don’t want. To get around this, we will create another circle which will overlay the second circle. The left half of this new semicircle would be given a border color(grey in our case ) of our parent container and the right half would be kept transparent. This would help us get rid of any unwanted coloring on the left side of the circle. So, let’s create this third circle now.

As you can see, the third circle is also given same dimensions and position so that it overlaps the second circle exactly. Using rotation of -135 degrees, and coloring the borders appropriately, the left semi-circle for the third layer circle is colored grey whereas the right semi-circle is transparent. Now, let’s see the output:

Figure 12: Unwanted colored portion offset using third layer of circle.

We have pretty much made our progress circle to work for 0 to 50 percent. But, hold on we won’t be specifying degrees to the progress circle, would we ? Ideally, we should accept a percentage prop to our circle and then rotate the circle based on the percentage that was passed. So, let’s do that.

Our progress bar does not get displayed for a rotation of -135deg, whereas it covers 50 percent for 45 degrees. So, let’s set the initial value of rotation to -135deg ( corresponds to zero). Then, depending upon the percentage value passed, we need to increment 180degrees ( -135 to 45 ) along 50 percent. This means each percent occupies 180/50 = 3.6 percent. To set the default value of our progress bar to zero, we will change the rotateZ property to -135 deg. If a percent prop is passed, we would dynamically change the value of the transform property using style={[styles.progressLayer, stylefromprops]} where our base styles would be maintained in the progressLayer styling and any styles passed as props would override the base styles. Let’s see the code for this:

We have destructured percent from props and passed it to the propStyle function which returns a deg value based on percent. Now, if we pass a percent prop to our Component like so:

<CircularProgress percent={20} />

it renders the following output:

Figure 13: Dynamic handling of rotation using percent prop.

For now, we have a working progress circle for percentages upto 50. Let’s see what happens if we go beyond that. If we pass a percent value of 70, we get the following output:

Figure 14: When percentage goes beyond 50

Hmm, that doesn't look right. Our semi-circle shifts from the top-right side, that’s why we do not see the color in the top right of the ring. Also, our 3rd layer overlays the left half of the semi-circle, so any color in the second layer that comes under the left semi-circle cannot be seen. To get around this, we would use following approach when the percent is greater than 50.

  1. Use the 2nd layers semi-circle to cover the entire right half( 50 percent) of the semi-circle.
  2. Remove/replace the third layer by creating another semi-circle and rotate is appropriately to occupy the desired percentage.

So now depending upon whether the percent is greater than 50, the third layer of the circle would be different.

So, in above code we have also handled cases where the percentage is greater than 50. Have also done some minor code refactoring here. Now, if we pass 80 percent as a prop to our component, we get the following output.

Figure 15: Handling all percentages from 0 to 100.

The only thing that remains to have our basic progress circle complete now is the percentage value to be displayed at the centre of the circle. We do this by adding a text tag to the parent view and displaying the percent value from props inside it. Relevant code for this is:

Now, the percentage value gets displayed inside the circle like so:

Figure 16: Output from our final code

Final Thoughts: With this we are done with our scrappy implementation of a circular progress bar in react native. From here, it would be simple to add support to other props like border color, circle radius etc to our progress view. The key concepts that we used here are using postion: ‘absolute’ to create layers, transparent borders so that we can see through the layers and rotation to display the required percentage value.

Before going, if you have not already, please do visit this article which is the basis of the idea and this article which is the basis of the implementation that we did.

The entire code can be found here which I would soon be updating.

Part 2 for this article can be found here.

--

--