The Canvas class provides us with many drawing functions; for example:
- drawArc()
- drawBitmap()
- drawOval()
- drawPath()
To draw a circular activity indicator, we can use the drawArc() method. Let's create the basic class and draw an arc:
public class CircularActivityIndicator extends View { private static final int DEFAULT_FG_COLOR = 0xffff0000; private static final int DEFAULT_BG_COLOR = 0xffa0a0a0; private Paint foregroundPaint; private int selectedAngle; public CircularActivityIndicator(Context context, AttributeSet
attributeSet) { super(context, attributeSet); foregroundPaint = new Paint(); foregroundPaint.setColor(DEFAULT_FG_COLOR); foregroundPaint.setStyle(Paint.Style.FILL); selectedAngle = 280; } @Override protected void onDraw(Canvas canvas) { canvas.drawArc( 0, 0, getWidth(), getHeight(), 0, selectedAngle, true, foregroundPaint); } }
The result is as shown in the following screenshot:
![](assets/2c9a946b-8846-4198-97c5-f5f935e32353.png)
Let's fix the ratio, so the width of the arc will be the same as the height:
@Override protected void onDraw(Canvas canvas) { int circleSize = getWidth(); if (getHeight() < circleSize) circleSize = getHeight(); int horMargin = (getWidth() - circleSize) / 2; int verMargin = (getHeight() - circleSize) / 2; canvas.drawArc( horMargin, verMargin, horMargin + circleSize, verMargin + circleSize, 0, selectedAngle, true, foregroundPaint); }
We'll use the smaller dimension, either width or height, and draw the arc centered with a square ratio: with the same width and same height.
This doesn't look like an activity indicator; let's change it and draw only a thin band of the arc. We can achieve this by using the clipping capabilities that canvas gives us. We can use canvas.clipRect or canvas.clipPath, for example. When using clipping methods, we can also specify a clipping operation. By default, if we don't specify it, it will intersect with the current clipping.
To draw only a thin band, we'll create a smaller arc in a path, around 75% of the size of the arc we'd like to draw. Then, we'll subtract it from the clipping rectangle of the whole view:
private Path clipPath; @Override protected void onDraw(Canvas canvas) { int circleSize = getWidth(); if (getHeight() < circleSize) circleSize = getHeight(); int horMargin = (getWidth() - circleSize) / 2; int verMargin = (getHeight() - circleSize) / 2; // create a clipPath the first time if(clipPath == null) { int clipWidth = (int) (circleSize * 0.75); int clipX = (getWidth() - clipWidth) / 2; int clipY = (getHeight() - clipWidth) / 2; clipPath = new Path(); clipPath.addArc( clipX, clipY, clipX + clipWidth, clipY + clipWidth, 0, 360); } canvas.clipRect(0, 0, getWidth(), getHeight()); canvas.clipPath(clipPath, Region.Op.DIFFERENCE); canvas.drawArc( horMargin, verMargin, horMargin + circleSize, verMargin + circleSize, 0, selectedAngle, true, foregroundPaint); }
In the following screenshot, we can see the difference:
![](assets/6ac73a16-70b2-4c7a-87ea-d1feacd304a0.png)
As finishing touches, let's add a background color to the arc and change the starting position to the top of the view.
To draw the background, we'll add the following code to create a background Paint to our constructor:
backgroundPaint = new Paint(); backgroundPaint.setColor(DEFAULT_BG_COLOR); backgroundPaint.setStyle(Paint.Style.FILL);
Then modify the onDraw() method to actually draw it, just before drawing the other arc:
canvas.drawArc( horMargin, verMargin, horMargin + circleSize, verMargin + circleSize, 0, 360, true, backgroundPaint);
As a small difference, we're drawing the whole 360 degrees so it will cover the whole circle.
To change the starting position of the arc, we'll rotate our drawing operations. Canvas supports rotation, translation, and matrix transformations as well. In this case, we only need to rotate 90 degrees anti-clockwise to get our starting point at the top of the arc:
@Override protected void onDraw(Canvas canvas) { int circleSize = getWidth(); if (getHeight() < circleSize) circleSize = getHeight(); int horMargin = (getWidth() - circleSize) / 2; int verMargin = (getHeight() - circleSize) / 2; // create a clipPath the first time if(clipPath == null) { int clipWidth = (int) (circleSize * 0.75); int clipX = (getWidth() - clipWidth) / 2; int clipY = (getHeight() - clipWidth) / 2; clipPath = new Path(); clipPath.addArc( clipX, clipY, clipX + clipWidth, clipY + clipWidth, 0, 360); } canvas.clipRect(0, 0, getWidth(), getHeight()); canvas.clipPath(clipPath, Region.Op.DIFFERENCE); canvas.save(); canvas.rotate(-90, getWidth() / 2, getHeight() / 2); canvas.drawArc( horMargin, verMargin, horMargin + circleSize, verMargin + circleSize, 0, 360, true, backgroundPaint); canvas.drawArc( horMargin, verMargin, horMargin + circleSize, verMargin + circleSize, 0, selectedAngle, true, foregroundPaint); canvas.restore(); }
We also used canvas.save() and canvas.restore() to preserve the state of our canvas; otherwise, it will be rotating -90 degrees each time it is drawn. When calling the canvas.rotate() method, we also specified the center point of the rotation, which matches with the center point of the screen and the center point of the arc.
Whenever we're using a canvas function as rotate, scale, or translate, for example, we're actually applying a transformation to all the successive canvas drawing operations.
The final result is shown in the following screenshot:
![](assets/725dac60-6a60-4a79-8249-148e5a8cf8a4.png)
Something we need to be aware of is that not all canvas operations are supported by hardware on all Android versions. Please check if the operations you have to do are supported or provide a runtime workaround for them. Find more information about what operations are hardware accelerated at:
https://developer.android.com/guide/topics/graphics/hardware-accel.html.
Here is the final implementation of the class:
public class CircularActivityIndicator extends View { private static final int DEFAULT_FG_COLOR = 0xffff0000; private static final int DEFAULT_BG_COLOR = 0xffa0a0a0; private Paint backgroundPaint; private Paint foregroundPaint; private int selectedAngle; private Path clipPath; public CircularActivityIndicator(Context context, AttributeSet
attributeSet) { super(context, attributeSet); backgroundPaint = new Paint(); backgroundPaint.setColor(DEFAULT_BG_COLOR); backgroundPaint.setStyle(Paint.Style.FILL); foregroundPaint = new Paint(); foregroundPaint.setColor(DEFAULT_FG_COLOR); foregroundPaint.setStyle(Paint.Style.FILL); selectedAngle = 280; } @Override protected void onDraw(Canvas canvas) { int circleSize = getWidth(); if (getHeight() < circleSize) circleSize = getHeight(); int horMargin = (getWidth() - circleSize) / 2; int verMargin = (getHeight() - circleSize) / 2; // create a clipPath the first time if(clipPath == null) { int clipWidth = (int) (circleSize * 0.75); int clipX = (getWidth() - clipWidth) / 2; int clipY = (getHeight() - clipWidth) / 2; clipPath = new Path(); clipPath.addArc( clipX, clipY, clipX + clipWidth, clipY + clipWidth, 0, 360); } canvas.clipPath(clipPath, Region.Op.DIFFERENCE); canvas.save(); canvas.rotate(-90, getWidth() / 2, getHeight() / 2); canvas.drawArc( horMargin, verMargin, horMargin + circleSize, verMargin + circleSize, 0, 360, true, backgroundPaint); canvas.drawArc( horMargin, verMargin, horMargin + circleSize, verMargin + circleSize, 0, selectedAngle, true, foregroundPaint); canvas.restore(); } }
The whole example source code can be found in the Example09-BasicRendering folder in the GitHub repository.
Furthermore, I gave a talk about this at the Android Developer's Backstage in Krakow in January 2015; here is a link to the presentation:
https://www.slideshare.net/RaimonRls/android-custom-views-72600098.