Neon Progress Bar in Android

Simple implementation of circular progress bar with simple rotation animation, gradient and blur mask for glowing effect

Yuriy Skul
8 min readJun 19, 2020
Progress bar with glowing and gradient

This is the first part of Neon UI series.
Part 2 “Material neon progress bar” coming soon

Step 1. Starter code

Let’s define two colors in colors.xml file for circular body and for glowing neon area around body. First one is 100% alpha and for glowing — 80%:

Create a class extending from android.view. But this is not so important — it
can be done by creating custom drawable by analogy.
Define and use in every constructor init() method:

Define progress bar body width as 12 dp. In this example I am going to use progress bar view with width and height 100 dp.
For smaller sizes such as 24 x 24 dp it is better to make body width less then 12 dp.

Neon circular line: let’s define glowing line width 3 times greater than the body width:
private static final int BODY_STROKE_WIDTH = 12;
private static final int GLOW_STROKE_WIDTH = BODY_STROKE_WIDTH * 3;

And we need padding to make the progress bar’s arc not to be out of the view bounds. The padding size should be half the size of GLOW_STROKE_WIDTH.
And define the length for progress bar: 270 degrees.

And define for padding, body width and glowing width three member variables in pixels:

and for drawing define next references:
- paint for body;
- paint for glowing;
- rectangle drawing area;

As it was mentioned before I use in the xml layout file the size of the custom progress bar view for this example 200 x 200 dp:

Step 2 . Simple body arc

As it is recommended do not stress the onDraw() with creating objects so let’s initialize objects in init() and reuse them in onDraw().
Inside init() method:
- extract colors for body and glowing.
- convert BODY_STROKE_WIDTH, GLOW_STROKE_WIDTH and PADDING to pixels and then init pixel variables.

Next inside init():

  • initialize rectangle’s reference mRect;
  • initialize mPaintBody reference;

Now we can override onDraw() method:
- set up to rectangle drawing bounds;
- draw the body by calling canvas.darawArc() with current rectangle, start angle = 0, end angle and paint for body;

Run the program and the expected result should be like this:
Temporary I have set to the parent layout black background color and have added to the view grey color for better visibility of progress bar’s paddings and borders:

For those who are still curious for padding parameter— without it at all result is going to look like this:

Result without padding

Step 3: Glowing arc

Inside init() method add to the end of it:

and add to the onDraw() method right before drawing body arc next line:
canvas.drawArc(..., ..., ..., ..., mPaintGlow);
Body arc should be drown on top of glowing arc!

Because we have defined the padding = half size of glow arc width the position of glowing arc is exactly inscribed into view bounds:

Step 4: Glowing effect

It still looks weird. Now let’s add some mask filters to make it look better.
Inside init() add next logic to the bottom of it:
- create BlurMaskFilter object with specific radius and type = Blur.NORMAL.
I am going to use blur radius = mBodyStrokeWidthPx but you can play around with it;
- set this maskFilter to the mPaintGlow object;
- fix the issue with maskFilter: disable hardware acceleration for this view and set it to software layer in the top of init() method otherwise BlurMaskFilter won’t make any effect.

I am going to comment out in onDraw() method drawing body arc for better visual explanation of the next issue:

@Override
protected void onDraw(Canvas canvas) {
...
canvas.drawArc(mRect, 0, BODY_LENGTH, false, mPaintGlow);
// temporary commented
// canvas.drawArc(mRect, 0, BODY_LENGTH, false, mPaintBody);
}
Glowing arc goes out of the view bounds

Since we have added blur mask, glowing arc is a little bit out of the view bounds. It is caused by additional blurring radius.

Fix it by adding to the PADDING constant additional value = blurMask radius. I use for radius the value = mBodyStrokeWidthPx ( BODY_STROKE_WIDTH in dp)
Update PADDING constant:

Uncomment in onDraw() previously commented line to draw body arc and remove test background.

Now it looks fine :

Step 5: Gradient (Optional)

As a result of this article this shape will be rotating so to make it look more “dynamic “ let’s add gradient to the body arc’s tail and glowing arc’s tail.

Define next field variables related to the body gradient effect:
- SweepGradient;
- array of ints. It has only two values: start color and end color;
- array of floats populated with start gradient position and end gradient.

Add below the definition of private RectF mRect next lines;

Define new constant NORMALIZED_GRADIENT_LENGTH.

Gradient length can be as long as arc size but I am going to apply gradient only to the part of arc from tail to the middle of its length. So the gradient length = BODY_LENGTH/2 degrees.

In addition I need it to be normalized(divided by 360 degrees) for future usage. So NORMALIZED_GRADIENT_LENGTH = BODY_LENGTH/2/360

Warning: this value is a float value (not int) in a range from 0 to 1 so don’t mess up with it otherwise it will be rounded to 1 and would produce a bug .

Add below definition of private static final int BODY_LENGTH = 270 gradient length constant:

Add to the bottom of init() method next lines under setting mask filter:
- initialize mBodyGradientFromToColors array with start color as transparent and end color as bodyColor. The value of bodyColor has been extracted as local variable inside init() method earlier.
- initialize mGradientFromToPositions with start and end normalized positions. Start is at 0 and end is = NORMALIZED_GRADIENT_LENGTH

Now SweepGradient mBodyGradient variable can be initialized. Because its constructor needs the x-coordinate of the center and the y-coordinate of the center it can’t be done inside init() method.

Override onSizeChanged(…) method:
- using its arguments(new width and new height) to calculate center x and y for mBodyGradient dynamically.
- set initialized mBodyGradient to the mPaintBody value.

I am going to use instead of transparent color - Color.RED as a start color of mBodyGradientFromToColors array for better visualization next issue.
Drawing glowing arc line is temporary commented inside onDraw()

Tail cup has no gradient effect

That is how it is rendered now. By setting mPaintBody.setStrokeCap(Paint.Cap.ROUND) to the paint object it renders additional rounding caps before arc’s start position and after arc’s end position.
Gradient starts at zero position (degree) but the start cup is out of the start-end gradient range. Going to be fixed right after next.

So let’s apply same logic with “gradient staff” to the glowing arc:

Define two field variables related to the glowing gradient effect right under mGradientFromToPositions filed:
- SweepGradient reference;
- array of ints. It has only two values: start color and end color.

Initialize mGradientFromToPosition:
add to the bottom of init() method next line :

For array of start-end positions I am going to use the same field as it is used for mBodyGradient: mGradientFromToPositions because start and end of body gradient is the exactly the same like start/end position of glowing gradient.

Inside onSizeChanged() method initialize mGlowGradient and set it to the mPaintGlow field. So the final look of onSizeChanged() method is:

As it was mentioned for better visualization I have replaced transparent color with Red during instantiating mBodyGradientFromToColors and mGlowGradientFromToColors arrays inside init() method:

Don’t forget to uncomment inside onDraw() method canvas.drawArc() for body and glowing arc retrospectively:

Now let’s get rid of “tail cup” bug.
As we can see there are additional tail rounded cups for glowing and body arcs before start position = 0.

Currently in onDraw():
canvas.drawArc(..., 0, ..., ..., mPaintGlow);
canvas.drawArc(..., 0, ..., ..., mPaintBody);

To fix it we have to add additional offset to the startAngle of drawArc() method’s argument instead of startAngle =0;
Something like this:
canvas.drawArc(..., mCupAdditionalOffset, ..., ..., mPaintGlow);
canvas.drawArc(..., mCupAdditionalOffset, ..., ..., mPaintBody);

Define it as a member variable below mGlowGradientFromToColor definition:

We have two cup radiuses and for computation start offset we need to use bigger one — glowing cup radius. Because we use padding equals the half size of the glowing arc width, the glowing cap radius (pixel) has paddingPx value.

▲ABC is a right-angled triangle where BC(arc radius) is its hypotenuse and BA(cup radius) is its cathetus. Those values could be extracted inside onSizeChange() method:

Implement method computeOffset(cupRadius, arcRadius):
- compute sinus of ∠ACB;
-
extract acrsin in Rad units;
-
convert result to degree;

Inside onDraw() method pass mCupAdditionalOffset value as the second argument of canvas.drawArc() witch is called twice for body and glowing arc:

Finally the gradient effect has applied:

Step 6 : Rotate animation

Finally static view is ready for rotation. It can be implemented in different ways…
As example we can rotate canvas from onDraw() by increment rotation value and calling postInvalidateDelayed().

But in this step I am going to use object animator to define rotation.

Define three constants:

And two member variables:

And setter for rotation variable. Calling invalidate() to redraw our view every time when new rotationAngle has been set:

Warning! This method is going to be used by ObjectAnimator so keep in mind that target’s name has to be setVariable() if we pass to the ofFloat() String property name = “variable” .

Define new method witch creates ObjectAnimator instance:

Use this method for initializing mRotationAnimator variable.
Put next line as the last one of the init() method’s body:

And finally inside onDraw() at the top of it add:
- call start() animator if it is not started yet;
- rotate canvas by rotation value around center point;

That is all. Thanx for reading.

Final code:

--

--