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.
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.
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.
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.
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:
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().
Then back in your setProgress function, just add that object to your animation before you call .start() on it like this:
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!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
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!
LikeLike