Neon Progress Bar in Android
Simple implementation of circular progress bar with simple rotation animation, gradient and blur mask for glowing effect
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:
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);
}
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()
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: