visual polish: add pulsing start button and color-shifting progress ring

- Countdown ring interpolates green → yellow → red as timer progresses
- Start button pulses continuously to draw attention
- Pulse stops while dialog is shown and restarts on resume
This commit is contained in:
2026-05-30 10:51:36 -07:00
parent 97b01437b2
commit 7a91ecba18
3 changed files with 70 additions and 0 deletions
@@ -47,6 +47,13 @@ class CountdownCanvasView @JvmOverloads constructor(
invalidate() invalidate()
} }
fun setColor(color: Int) {
if (ringPaint.color != color) {
ringPaint.color = color
invalidate()
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh) super.onSizeChanged(w, h, oldw, oldh)
val inset = ringPaint.strokeWidth / 2f + 12f val inset = ringPaint.strokeWidth / 2f + 12f
@@ -3,6 +3,8 @@ package com.example.countdowntimer
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import android.media.AudioManager import android.media.AudioManager
import android.media.ToneGenerator import android.media.ToneGenerator
import android.os.Bundle import android.os.Bundle
@@ -56,11 +58,28 @@ class CountdownDialog(
private fun startTimer() { private fun startTimer() {
cvCountdown.setProgress(1f) cvCountdown.setProgress(1f)
cvCountdown.setColor(ContextCompat.getColor(context, R.color.md_color_ring_progress))
timer = object : CountDownTimer(durationMillis, 1000) { timer = object : CountDownTimer(durationMillis, 1000) {
override fun onTick(millisUntilFinished: Long) { override fun onTick(millisUntilFinished: Long) {
val progress = millisUntilFinished.toFloat() / durationMillis val progress = millisUntilFinished.toFloat() / durationMillis
cvCountdown.setProgress(progress) cvCountdown.setProgress(progress)
// Interpolate ring color: green → yellow at 50% → red at end
val green = android.graphics.Color.parseColor("#2E7D32")
val yellow = android.graphics.Color.parseColor("#FFC107")
val red = android.graphics.Color.parseColor("#F44336")
val ringColor = when {
progress >= 0.5f -> {
val t = (progress - 0.5f) * 2f
ColorUtils.blendARGB(green, yellow, t)
}
else -> {
val t = progress * 2f
ColorUtils.blendARGB(yellow, red, t)
}
}
cvCountdown.setColor(ringColor)
val secondsLeft = (millisUntilFinished / 1000).toInt() + 1 val secondsLeft = (millisUntilFinished / 1000).toInt() + 1
cvCountdown.setText(secondsLeft.toString()) cvCountdown.setText(secondsLeft.toString())
@@ -11,6 +11,8 @@ import android.view.inputmethod.InputMethodManager
import android.widget.Button import android.widget.Button
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import android.animation.ValueAnimator
import android.view.animation.AnticipateOvershootInterpolator
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@@ -24,9 +26,11 @@ class MainActivity : AppCompatActivity() {
private lateinit var btnDurationLabel: Button private lateinit var btnDurationLabel: Button
private lateinit var hsvDurationOptions: View private lateinit var hsvDurationOptions: View
private lateinit var chipGroup: ChipGroup private lateinit var chipGroup: ChipGroup
private lateinit var btnStart: View
private lateinit var vibrator: Vibrator private lateinit var vibrator: Vibrator
private var selectedDuration: Long = DEFAULT_DURATION private var selectedDuration: Long = DEFAULT_DURATION
private var lastSelectedChipId: Int = -1 private var lastSelectedChipId: Int = -1
private var pulseAnimator: ValueAnimator? = null
private fun durationToChipId(duration: Long): Int = when (duration) { private fun durationToChipId(duration: Long): Int = when (duration) {
5000L -> R.id.chip5s 5000L -> R.id.chip5s
@@ -57,6 +61,30 @@ class MainActivity : AppCompatActivity() {
} }
} }
private fun startPulseAnimation() {
pulseAnimator = ValueAnimator.ofFloat(1f, 0.78f, 1f).apply {
duration = 1500
repeatMode = ValueAnimator.REVERSE
repeatCount = ValueAnimator.INFINITE
interpolator = AnticipateOvershootInterpolator(1f)
addUpdateListener { animation ->
val fraction = animation.animatedValue as Float
btnStart.scaleX = fraction
btnStart.scaleY = fraction
btnStart.alpha = fraction
}
}
pulseAnimator?.start()
}
private fun stopPulseAnimation() {
pulseAnimator?.cancel()
pulseAnimator = null
btnStart.scaleX = 1f
btnStart.scaleY = 1f
btnStart.alpha = 1f
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
@@ -67,6 +95,7 @@ class MainActivity : AppCompatActivity() {
btnDurationLabel = findViewById(R.id.btnDurationLabel) btnDurationLabel = findViewById(R.id.btnDurationLabel)
hsvDurationOptions = findViewById(R.id.hsvDurationOptions) hsvDurationOptions = findViewById(R.id.hsvDurationOptions)
chipGroup = findViewById(R.id.chipGroup) chipGroup = findViewById(R.id.chipGroup)
btnStart = findViewById(R.id.btnStart)
val vibratorManager = getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager val vibratorManager = getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
vibrator = vibratorManager.defaultVibrator vibrator = vibratorManager.defaultVibrator
@@ -111,7 +140,22 @@ class MainActivity : AppCompatActivity() {
findViewById<View>(R.id.chip30s).setOnClickListener { onChipClicked(R.id.chip30s) } findViewById<View>(R.id.chip30s).setOnClickListener { onChipClicked(R.id.chip30s) }
findViewById<View>(R.id.btnStart).setOnClickListener { findViewById<View>(R.id.btnStart).setOnClickListener {
stopPulseAnimation()
CountdownDialog(this, selectedDuration, prefs).show() CountdownDialog(this, selectedDuration, prefs).show()
} }
startPulseAnimation()
}
override fun onResume() {
super.onResume()
if (!isFinishing && !isDestroyed) {
startPulseAnimation()
}
}
override fun onPause() {
super.onPause()
stopPulseAnimation()
} }
} }