Basics of building a circular progress view
There are many libraries out there of fancy circular loading views based on percentages and whatnot, but they do not always cover all the things developers and/or organization want them to do. Honestly, I never looked at the source code for the circular loading libraries, and over the past week due to a project I’m working on, wondered how it was done without using a library. This post will explain the starting point for a custom circular view.
The problem can be solved drawing arcs on a canvas arcs, instead of circles.
Lets take a look at what we want on paper before beginning.
From the bad drawing above, we can see that we need 2 arcs. One to represent the normal, un-filled circle, and another inside of it to represent the filled up percentage. Lets start with the main arc.
We create a custom class that extends View and draw the parent arc. The arc that represents the circular view without any percentages displayed.
class CircularProgressView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
// required to draw the arcs
private val rectF = RectF()
// Used to draw pretty much anything on a canvas, which is what we will be drawing on
private val paint = Paint().apply {
// how we want the arcs to be draw, we want to make sure the arc centers are not colored
// so we use a STROKE instead.
style = Paint.Style.STROKE
}
// ...
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.let {
rectF.apply {
val width = (it.width.div(2)).toFloat() // center X of the canvas
val height = (it.height.div(2)).toFloat() // center Y of the canvas
// place the rectF it at the center of the screen with height and width of 200dp
set(width - 200, height - 200, width + 200, height + 200)
}
// draw an arc that will represent an empty loading view
it.drawArc(rectF, 0f, 360f, false, paint.apply {
color = Color.GRAY
// how wide the stroke should be, typically more than or equal to the strokeWidth
// of the arc representing a filled percentage
strokeWidth = 60f
})
// ...
}
}
}
The RectF is required in one of the Canvas#drawArc() overloaded methods.
RectF holds four float coordinates for a rectangle. The rectangle is represented by the coordinates of its 4 edges (left, top, right bottom). These fields can be accessed directly. Use width() and height() to retrieve the rectangle’s width and height. Note: most methods do not check to see that the coordinates are sorted correctly (i.e. left <= right and top <= bottom).
It is important that we realize that the style of the Paint object is set to Stroke. All Paint styles are straight forward, meaning that the center of the shape that the arc draws will not be filled up, only the borders will be shown.
By running this code alone, we will see a single arc drawn at the center of the screen.
Ok, so we know that an arc can pretty much draw pieces of a circle, in the example above we draw the circle, from 0 degrees (start point) to 360 (end point). We need to draw another arc on top of the existing arc with a different color (lets say green), to represent the percentage wanted. Normally, we think of percentages for anything going from 0% to 100%. So how can we do this when the arcs 100% is 360 degrees? With very simple math: fillPercentage = 360 (PERCENTAGE_WE_WANT / 100)
So if we wanted to draw a 25% fill to our existing arc, we would replace PERCENTAGE_WE_WANT with 25 to get the percentage for 360 degrees.
class CircularProgressView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
// ...
// default percentage set to 0
private var percentage = 0
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.let {
// ...
// get the actual percentage as a float
val fillPercentage = (360 * (percentage / 100.0)).toFloat()
// draw the arc that will represent the percentage filled up
it.drawArc(rectF, 270f, fillPercentage, false, paint.apply {
color = Color.GREEN // filled percentage color
// how wide the stroke should be, typically less than or equal to the strokeWidth
// of the empty arc
strokeWidth = 30f
})
}
}
// ...
}
Thats pretty much it. The code to get started is extremely simple, but can be used to create complex circular loading views. I hope this helps.
Here is the full code for the CircularProgressView class. Full code with sample can be found HERE.
class CircularProgressView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
// required to draw the arcs
private val rectF = RectF()
// Used to draw pretty much anything on a canvas, which is what we will be drawing on
private val paint = Paint().apply {
// how we want the arcs to be draw, we want to make sure the arc centers are not colored
// so we use a STROKE instead.
style = Paint.Style.STROKE
}
// default percentage set to 0
private var percentage = 0
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.let {
rectF.apply {
val width = (it.width.div(2)).toFloat() // center X of the canvas
val height = (it.height.div(2)).toFloat() // center Y of the canvas
// place the rectF it at the center of the screen with height and width of 200dp
set(width - 200, height - 200, width + 200, height + 200)
}
// draw an arc that will represent an empty loading view
it.drawArc(rectF, 0f, 360f, false, paint.apply {
color = Color.GRAY
// how wide the stroke should be, typically more than or equal to the strokeWidth
// of the arc representing a filled percentage
strokeWidth = 60f
})
// get the actual percentage as a float
val fillPercentage = (360 * (percentage / 100.0)).toFloat()
// draw the arc that will represent the percentage filled up
it.drawArc(rectF, 270f, fillPercentage, false, paint.apply {
color = Color.GREEN // filled percentage color
// how wide the stroke should be, typically less than or equal to the strokeWidth
// of the empty arc
strokeWidth = 30f
})
}
}
fun setPercentage(percentage: Int) {
this.percentage = percentage
invalidate()
}
}