How we created a custom Android view which draws a glowing progress ring with rounded ends.
With Glose you can set a goal of reading a given number of pages each day. You can then follow your daily progress with the daily goal widget on the home screen:
The first implementation of the widget used a circular
ProgressBar to represent the progress ring. This did the job but missed a few features we wanted:
- The ends of the progress ring should be rounded. With the circular
ProgressBarthe ends are flat and do not match the overall style of the app.
- We should be able to use any drawable for the progress ring, like a gradient for instance, and not be limited to a solid color.
- The progress ring should project a glow when you’ve exceeded your goal. The more the goal has been exceeded, the bigger the glow effect.
With these features in mind, we set out to update our implementation of the daily goal widget.
Rounding the ends
We couldn’t find a way to create a drawable for the
ProgressBar that would draw rounded ends on the progress ring. So we replaced the
ProgressBar with a custom view inspired by this StackOverflow answer.
This custom view sets up the two
Paint objects that will be used to draw the ring:
ringPaint is configured with the
ROUND stroke cap, which is how the progress ring will get its ends rounded.
The drawing of the ring is then performed by the
onDraw() override using simple
arcRect is a
RectF that caches the bounds of the arc used to draw the ring. It is updated each time
onSizeChanged() is called:
And that’s all it takes to get nice rounded ends:
Using a drawable for the progress ring
The progress ring is also used in a popup panel showing historical data. In this popup the ring must be drawn using a gradient that is lighter at the top and darker at the bottom.
To achieve this, we’ve updated the
onDraw() method so that the arc it draws acts as a mask on top of the ring drawable:
The key here is
ringMaskPaint, which is used as the paint for the second
Canvas.saveLayer() call. It is configured with the
PorterDuff.Mode.MULTIPLY transfer mode, and as a result anything that is drawn in this layer (the progress ring) will act as a mask over what is drawn in the layer below (the ring drawable). See Porter/Duff Compositing and Blend Modes by Søren Sandmann Pedersen for an illustrated explanation of these concepts.
onSizeChanged() method is also modified to adjust the bounds of
And here is the result:
Adding the glow effect
The progress ring is expected to glow when you’ve exceeded your reading goal, and the more you’ve exceeded your goal, the more the ring glows. We achieve this in the
onDraw() override by drawing an arc with a
Paint object configured to draw a shadow layer below the arc. We’ve also added the following attributes on the custom view:
insetdefines the dimension by which the progress ring is inset inside the view bounds to leave space for the glow effect.
glowColoris the color used to draw the glow effect.
glowPeakis the ratio beyond
maxProgresswhere the glow effect reaches its peak.
And that is what it looks like in the daily goal widget:
Animating the ring and putting it all together
Lastly, we added a
setProgressAnimated() method on the custom view that we use to animate the progress ring from zero to its current value. This uses a
ValueAnimator that updates the
progress value from zero to the desired value. The duration of the animation is adjusted so that the progress ring grows approximately at the same speed across the progress value spectrum.
This is how the progress ring is animated in the popup panel:
And that is how it is animated in the daily goal widget:
Here’s the same animation in slow-motion:
Curious about what it looks like in real life? Go ahead and install Glose for Android, and let us know what you think!