class CircularImageView : AppCompatImageView {
private val mDrawableRect = RectF()
private val mBorderRect = RectF()
private val mShaderMatrix = Matrix()
private val mBitmapPaint = Paint()
private val mBorderPaint = Paint()
private val mFillPaint = Paint()
private var mBorderColor = DEFAULT_BORDER_COLOR
private var mBorderWidth = DEFAULT_BORDER_WIDTH
private var mFillColor = DEFAULT_FILL_COLOR
private var mBitmap: Bitmap? = null
private var mBitmapShader: BitmapShader? = null
private var mBitmapWidth: Int = 0
private var mBitmapHeight: Int = 0
private var mDrawableRadius: Float = 0.toFloat()
private var mBorderRadius: Float = 0.toFloat()
private var mColorFilter: ColorFilter? = null
private var mReady: Boolean = false
private var mSetupPending: Boolean = false
private var mBorderOverlay: Boolean = false
private val barLength = 16
//Sizes (with defaults in DP)
private var circleRadius = 28
private var barWidth = 4
private var rimWidth = 4
private var fillRadius = false
private var timeStartGrowing = 0.0
private var barSpinCycleTime = 460.0
private var barExtraLength = 0f
private var barGrowingFromFront = true
private var pausedTimeWithoutGrowing: Long = 0
//Colors (with defaults)
private var barColor = -0x56000000
private var rimColor = 0x00FFFFFF
//Paints
private val barPaint = Paint()
private val rimPaint = Paint()
//Rectangles
private var circleBounds = RectF()
//Animation
//The amount of degrees per second
private var spinSpeed = 230.0f
// The last time the spinner was animated
private var lastTimeAnimated: Long = 0
private var linearProgress: Boolean = false
var isProgressVisible: Boolean = false
private var mProgress = 0.0f
private var mTargetProgress = 0.0f
private var isSpinning = false
private var callback: ProgressCallback? = null
private var mFadEnable: Boolean=false
private var fadPaint: Paint? = null
private var fad: Int = 0
private var textPaint: Paint? = null
private var text: String? = null
private var isBorderRequired: Boolean = false
constructor(context: Context) : super(context) {
init()
}
constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0) {
parseAttributes(context.obtainStyledAttributes(attrs,
R.styleable.ProgressWheel))
}
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) {
val a = context.obtainStyledAttributes(attrs, R.styleable.CircularImageView, defStyle, 0)
mBorderWidth = a.getDimensionPixelSize(R.styleable.CircularImageView_civ_border_width, DEFAULT_BORDER_WIDTH)
mBorderColor = a.getColor(R.styleable.CircularImageView_civ_border_color, DEFAULT_BORDER_COLOR)
mBorderOverlay = a.getBoolean(R.styleable.CircularImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY)
mFadEnable = a.getBoolean(R.styleable.CircularImageView_civ_fad_overlay, DEFAULT_BORDER_OVERLAY)
mFillColor = a.getColor(R.styleable.CircularImageView_civ_fill_color, DEFAULT_FILL_COLOR)
a.recycle()
init()
}
private fun init() {
super.setScaleType(SCALE_TYPE)
mReady = true
if (mSetupPending) {
setup()
mSetupPending = false
}
}
override fun getScaleType(): ImageView.ScaleType {
return SCALE_TYPE
}
override fun setScaleType(scaleType: ImageView.ScaleType) {
if (scaleType != SCALE_TYPE) {
throw IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType))
}
}
override fun setAdjustViewBounds(adjustViewBounds: Boolean) {
if (adjustViewBounds) {
throw IllegalArgumentException("adjustViewBounds not supported.")
}
}
override fun onDraw(canvas: Canvas) {
if (mBitmap != null) {
//draw image
if (mFillColor != Color.TRANSPARENT) {
canvas.drawCircle(width / 2.0f, height / 2.0f, mDrawableRadius, mFillPaint)
}
canvas.drawCircle(width / 2.0f, height / 2.0f, mDrawableRadius, mBitmapPaint)
if (mBorderWidth != 0) {
canvas.drawCircle(width / 2.0f, height / 2.0f, mBorderRadius, mBorderPaint)
}
isProgressVisible = false
} else if (text != null && !text!!.isEmpty() && textPaint != null) {
if (isBorderRequired && mBorderWidth != 0) {
canvas.drawCircle(width / 2.0f, height / 2.0f, mBorderRadius, mBorderPaint)
}
canvas.drawText(text!!, width / 2.0f - textPaint!!.measureText(text) / 2, height / 2.0f - (textPaint!!.descent() + textPaint!!.ascent()) / 2, textPaint!!)
}
if (mFadEnable && fadPaint != null) {
fadPaint!!.alpha = fad
canvas.drawCircle(width / 2.0f, height / 2.0f, mDrawableRadius, fadPaint!!)
}
//draw progress bar
if (isProgressVisible) {
drawProgressBar(canvas)
}
}
fun loaderState(state: Boolean) {
isProgressVisible = state
}
private fun drawProgressBar(canvas: Canvas) {
canvas.drawArc(circleBounds, 360f, 360f, false, rimPaint)
var mustInvalidate = false
if (isSpinning) {
//Draw the spinning bar
mustInvalidate = true
val deltaTime = SystemClock.uptimeMillis() - lastTimeAnimated
val deltaNormalized = deltaTime * spinSpeed / 1000.0f
updateBarLength(deltaTime)
mProgress += deltaNormalized
if (mProgress > 360) {
mProgress -= 360f
// A full turn has been completed
// we run the callback with -1 in case we want to
// do something, like changing the color
runCallback(-1.0f)
}
lastTimeAnimated = SystemClock.uptimeMillis()
var from = mProgress - 90
var length = barLength + barExtraLength
if (isInEditMode) {
from = 0f
length = 135f
}
canvas.drawArc(circleBounds, from, length, false,
barPaint)
} else {
val oldProgress = mProgress
if (mProgress != mTargetProgress) {
//We smoothly increase the progress bar
mustInvalidate = true
val deltaTime = (SystemClock.uptimeMillis() - lastTimeAnimated).toFloat() / 1000
val deltaNormalized = deltaTime * spinSpeed
mProgress = Math.min(mProgress + deltaNormalized, mTargetProgress)
lastTimeAnimated = SystemClock.uptimeMillis()
}
if (oldProgress != mProgress) {
runCallback()
}
var offset = 0.0f
var progress = mProgress
if (!linearProgress) {
val factor = 2.0f
offset = (1.0f - Math.pow((1.0f - mProgress / 360.0f).toDouble(), (2.0f * factor).toDouble())).toFloat() * 360.0f
progress = (1.0f - Math.pow((1.0f - mProgress / 360.0f).toDouble(), factor.toDouble())).toFloat() * 360.0f
}
if (isInEditMode) {
progress = 360f
}
canvas.drawArc(circleBounds, offset - 90, progress, false, barPaint)
}
if (mustInvalidate) {
invalidate()
}
}
override fun onSizeChanged(w: Int, h: Int, oldW: Int, oldH: Int) {
super.onSizeChanged(w, h, oldW, oldH)
setUpBounds(w, h)
setupPaints()
setup()
}
override fun setImageBitmap(bm: Bitmap) {
super.setImageBitmap(bm)
mBitmap = bm
setup()
}
override fun setImageDrawable(drawable: Drawable?) {
super.setImageDrawable(drawable)
mBitmap = getBitmapFromDrawable(drawable)
setup()
}
override fun setImageResource(@DrawableRes resId: Int) {
super.setImageResource(resId)
mBitmap = getBitmapFromDrawable(drawable)
setup()
}
override fun setImageURI(uri: Uri?) {
super.setImageURI(uri)
mBitmap = if (uri != null) getBitmapFromDrawable(drawable) else null
setup()
}
override fun setColorFilter(cf: ColorFilter) {
if (cf === mColorFilter) {
return
}
mColorFilter = cf
mBitmapPaint.colorFilter = mColorFilter
invalidate()
}
private fun getBitmapFromDrawable(drawable: Drawable?): Bitmap? {
if (drawable == null) {
return null
}
if (drawable is BitmapDrawable) {
return drawable.bitmap
}
try {
val bitmap: Bitmap
if (drawable is ColorDrawable) {
bitmap = Bitmap.createBitmap(COLOR_DRAWABLE_DIMENSION, COLOR_DRAWABLE_DIMENSION, BITMAP_CONFIG)
} else {
bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, BITMAP_CONFIG)
}
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
} catch (e: Exception) {
Log.d("error", e.toString())
return null
}
}
private fun setup() {
if (!mReady) {
mSetupPending = true
return
}
if (width == 0 && height == 0) {
return
}
mBorderPaint.style = Paint.Style.STROKE
mBorderPaint.isAntiAlias = true
mBorderPaint.color = mBorderColor
mBorderPaint.strokeWidth = mBorderWidth.toFloat()
mFillPaint.style = Paint.Style.FILL
mFillPaint.isAntiAlias = true
mFillPaint.color = mFillColor
mBorderRect.set(0f, 0f, width.toFloat(), height.toFloat())
mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2.0f, (mBorderRect.width() - mBorderWidth) / 2.0f)
mDrawableRect.set(mBorderRect)
if (!mBorderOverlay) {
mDrawableRect.inset(mBorderWidth.toFloat(), mBorderWidth.toFloat())
}
mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f)
if (mBitmap == null) {
invalidate()
return
}
mBitmapShader = BitmapShader(mBitmap!!, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
mBitmapPaint.isAntiAlias = true
mBitmapPaint.shader = mBitmapShader
mBitmapHeight = mBitmap!!.height
mBitmapWidth = mBitmap!!.width
updateShaderMatrix()
invalidate()
}
fun setBorderColor(color: Int) {
mBorderColor = color
mBorderPaint.color = mBorderColor
invalidate()
}
private fun updateShaderMatrix() {
val scale: Float
var dx = 0f
var dy = 0f
mShaderMatrix.set(null)
if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) {
scale = mDrawableRect.height() / mBitmapHeight.toFloat()
dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f
} else {
scale = mDrawableRect.width() / mBitmapWidth.toFloat()
dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f
}
mShaderMatrix.setScale(scale, scale)
mShaderMatrix.postTranslate((dx + 0.5f).toInt() + mDrawableRect.left, (dy + 0.5f).toInt() + mDrawableRect.top)
mBitmapShader!!.setLocalMatrix(mShaderMatrix)
}
fun createFadPaint(color: String) {
fadPaint = Paint()
fadPaint!!.style = Paint.Style.FILL
fadPaint!!.isAntiAlias = true
fadPaint!!.color = Color.parseColor(color)
}
fun createTextPaint(color: String, fontSize: Int, isBorderRequired: Boolean) {
textPaint = Paint()
textPaint!!.style = Paint.Style.FILL
textPaint!!.isAntiAlias = true
textPaint!!.color = Color.parseColor(color)
textPaint!!.textSize = fontSize.toFloat()
this.isBorderRequired = isBorderRequired
}
fun setText(text: String) {
this.text = text
mBitmap = null
Log.i(">>>", "" + text)
invalidate()
}
fun setFad(fad: Int) {
this.fad = fad
Log.i(">>>", "" + fad)
invalidate()
}
interface ProgressCallback {
fun onProgressUpdate(progress: Float)
}
private fun parseAttributes(a: TypedArray) {
// We transform the default values from DIP to pixels
val metrics = context.resources.displayMetrics
barWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, barWidth.toFloat(), metrics).toInt()
rimWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, rimWidth.toFloat(), metrics).toInt()
circleRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, circleRadius.toFloat(), metrics).toInt()
circleRadius = a.getDimension(R.styleable.ProgressWheel_matProg_circleRadius, circleRadius.toFloat()).toInt()
fillRadius = a.getBoolean(R.styleable.ProgressWheel_matProg_fillRadius, false)
barWidth = a.getDimension(R.styleable.ProgressWheel_matProg_barWidth, barWidth.toFloat()).toInt()
rimWidth = a.getDimension(R.styleable.ProgressWheel_matProg_rimWidth, rimWidth.toFloat()).toInt()
val baseSpinSpeed = a.getFloat(R.styleable.ProgressWheel_matProg_spinSpeed, spinSpeed / 360.0f)
spinSpeed = baseSpinSpeed * 360
barSpinCycleTime = a.getInt(R.styleable.ProgressWheel_matProg_barSpinCycleTime, barSpinCycleTime.toInt()).toDouble()
barColor = a.getColor(R.styleable.ProgressWheel_matProg_barColor, barColor)
rimColor = a.getColor(R.styleable.ProgressWheel_matProg_rimColor, rimColor)
isProgressVisible = a.getBoolean(R.styleable.ProgressWheel_matProg_visible, false)
linearProgress = a.getBoolean(R.styleable.ProgressWheel_matProg_linearProgress, false)
if (a.getBoolean(R.styleable.ProgressWheel_matProg_progressIndeterminate, false)) {
spin()
}
// Recycle
a.recycle()
}
private fun spin() {
lastTimeAnimated = SystemClock.uptimeMillis()
isSpinning = true
invalidate()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val viewWidth = circleRadius + this.paddingLeft + this.paddingRight
val viewHeight = circleRadius + this.paddingTop + this.paddingBottom
val widthMode = View.MeasureSpec.getMode(widthMeasureSpec)
val widthSize = View.MeasureSpec.getSize(widthMeasureSpec)
val heightMode = View.MeasureSpec.getMode(heightMeasureSpec)
val heightSize = View.MeasureSpec.getSize(heightMeasureSpec)
val width: Int
val height: Int
//Measure Width
if (widthMode == View.MeasureSpec.EXACTLY) {
//Must be this size
width = widthSize
} else if (widthMode == View.MeasureSpec.AT_MOST) {
//Can't be bigger than...
width = Math.min(viewWidth, widthSize)
} else {
//Be whatever you want
width = viewWidth
}
//Measure Height
if (heightMode == View.MeasureSpec.EXACTLY || widthMode == View.MeasureSpec.EXACTLY) {
//Must be this size
height = heightSize
} else if (heightMode == View.MeasureSpec.AT_MOST) {
//Can't be bigger than...
height = Math.min(viewHeight, heightSize)
} else {
//Be whatever you want
height = viewHeight
}
setMeasuredDimension(width, height)
}
private fun setUpBounds(layoutWidth: Int, layoutHeight: Int) {
val paddingTop = paddingTop
val paddingBottom = paddingBottom
val paddingLeft = paddingLeft
val paddingRight = paddingRight
if (!fillRadius) {
// Width should equal to Height, find the min value to setup the circle
val minValue = Math.min(layoutWidth - paddingLeft - paddingRight,
layoutHeight - paddingBottom - paddingTop)
val circleDiameter = Math.min(minValue, circleRadius * 2 - barWidth * 2)
// Calc the Offset if needed for centering the wheel in the available space
val xOffset = (layoutWidth - paddingLeft - paddingRight - circleDiameter) / 2 + paddingLeft
val yOffset = (layoutHeight - paddingTop - paddingBottom - circleDiameter) / 2 + paddingTop
circleBounds = RectF((xOffset + barWidth).toFloat(),
(yOffset + barWidth).toFloat(),
(xOffset + circleDiameter - barWidth).toFloat(),
(yOffset + circleDiameter - barWidth).toFloat())
} else {
circleBounds = RectF((paddingLeft + barWidth).toFloat(),
(paddingTop + barWidth).toFloat(),
(layoutWidth - paddingRight - barWidth).toFloat(),
(layoutHeight - paddingBottom - barWidth).toFloat())
}
}
private fun setupPaints() {
barPaint.color = barColor
barPaint.isAntiAlias = true
barPaint.style = Paint.Style.STROKE
barPaint.strokeWidth = barWidth.toFloat()
rimPaint.color = rimColor
rimPaint.isAntiAlias = true
rimPaint.style = Paint.Style.STROKE
rimPaint.strokeWidth = rimWidth.toFloat()
}
fun setCallback(progressCallback: ProgressCallback) {
callback = progressCallback
if (!isSpinning) {
runCallback()
}
}
private fun runCallback() {
if (callback != null) {
val normalizedProgress = Math.round(mProgress * 100 / 360.0f).toFloat() / 100
callback!!.onProgressUpdate(normalizedProgress)
}
}
private fun runCallback(value: Float) {
if (callback != null) {
callback!!.onProgressUpdate(value)
}
}
override fun onVisibilityChanged(changedView: View, visibility: Int) {
super.onVisibilityChanged(changedView, visibility)
if (visibility == View.VISIBLE) {
lastTimeAnimated = SystemClock.uptimeMillis()
}
}
private fun updateBarLength(deltaTimeInMilliSeconds: Long) {
if (pausedTimeWithoutGrowing >= 200) {
timeStartGrowing += deltaTimeInMilliSeconds.toDouble()
if (timeStartGrowing > barSpinCycleTime) {
// We completed a size change cycle
// (growing or shrinking)
timeStartGrowing -= barSpinCycleTime
pausedTimeWithoutGrowing = 0
barGrowingFromFront = !barGrowingFromFront
}
val distance = Math.cos((timeStartGrowing / barSpinCycleTime + 1) * Math.PI).toFloat() / 2 + 0.5f
val destLength = 270.toFloat() - barLength
if (barGrowingFromFront) {
barExtraLength = distance * destLength
} else {
val newLength = destLength * (1 - distance)
mProgress += barExtraLength - newLength
barExtraLength = newLength
}
} else {
pausedTimeWithoutGrowing += deltaTimeInMilliSeconds
}
}
fun stopSpinning() {
isSpinning = false
mProgress = 0.0f
mTargetProgress = 0.0f
invalidate()
}
public override fun onSaveInstanceState(): Parcelable? {
val superState = super.onSaveInstanceState()
val ss = WheelSavedState(superState)
// We save everything that can be changed at runtime
ss.mProgress = this.mProgress
ss.mTargetProgress = this.mTargetProgress
ss.isSpinning = this.isSpinning
ss.spinSpeed = this.spinSpeed
ss.barWidth = this.barWidth
ss.barColor = this.barColor
ss.rimWidth = this.rimWidth
ss.rimColor = this.rimColor
ss.circleRadius = this.circleRadius
ss.linearProgress = this.linearProgress
ss.fillRadius = this.fillRadius
return ss
}
public override fun onRestoreInstanceState(state: Parcelable) {
if (state !is WheelSavedState) {
super.onRestoreInstanceState(state)
return
}
super.onRestoreInstanceState(state.superState)
this.mProgress = state.mProgress
this.mTargetProgress = state.mTargetProgress
this.isSpinning = state.isSpinning
this.spinSpeed = state.spinSpeed
this.barWidth = state.barWidth
this.barColor = state.barColor
this.rimWidth = state.rimWidth
this.rimColor = state.rimColor
this.circleRadius = state.circleRadius
this.linearProgress = state.linearProgress
this.fillRadius = state.fillRadius
this.lastTimeAnimated = SystemClock.uptimeMillis()
}
internal class WheelSavedState : View.BaseSavedState {
var mProgress: Float = 0.toFloat()
var mTargetProgress: Float = 0.toFloat()
var isSpinning: Boolean = false
var spinSpeed: Float = 0.toFloat()
var barWidth: Int = 0
var barColor: Int = 0
var rimWidth: Int = 0
var rimColor: Int = 0
var circleRadius: Int = 0
var linearProgress: Boolean = false
var fillRadius: Boolean = false
constructor(superState: Parcelable) : super(superState) {}
private constructor(`in`: Parcel) : super(`in`) {
this.mProgress = `in`.readFloat()
this.mTargetProgress = `in`.readFloat()
this.isSpinning = `in`.readByte().toInt() != 0
this.spinSpeed = `in`.readFloat()
this.barWidth = `in`.readInt()
this.barColor = `in`.readInt()
this.rimWidth = `in`.readInt()
this.rimColor = `in`.readInt()
this.circleRadius = `in`.readInt()
this.linearProgress = `in`.readByte().toInt() != 0
this.fillRadius = `in`.readByte().toInt() != 0
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeFloat(this.mProgress)
out.writeFloat(this.mTargetProgress)
out.writeByte((if (isSpinning) 1 else 0).toByte())
out.writeFloat(this.spinSpeed)
out.writeInt(this.barWidth)
out.writeInt(this.barColor)
out.writeInt(this.rimWidth)
out.writeInt(this.rimColor)
out.writeInt(this.circleRadius)
out.writeByte((if (linearProgress) 1 else 0).toByte())
out.writeByte((if (fillRadius) 1 else 0).toByte())
}
companion object {
//required field that makes Parcelables from a Parcel
val CREATOR: Parcelable.Creator<WheelSavedState> = object : Parcelable.Creator<WheelSavedState> {
override fun createFromParcel(`in`: Parcel): WheelSavedState {
return WheelSavedState(`in`)
}
override fun newArray(size: Int): Array<WheelSavedState?> {
return arrayOfNulls(size)
}
}
}
}
companion object {
private val SCALE_TYPE = ImageView.ScaleType.CENTER_CROP
private val BITMAP_CONFIG = Bitmap.Config.ARGB_8888
private val COLOR_DRAWABLE_DIMENSION = 2
private val DEFAULT_BORDER_WIDTH = 0
private val DEFAULT_BORDER_COLOR = Color.BLACK
private val DEFAULT_FILL_COLOR = Color.TRANSPARENT
private val DEFAULT_BORDER_OVERLAY = false
}
}