Skip to content

Instantly share code, notes, and snippets.

@nicemak
Last active October 25, 2022 05:56
Show Gist options
  • Save nicemak/7550892aa0e201fc856fd11ad3b26419 to your computer and use it in GitHub Desktop.
Save nicemak/7550892aa0e201fc856fd11ad3b26419 to your computer and use it in GitHub Desktop.
CircularTimerView
<declare-styleable name="CircularTimerView">
<attr name="progressColor" format="color"/>
<attr name="backgroundColor" format="color"/>
<attr name="progressBackgroundColor" format="color"/>
<attr name="strokeWidthDimension" format="dimension"/>
<attr name="backgroundWidth" format="float"/>
<attr name="roundedCorners" format="boolean"/>
<attr name="maxValue" format="float"/>
<attr name="progressTextColor" format="color"/>
<attr name="textSize" format="dimension"/>
<attr name="progressText" format="string"/>
<attr name="suffix" format="string"/>
<attr name="prefix" format="string"/>
<attr name="isClockwise" format="boolean"/>
<attr name="startingPoint" format="enum">
<enum name="top" value="270" />
<enum name="bottom" value="90" />
<enum name="right" value="0" />
<enum name="left" value="180" />
</attr>
</declare-styleable>
interface CircularTimerListener {
fun updateDataOnTick(remainingTimeInMs: Long): String?
fun onTimerFinished()
}
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.os.CountDownTimer
import android.text.TextPaint
import android.text.TextUtils
import android.util.AttributeSet
import android.view.View
import androidx.core.content.res.ResourcesCompat
import <package_name>.R
class CircularTimerView : View {
private var progressBarPaint: Paint? = null
private var progressBarBackgroundPaint: Paint? = null
private var backgroundPaint: Paint? = null
private lateinit var textPaint: Paint
private var mRadius = 0f
private val mArcBounds = RectF()
var drawUpto = 0f
constructor(context: Context?) : super(context) {
// create the Paint and set its color
}
private var progressColor = 0
private var progressBackgroundColor = 0
private var backgroundColor = 0
private var strokeWidthDimension = 0f
private var backgroundWidth = 0f
private var roundedCorners = false
private var maxValue = 0f
private var progressTextColor = Color.BLACK
private var textSize = 18f
private var text: String? = ""
private var suffix: String? = ""
private var prefix: String? = ""
private var isClockwise = true
private var startingAngle = 270
var defStyleAttr = 0
private var circularTimerListener: CircularTimerListener? = null
private var countDownTimer: CountDownTimer? = null
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
this.defStyleAttr = defStyleAttr
initPaints(context, attrs)
}
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
{
initPaints(context, attrs)
}
private fun initPaints(context: Context, attrs: AttributeSet?)
{
val ta =
context.obtainStyledAttributes(attrs, R.styleable.CircularTimerView, defStyleAttr, 0)
progressColor = ta.getColor(R.styleable.CircularTimerView_progressColor, Color.BLUE)
backgroundColor = ta.getColor(R.styleable.CircularTimerView_backgroundColor, Color.GRAY)
progressBackgroundColor =
ta.getColor(R.styleable.CircularTimerView_progressBackgroundColor, Color.GRAY)
strokeWidthDimension = ta.getFloat(R.styleable.CircularTimerView_strokeWidthDimension, 10f)
backgroundWidth = ta.getFloat(R.styleable.CircularTimerView_backgroundWidth, 10f)
roundedCorners = ta.getBoolean(R.styleable.CircularTimerView_roundedCorners, false)
maxValue = ta.getFloat(R.styleable.CircularTimerView_maxValue, 100f)
progressTextColor =
ta.getColor(R.styleable.CircularTimerView_progressTextColor, Color.BLACK)
textSize = ta.getDimension(R.styleable.CircularTimerView_textSize, 18f)
suffix = ta.getString(R.styleable.CircularTimerView_suffix)
prefix = ta.getString(R.styleable.CircularTimerView_prefix)
text = ta.getString(R.styleable.CircularTimerView_progressText)
isClockwise = ta.getBoolean(R.styleable.CircularTimerView_isClockwise, true)
startingAngle = ta.getInt(R.styleable.CircularTimerView_startingPoint, 270)
progressBarPaint = Paint(Paint.ANTI_ALIAS_FLAG)
progressBarPaint!!.style = Paint.Style.FILL
progressBarPaint!!.color = progressColor
progressBarPaint!!.style = Paint.Style.STROKE
progressBarPaint!!.strokeWidth = strokeWidthDimension * resources.displayMetrics.density
if (roundedCorners)
{
progressBarPaint!!.strokeCap = Paint.Cap.ROUND
}
else
{
progressBarPaint!!.strokeCap = Paint.Cap.BUTT
}
val pc = String.format("#%06X", 0xFFFFFF and progressColor)
progressBarPaint!!.color = Color.parseColor(pc)
progressBarBackgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
progressBarBackgroundPaint!!.style = Paint.Style.FILL
progressBarBackgroundPaint!!.color = progressBackgroundColor
progressBarBackgroundPaint!!.style = Paint.Style.STROKE
progressBarBackgroundPaint!!.strokeWidth =
backgroundWidth * resources.displayMetrics.density
progressBarBackgroundPaint!!.strokeCap = Paint.Cap.SQUARE
val bc = String.format("#%06X", 0xFFFFFF and progressBackgroundColor)
progressBarBackgroundPaint!!.color = Color.parseColor(bc)
backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
backgroundPaint!!.style = Paint.Style.FILL
backgroundPaint!!.color = backgroundColor
val bcfill = String.format("#%06X", 0xFFFFFF and backgroundColor)
backgroundPaint!!.color = Color.parseColor(bcfill)
ta.recycle()
textPaint = TextPaint()
textPaint.color = progressTextColor
val c = String.format("#%06X", 0xFFFFFF and progressTextColor)
textPaint.color = Color.parseColor(c)
textPaint.textSize = textSize
textPaint.isAntiAlias = true
//paint.setAntiAlias(true);
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mRadius = w.coerceAtMost(h) / 2f
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val w = MeasureSpec.getSize(widthMeasureSpec)
val h = MeasureSpec.getSize(heightMeasureSpec)
val size = w.coerceAtMost(h)
setMeasuredDimension(size, size)
}
override fun onDraw(canvas: Canvas)
{
super.onDraw(canvas)
val mouthInset = mRadius / 3
canvas.drawCircle(mRadius, mRadius, mouthInset * 2, backgroundPaint!!)
mArcBounds[mouthInset, mouthInset, mRadius * 2 - mouthInset] = mRadius * 2 - mouthInset
canvas.drawArc(mArcBounds, 0f, 360f, false, progressBarBackgroundPaint!!)
if (isClockwise)
{
canvas.drawArc(
mArcBounds, startingAngle.toFloat(),
drawUpto / getMaxValue() * 360, false,
progressBarPaint!!
)
}
else
{
canvas.drawArc(
mArcBounds, startingAngle.toFloat(),
drawUpto / getMaxValue() * -360, false,
progressBarPaint!!
)
}
if (TextUtils.isEmpty(suffix))
{
suffix = ""
}
if (TextUtils.isEmpty(prefix))
{
prefix = ""
}
val drawnText = prefix + text + suffix
if (!TextUtils.isEmpty(text))
{
val textHeight = textPaint.descent() + textPaint.ascent()
val customTypeface = ResourcesCompat.getFont(context, R.font.digital)
textPaint.typeface = customTypeface
textPaint.letterSpacing = 0.2f
canvas.drawText(
drawnText,
(width - textPaint.measureText(drawnText)) / 2.0f,
(width - textHeight) / 2.0f,
textPaint
)
}
}
override fun onDetachedFromWindow() {
if (countDownTimer != null) {
countDownTimer!!.cancel()
}
super.onDetachedFromWindow()
}
var progress: Float
get() = drawUpto
set(f) {
drawUpto = f
invalidate()
}
val progressPercentage: Float
get() = drawUpto / getMaxValue() * 100
fun setProgressColor(color: Int) {
progressColor = color
progressBarPaint!!.color = color
invalidate()
}
fun setProgressColor(color: String?) {
progressBarPaint!!.color = Color.parseColor(color)
invalidate()
}
override fun setBackgroundColor(color: Int) {
backgroundColor = color
backgroundPaint!!.color = color
invalidate()
}
fun setBackgroundColor(color: String?) {
backgroundPaint!!.color = Color.parseColor(color)
invalidate()
}
fun setProgressBackgroundColor(color: Int) {
progressBackgroundColor = color
progressBarBackgroundPaint!!.color = color
invalidate()
}
fun setProgressBackgroundColor(color: String?) {
progressBarBackgroundPaint!!.color = Color.parseColor(color)
invalidate()
}
fun getMaxValue(): Float {
return maxValue
}
fun setMaxValue(max: Float) {
maxValue = max
invalidate()
}
fun setStrokeWidthDimension(width: Float) {
strokeWidthDimension = width
invalidate()
}
fun getStrokeWidthDimension(): Float {
return strokeWidthDimension
}
fun setBackgroundWidth(width: Float) {
backgroundWidth = width
invalidate()
}
fun getBackgroundWidth(): Float {
return backgroundWidth
}
fun setText(progressText: String?) {
text = progressText
invalidate()
}
fun getText(): String? {
return text
}
fun setTextColor(color: String?) {
textPaint.color = Color.parseColor(color)
invalidate()
}
var textColor: Int
get() = progressTextColor
set(color) {
progressTextColor = color
textPaint.color = color
invalidate()
}
fun setSuffix(suffix: String?) {
this.suffix = suffix
invalidate()
}
fun getSuffix(): String? {
return suffix
}
fun getPrefix(): String? {
return prefix
}
fun setPrefix(prefix: String?) {
this.prefix = prefix
invalidate()
}
fun getClockwise(): Boolean {
return isClockwise
}
fun setClockwise(clockwise: Boolean) {
isClockwise = clockwise
invalidate()
}
fun getStartingAngle(): Int {
return startingAngle
}
/**
* @param startingAngle 270 for Top
* 0 for Right
* 90 for Bottom
* 180 for Left
*/
fun setStartingAngle(startingAngle: Int) {
this.startingAngle = startingAngle
invalidate()
}
/**
* Use this method to initialize Timer, default interval time is 1second, you can use other method to define interval
*
* @param circularTimerListener Pass your listener to listen ticks and provide data and to listen finish call
* @param time time in long, e.g 1,2,3,4 or any long digit
* @param timeFormatEnum Format to define whether the given long time number is milli, second, minute, hour or day
*/
fun setCircularTimerListener(
circularTimerListener: CircularTimerListener,
time: Long,
timeFormatEnum: TimeFormatEnum?
) {
this.circularTimerListener = circularTimerListener
var timeInMillis: Long = 0
val intervalDuration: Long = 1000
when (timeFormatEnum)
{
TimeFormatEnum.MILLIS -> timeInMillis = time
TimeFormatEnum.SECONDS -> timeInMillis = time * 1000
TimeFormatEnum.MINUTES -> timeInMillis = time * 1000 * 60
TimeFormatEnum.HOUR -> timeInMillis = time * 1000 * 60 * 60
TimeFormatEnum.DAY -> timeInMillis = time * 1000 * 60 * 60 * 24
else -> {}
}
if (countDownTimer != null)
{
countDownTimer!!.cancel()
}
val maxTime = timeInMillis
countDownTimer = object : CountDownTimer(maxTime, intervalDuration) {
override fun onTick(l: Long) {
val percentTimeCompleted = (maxTime - l) / maxTime.toDouble()
drawUpto = (maxValue * percentTimeCompleted).toFloat()
text = circularTimerListener.updateDataOnTick(l)
invalidate()
}
override fun onFinish() {
val percentTimeCompleted = 1.0
drawUpto = (maxValue * percentTimeCompleted).toFloat()
circularTimerListener.onTimerFinished()
invalidate()
}
}
}
/**
* Use this method to initialize Timer, default interval time is 1second, you can use other method to define interval
*
* @param circularTimerListener Pass your listener to listen ticks and provide data and to listen finish call
* @param time time in long, e.g 1,2,3,4 or any long digit
* @param timeFormatEnum Format to define whether the given long time number is milli, second, minute, hour or day
*/
fun setCircularTimerListener(
circularTimerListener: CircularTimerListener,
time: Long,
timeFormatEnum: TimeFormatEnum?,
timeinterval: Long
) {
this.circularTimerListener = circularTimerListener
var timeInMillis: Long = 0
when (timeFormatEnum) {
TimeFormatEnum.MILLIS -> timeInMillis = time
TimeFormatEnum.SECONDS -> timeInMillis = time * 1000
TimeFormatEnum.MINUTES -> timeInMillis = time * 1000 * 60
TimeFormatEnum.HOUR -> timeInMillis = time * 1000 * 60 * 60
TimeFormatEnum.DAY -> timeInMillis = time * 1000 * 60 * 60 * 24
else -> {}
}
if (countDownTimer != null) {
countDownTimer!!.cancel()
}
val maxTime = timeInMillis
countDownTimer = object : CountDownTimer(maxTime, timeinterval) {
override fun onTick(l: Long) {
val percentTimeCompleted = (maxTime - l) / maxTime.toDouble()
drawUpto = (maxValue * percentTimeCompleted).toFloat()
text = circularTimerListener.updateDataOnTick(l)
invalidate()
}
override fun onFinish() {
val percentTimeCompleted = 1.0
drawUpto = (maxValue * percentTimeCompleted).toFloat()
text = circularTimerListener.updateDataOnTick(0)
circularTimerListener.onTimerFinished()
invalidate()
}
}
}
fun startTimer(): Boolean {
return if (countDownTimer == null) {
false
} else {
countDownTimer!!.start()
true
}
}
}
private fun setTimer()
{
val dateFormat = SimpleDateFormat("MM/dd/yyyy h:mm:ss a")
val endTime : Date = dateFormat.parse("10/25/2022 10:30:00 AM")
val currentTime = Date()
val diff : Long = endTime.time - currentTime.time
binding.circularTimerView.setCircularTimerListener(object : CircularTimerListener
{
override fun updateDataOnTick(remainingTimeInMs: Long): String
{
val secs = ceil((remainingTimeInMs / 1000f).toDouble()).toInt()
val hours = secs / 3600;
val minutes = (secs % 3600) / 60;
val seconds = secs % 60;
//timeString = String.format("%02d:%02d:%02d", hours, minutes, seconds);
return String.format("%02d:%02d:%02d", hours, minutes, seconds);
}
override fun onTimerFinished()
{
binding.circularTimerView.setPrefix("")
binding.circularTimerView.setSuffix("")
binding.circularTimerView.setText("Time Ended!")
}
}, diff, TimeFormatEnum.MILLIS, 10)
binding.circularTimerView.startTimer()
}
<CircularTimerView
android:id="@+id/circularTimerView"
android:layout_width="300dp"
android:layout_height="300dp"
android:fontFamily="@font/digital"
app:backgroundColor="#FFAB00"
app:backgroundWidth="8"
app:isClockwise="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:maxValue="100"
app:prefix=""
app:progressBackgroundColor="#000000"
app:progressColor="#fff"
app:progressText="Time Remaining..."
app:progressTextColor="#000"
app:startingPoint="top"
app:suffix=""
app:textSize="22sp" />
import java.lang.Enum
import kotlin.String
enum class TimeFormatEnum {
MILLIS, SECONDS, MINUTES, HOUR, DAY;
fun canonicalForm(): String {
return name
}
companion object {
fun fromCanonicalForm(canonical: String): TimeFormatEnum {
return Enum.valueOf(TimeFormatEnum::class.java, canonical)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment