Last active
August 28, 2021 14:07
-
-
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
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.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) | |
} | |
} |
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
<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> |
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
<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> |
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 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