Creating animating circular views in Android

Last week I demonstrated how to create a circular progress view in iOS that animated the progress layer when the value changed. This week I’ll show you how to do the same thing in Android. We’ll be using Kotlin for this, but converting it to Java shouldn’t be too difficult if you’re still into that.

ezgif-5-623e0b5518

what we’ll be making

So first we need to define our Paint objects. We need a track paint and a progress paint, and then we’ll set them up in our init {} method. We also need a trackWidth and colours for the two tracks.

Screen Shot 2018-03-27 at 17.18.10.png

Next we need to override onMeasure and onDraw so we can actually draw these circles. Create a new RectF property on your class and override onMeasure. Here, like in the iOS demo I did last week, we need to find which side is longest in order to make our circle fit within the bounds of the view. Then we need to initialise our RectF property.

Screen Shot 2017-12-10 at 15.08.18

You’ll notice when we create the arcBounds variable, we use 0.004 and multiply it by the trackWidth. Android does some weird stuff if we remove or change this. Removing it the (0.004 * trackWidth) will still make the track draw, but our progress track won’t draw or animate. Our circle will also clip at the edges. Through some trial and error I found 0.004 to always work, but I’m not 100% sure what it is or why the progress track doesn’t draw if it’s not there.

Now we’re ready for onDraw. Android provides two methods to help us draw a circle: drawCircle, and drawArc. For this we’re not actually gonna use drawCircle, but instead use drawArc, which allows us to pass through our RectF object.

Screen Shot 2017-12-10 at 15.15.18

Now if you add this new view to an activity xml and run, you should see it draw a circle in the view’s bounds! So now we just need to add a way of changing the progress of the progressLayer. To do this we’re gonna use the ValueAnimator.ofFloat.

Create a new function like changeProgress(newProgress: Double) and in here create a ValueAnimator object, passing in the current progress value and the new progress value. ValueAnimator has a few static functions such as ofFloat, ofInt, ofDouble, etc. A ValueAnimator object will change that value you give it over the time you specify from the initial value, to the final value. For a ValueAnimator, you need to call the addUpdateListener, which will just give you the new value, which we’re storing in a property and calling invalidate. Invalidate will make onDraw be called, and since in our inDraw method we take the progress property into account to draw our progress track, the path animates. ValueAnimator has a bunch of other stuff you can set on it, such as an interpolator, a repeat count, repeat mode, etc. There’s a bunch to make use of here. ObjectAnimator is a subclass of ValueAnimator, which doesn’t require you to add the update listener, as it takes a few more parameters in when you create it.

Set a duration of the animation and add an update listener. In here we just set the current progress value to the animated value of the ValueAnimator object we get passed in the lambda and call invalidate, which will cause onDraw to get called again. Then just call .start() on the animation object. Something like this:

Screen Shot 2017-12-10 at 15.26.37.png

Call this function from your activity and you’ll see the progress view animate to the progress value you pass in! Now the last thing to do is create a block for when the animation finishes. This is simple enough in Kotlin. First create a new optional property of: var onAnimationFinished: (() -> Unit)? = null

Then create another property, an object, which conforms to Animator.AnimatorListener. Implement the four methods and in onAnimationEnd, call the block like onAnimationFinished?.invoke().

Screen Shot 2017-12-10 at 15.34.11

Then back in your setProgress function, just add that object to your animation before you call .start() on it like this:

Screen Shot 2017-12-10 at 15.35.39

And  there we go. Now you can go ahead and add your styled attributes to the view and grab them out in the init {} method if you want, and you’re done! Hope you enjoyed this little mini-series on custom views and animations in iOS and Android! See you next week!


import android.animation.Animator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.support.annotation.ColorRes
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
class ProgressView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0) : View(context, attrs, defStyleAttr, defStyleRes) {
private val trackPaint = Paint()
private val progressPaint = Paint()
private val rectF = RectF()
private var progress = 0.0
private var animation: ValueAnimator? = null
private val animatorListener = object : Animator.AnimatorListener {
override fun onAnimationRepeat(p0: Animator?) {
}
override fun onAnimationCancel(p0: Animator?) {
}
override fun onAnimationStart(p0: Animator?) {
}
override fun onAnimationEnd(p0: Animator?) {
onAnimationFinished?.invoke()
}
}
@ColorRes
var trackColor = Color.LTGRAY
@ColorRes
var progressColor = Color.GRAY
var trackWidth = 15f
var onAnimationFinished: (() -> Unit)? = null
init {
val trackWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, trackWidth, resources.displayMetrics)
trackPaint.color = trackColor
trackPaint.isAntiAlias = true
trackPaint.style = Paint.Style.STROKE
trackPaint.strokeWidth = trackWidth
progressPaint.color = progressColor
progressPaint.isAntiAlias = true
progressPaint.style = Paint.Style.STROKE
progressPaint.strokeWidth = trackWidth
progressPaint.strokeCap = Paint.Cap.ROUND
}
fun setProgress(newProgress: Double, duration: Long = 500) {
animation?.cancel()
animation = ValueAnimator.ofFloat(progress.toFloat(), newProgress.toFloat())
animation?.duration = duration
animation?.addUpdateListener {
progress = (it.animatedValue as Float).toDouble()
invalidate()
}
animation?.addListener(animatorListener)
animation?.start()
invalidate()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val longestSide = if (measuredHeight < measuredWidth) measuredHeight else measuredWidth
val arcBounds = (longestSide * (0.004 * trackWidth)).toInt()
rectF.set(arcBounds.toFloat(), arcBounds.toFloat(), (longestSide arcBounds).toFloat(), (longestSide arcBounds).toFloat())
}
override fun onDraw(canvas: Canvas?) {
canvas?.drawArc(rectF, 90f, 360f, false, trackPaint)
canvas?.drawArc(rectF, 90f, 360f / 100 * (progress.toFloat()), false, progressPaint)
}
}

view raw

ProgressView.kt

hosted with ❤ by GitHub

One thought on “Creating animating circular views in Android”

  1. Wow, superb weblog structure! How lengthy have you ever been blogging for? you make blogging look easy. The entire look of your site is magnificent, let alone the content material!

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s