Indeterminate – Part 3

Indeterminate ProgressBars are a useful tool for communicating to our users that an operation is in progress when we cannot predict how long it is likely to take. Previously on Styling Android we’ve covered how to create a backwardly compatible approximation of the material styled horizontal indeterminate ProgressBar but we haven’t looked at the circular form – in this series we’ll create an approximation of the material circular indeterminate ProgressBar which will be backwardly compatible to API 11 (Honeycomb).

Previously we’ve taken a deep dive in to the Lollipop+ implementation of the material circular indeterminate drawable, and now we’ll turn our attention to actually creating our own.

First we’ll look at the actual Drawable component (which is analogous to the VectorDrawable we looked at in AOSP) that is responsible for actually drawing the circular component (or a section of one, at least). We can’t use VectorDrawable because it’s Lollipop+ only and VectorDrawableCompat isn’t ready for serious use (at the time of writing). But we can use good, old-fashioned Canvas and Paint to do what we need.

First we determine the pixel values for the stroke width and internal padding of the drawable to closely match those used my the actual material drawable. It is important to convert these from DIP to PX values based upon the DisplayMetrics at runtime to ensure that we can consistency across displays with differing densities.

Next we create a Paint object with the necessary colour and stroke width which will be used to actually draw the section of the circle. We can retrieve the correct colour from the theme by performing the appropriate lookup (we’re using the Design support library so this will honour the values in our Theme).

It’s important to turn on anti-aliasing on the Paint otherwise we’ll get some lovely jaggy edges. Also we set a square cap on the path which mimics what is set for the pathData in the material VectorDrawable that we looked at earlier in the series.

We also create a RectF object which we’ll use later on. Both the Paint and RectF objects will be used in the onDraw() method and we create them in advance and re-use them each time onDraw() is called to avoid any object creation and keep our onDraw() implementation as lean and clean as possible to ensure that we don’t drop any frames in the animations.

Next we actually create the IndeterminateDrawable instance itself before calling createAnimator() to construct the animations. This is currently a dummy implementation which just sets some static default values. We’ll get the the Animators in due course!

Because we subclass Drawable, we need to override some abstract methods:

.drawable.IndeterminateDrawable.java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

publicclassIndeterminateDrawableextendsDrawableimplementsAnimatable{

.

.

.

@Override

publicvoidsetAlpha(intalpha){

paint.setAlpha(alpha);

}

@Override

publicvoidsetColorFilter(ColorFilter colorFilter){

paint.setColorFilter(colorFilter);

}

@Override

publicintgetOpacity(){

returnpaint.getAlpha();

}

.

.

.

}

We just need to apply changes to our Paint object as needed.

Because we implement Animatable we also need to override some methods to control the animation state:

.drawable.IndeterminateDrawable.java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

publicclassIndeterminateDrawableextendsDrawableimplementsAnimatable{

.

.

.

@Override

publicvoidstart(){

animator.start();

}

@Override

publicvoidstop(){

animator.end();

}

@Override

publicbooleanisRunning(){

returnanimator.isRunning();

}

.

.

.

}

Once again we delegate these only to the Animator this time.

We now need to implement some setters which will be used by the Animators later to change the parameters of the Drawable in order to animate it:

.drawable.IndeterminateDrawable.java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

publicclassIndeterminateDrawableextendsDrawableimplementsAnimatable{

.

.

.

publicvoidsetStartAngle(floatstartAngle){

this.startAngle=startAngle;

invalidateSelf();

}

publicvoidsetEndAngle(floatendAngle){

this.endAngle=endAngle;

invalidateSelf();

}

publicvoidsetRotation(floatrotation){

this.rotation=rotation;

invalidateSelf();

}

.

.

.

}

If you’ve read the previous article in this series these should be fairly self-explanatory – we are going to use animators to dynamically change the start and end of the sweep, and also rotate the whole thing.

This is really simple! First we populate the RectF bounds object that we created earlier with the dimensions of the Canvas taking in to account the internal padding. Then we draw an arc within these bounds and set the start and end angles according to the values set. We also add the rotation to the start angle and this will effectively rotate everything. This will get rendered using the Paint object that we set up earlier.

Actually using this in our Activity is pretty easy. We include a View in our layout (I’ve used an ImageView in the example code) and then construct the IndeterminateDrawable, and start the animations in exactly the same way as you would start and AnimatedVectorDrawable:

If we run this we can see that we get a static three quarters of a circle drawn because of the default values that we use in place of the animator construction:

Just before we finish it is worth pointing out that there are some known issues with drawArc in the Skia library which Android uses to render things. Eugenio Marletti has described these in detail and his post is well worth a read. However, we’re only drawing a single arc here so I’m not bothered about the slight rendering discrepancies – they simply won’t be apparent in this case.

In the next article we’ll turn our attention to the Animators which will bring this to life!