Skip to content

Instantly share code, notes, and snippets.

@momvart
Last active August 28, 2021 14:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save momvart/98ed97b56f9877aa4f38f7aa31ffbd7d to your computer and use it in GitHub Desktop.
Save momvart/98ed97b56f9877aa4f38f7aa31ffbd7d to your computer and use it in GitHub Desktop.
My simple implementation of a circular progress bar with support of animation in Android
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.AnimationUtils
import android.view.animation.Interpolator
import org.jetbrains.anko.displayMetrics
import kotlin.math.absoluteValue
import kotlin.math.pow
class CircularProgressBar @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
companion object {
const val DEFAULT_MAX_DURATION = 500
private const val DURATION_DECELERATION_FACTOR = 4f
private const val DEFAULT_STROKE_WIDTH_DP = 4f
}
var isAnimationEnabled: Boolean = true
var maxAnimationDuration: Long = DEFAULT_MAX_DURATION.toLong()
var animationInterpolator: Interpolator = AccelerateDecelerateInterpolator()
private val paint: Paint = Paint().apply {
isAntiAlias = true
style = Paint.Style.STROKE
}
var color: Int
get() = paint.color
set(value) {
paint.color = value
}
private val drawRect = RectF()
var thickness: Float
get() = paint.strokeWidth
set(value) {
paint.strokeWidth = value
updateDrawRect(width, height, value)
}
var startAngle: Float by equalityVetoObservable(0f, { invalidate() })
private var fillDirectionFactor by equalityVetoObservable(1f, { invalidate() })
var isClockwise: Boolean by equalityVetoObservable(true, { newValue ->
fillDirectionFactor = if (newValue) 1f else -1f
invalidate()
})
var progress: Float
get() = internalProgress
set(value) {
val value = value.coerceIn(0f, 1f)
if (internalProgress == value) return
if (isAnimationEnabled)
restartProgressAnimation(value);
else
internalProgress = value
}
//We keep internal progress safe (value in range of [0, 1] from outside to prevent repeated checks
private var internalProgress: Float = 0f
get
set(value) {
field = value
invalidate()
}
private var progressAnimator: ValueAnimator? = null
init {
val a = context.obtainStyledAttributes(
attrs,
R.styleable.CircularProgressBar,
R.attr.circularProgressBarStyle,
R.style.MyCircularProgressBarStyle
)
thickness = a.getDimension(
R.styleable.CircularProgressBar_android_thickness,
context.displayMetrics.density * DEFAULT_STROKE_WIDTH_DP
)
color = a.getColor(R.styleable.CircularProgressBar_android_color, Color.BLACK)
internalProgress = a.getFloat(R.styleable.CircularProgressBar_progress, 0f)
startAngle = a.getFloat(R.styleable.CircularProgressBar_startAngle, 0f)
isClockwise = a.getBoolean(R.styleable.CircularProgressBar_clockwise, true)
isAnimationEnabled = a.getBoolean(R.styleable.CircularProgressBar_enableAnimation, true)
maxAnimationDuration =
a.getInt(R.styleable.CircularProgressBar_maxAnimationDuration, DEFAULT_MAX_DURATION)
.toLong()
val interpolatorId = a.getResourceId(
R.styleable.CircularProgressBar_android_interpolator,
android.R.interpolator.accelerate_decelerate
)
animationInterpolator = AnimationUtils.loadInterpolator(context, interpolatorId)
a.recycle()
}
private fun restartProgressAnimation(newProgress: Float) {
progressAnimator?.cancel()
progressAnimator = ValueAnimator.ofFloat(progress, newProgress).apply {
addUpdateListener { internalProgress = it.animatedValue as Float }
duration = calcChangeAnimDuration(newProgress - progress)
interpolator = animationInterpolator
}.apply { start() }
}
//Using deceleration formula to calculate the animation duration
private fun calcChangeAnimDuration(totalChange: Float) =
((1 - (1 - totalChange.absoluteValue).pow(DURATION_DECELERATION_FACTOR)) * maxAnimationDuration).toLong()
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
updateDrawRect(w, h, this.thickness)
}
private fun updateDrawRect(width: Int, height: Int, thickness: Float) {
with(drawRect)
{
val half = thickness / 2
left = half + paddingLeft
top = half + paddingTop
right = width.toFloat() - half - paddingRight
bottom = height.toFloat() - half - paddingBottom
}
}
override fun draw(canvas: Canvas) {
super.draw(canvas)
canvas.drawArc(drawRect, startAngle, fillDirectionFactor * progress * 360, false, paint)
}
}
<resources>
<declare-styleable name="Theme">
<attr name="circularProgressBarStyle" format="reference" />
</declare-styleable>
<declare-styleable name="CircularProgressBar">
<attr name="android:color" />
<attr name="android:thickness" />
<attr name="progress" format="float" />
<attr name="startAngle" format="float" />
<attr name="clockwise" format="boolean" />
<attr name="enableAnimation" format="boolean" />
<attr name="maxAnimationDuration" format="integer" />
<attr name="android:interpolator" format="reference" />
</declare-styleable>
</resources>
<resources>
<style name="BaseAppTheme">
<item name="circularProgressBarStyle">@style/MyCircularProgressBarStyle</item>
</style>
<style name="MyCircularProgressBarStyle">
<item name="android:thickness">4dp</item>
<item name="android:color">?colorPrimary</item>
<item name="progress">0</item>
<item name="startAngle">-90</item>
<item name="clockwise">true</item>
</style>
</resources>
import kotlin.properties.ObservableProperty
import kotlin.reflect.KProperty
inline fun <T> equalityVetoObservable(
initialValue: T,
crossinline afterChange: (newValue: T) -> Unit,
crossinline equalityCheck: (oldValue: T, newValue: T) -> Boolean = { o, n -> o != n }
) =
object : ObservableProperty<T>(initialValue) {
override fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean =
equalityCheck(oldValue, newValue)
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) =
afterChange(newValue)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment