diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4cdc27a..ad5a4e7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ - + - diff --git a/app/src/main/java/com/example/countdowntimer/CountdownCanvasView.kt b/app/src/main/java/com/example/countdowntimer/CountdownCanvasView.kt index 0e95b26..a5494c2 100644 --- a/app/src/main/java/com/example/countdowntimer/CountdownCanvasView.kt +++ b/app/src/main/java/com/example/countdowntimer/CountdownCanvasView.kt @@ -1,11 +1,14 @@ package com.example.countdowntimer +import android.animation.ValueAnimator import android.content.Context import androidx.core.content.ContextCompat import android.graphics.Canvas import android.graphics.Paint +import android.graphics.RectF import android.util.AttributeSet import android.view.View +import android.view.animation.AccelerateDecelerateInterpolator class CountdownCanvasView @JvmOverloads constructor( context: Context, @@ -14,12 +17,32 @@ class CountdownCanvasView @JvmOverloads constructor( ) : View(context, attrs, defStyleAttr) { private var textToDraw: String = "" + private var progressFraction: Float = 1.0f private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = ContextCompat.getColor(context, R.color.md_theme_light_primary) textSize = 180f // Roughly equivalent to 72sp depending on density textAlign = Paint.Align.CENTER isFakeBoldText = false } + private val ringPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.parseColor("#D0BCFF") + style = Paint.Style.STROKE + strokeWidth = 16f + } + private val ringBgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.parseColor("#E8DEF8") + style = Paint.Style.STROKE + strokeWidth = 16f + } + private val rectF = RectF() + private val progressAnimator = ValueAnimator.ofFloat(1.0f, 0.0f).apply { + duration = 10000 + interpolator = AccelerateDecelerateInterpolator() + addUpdateListener { animation -> + progressFraction = animation.animatedValue as Float + invalidate() + } + } fun setText(text: String) { if (textToDraw != text) { @@ -28,15 +51,42 @@ class CountdownCanvasView @JvmOverloads constructor( } } + fun setProgress(fraction: Float) { + progressFraction = fraction + invalidate() + } + + fun startProgressAnimation(duration: Long) { + progressAnimator.duration = duration + progressAnimator.start() + } + + fun cancelProgressAnimation() { + progressAnimator.cancel() + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + val inset = ringPaint.strokeWidth / 2f + 12f + rectF.set(inset, inset, w - inset, h - inset) + } + override fun onDraw(canvas: Canvas) { super.onDraw(canvas) - if (textToDraw.isEmpty()) return + if (textToDraw.isEmpty() || rectF.width() <= 0f) return + val cx = rectF.centerX() + val cy = rectF.centerY() + + // Draw background ring + canvas.drawArc(rectF, -90f, 360f, false, ringBgPaint) + // Draw progress ring (remaining time, starts at 12 o'clock) + canvas.drawArc(rectF, -90f, 360f * progressFraction, false, ringPaint) + + // Draw text val fontMetrics = paint.fontMetrics - val x = width / 2f - val y = (height / 2f) - ((fontMetrics.descent + fontMetrics.ascent) / 2f) - - canvas.drawText(textToDraw, x, y, paint) + val textY = (cy - (fontMetrics.descent + fontMetrics.ascent) / 2f) + canvas.drawText(textToDraw, cx, textY, paint) } } diff --git a/app/src/main/java/com/example/countdowntimer/CountdownDialog.kt b/app/src/main/java/com/example/countdowntimer/CountdownDialog.kt index cc85e77..47bc7ef 100644 --- a/app/src/main/java/com/example/countdowntimer/CountdownDialog.kt +++ b/app/src/main/java/com/example/countdowntimer/CountdownDialog.kt @@ -2,21 +2,29 @@ package com.example.countdowntimer import android.app.Dialog import android.content.Context +import android.content.SharedPreferences import android.media.AudioManager import android.media.ToneGenerator import android.os.Bundle import android.os.CountDownTimer +import android.os.Vibrator import android.view.View import android.view.WindowManager import android.widget.Button +import com.google.android.material.chip.ChipGroup -class CountdownDialog(private val context: Context, private val durationMillis: Long) : Dialog(context) { +class CountdownDialog( + private val context: Context, + private val durationMillis: Long, + private val prefs: SharedPreferences +) : Dialog(context) { private lateinit var cvCountdown: CountdownCanvasView private lateinit var btnStop: Button private lateinit var btnRestart: Button private lateinit var btnDismiss: Button private var timer: CountDownTimer? = null private var toneGenerator: ToneGenerator? = null + private var vibrator: Vibrator? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -38,6 +46,7 @@ class CountdownDialog(private val context: Context, private val durationMillis: btnRestart = findViewById(R.id.btnRestart) btnDismiss = findViewById(R.id.btnDismiss) toneGenerator = ToneGenerator(AudioManager.STREAM_ALARM, 100) + vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator btnStop.setOnClickListener { cancelTimer() } btnRestart.setOnClickListener { restartTimer() } @@ -47,22 +56,32 @@ class CountdownDialog(private val context: Context, private val durationMillis: } private fun startTimer() { + cvCountdown.setProgress(1f) timer = object : CountDownTimer(durationMillis, 1000) { override fun onTick(millisUntilFinished: Long) { val secondsLeft = (millisUntilFinished / 1000).toInt() + 1 - if (secondsLeft < 10) { + val progress = millisUntilFinished.toFloat() / durationMillis + if (secondsLeft < 10) { cvCountdown.setText(secondsLeft.toString()) } else { cvCountdown.setText(context.getString(R.string.countdown_text)) } + cvCountdown.setProgress(progress) if (secondsLeft in 1..3) { playTone(ToneGenerator.TONE_PROP_BEEP) + if (prefs.getBoolean("vibration", true)) { + vibrator?.vibrate(50) + } } } override fun onFinish() { cvCountdown.setText(context.getString(R.string.timer_zero)) + cvCountdown.setProgress(0f) playTone(ToneGenerator.TONE_SUP_CONFIRM, 500) + if (prefs.getBoolean("vibration", true)) { + vibrator?.vibrate(200) + } btnStop.visibility = View.GONE btnRestart.visibility = View.GONE btnDismiss.visibility = View.VISIBLE diff --git a/app/src/main/java/com/example/countdowntimer/MainActivity.kt b/app/src/main/java/com/example/countdowntimer/MainActivity.kt index f007bfd..fed1022 100644 --- a/app/src/main/java/com/example/countdowntimer/MainActivity.kt +++ b/app/src/main/java/com/example/countdowntimer/MainActivity.kt @@ -1,18 +1,78 @@ package com.example.countdowntimer +import android.content.Context +import android.content.SharedPreferences import android.os.Bundle +import android.view.View +import android.view.inputmethod.InputMethodManager import android.widget.Button import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.chip.ChipGroup class MainActivity : AppCompatActivity() { - private val TIMER_DURATION_MS = 10000L + + companion object { + private const val PREFS_NAME = "timer_prefs" + private const val KEY_SELECTED_DURATION = "selected_duration" + private const val DEFAULT_DURATION = 10000L + } + + private lateinit var prefs: SharedPreferences + private lateinit var btnDurationLabel: Button + private lateinit var hsvDurationOptions: View + private lateinit var chipGroup: ChipGroup + private var selectedDuration: Long = DEFAULT_DURATION override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + selectedDuration = prefs.getLong(KEY_SELECTED_DURATION, DEFAULT_DURATION) + + btnDurationLabel = findViewById(R.id.btnDurationLabel) + hsvDurationOptions = findViewById(R.id.hsvDurationOptions) + chipGroup = findViewById(R.id.chipGroup) + + // Restore selected chip + val chipId = when (selectedDuration) { + 5000L -> R.id.chip5s + 15000L -> R.id.chip15s + 30000L -> R.id.chip30s + else -> R.id.chip10s + } + chipGroup.check(chipId) + + btnDurationLabel.setOnClickListener { + if (hsvDurationOptions.visibility == View.GONE) { + hsvDurationOptions.visibility = View.VISIBLE + } else { + hsvDurationOptions.visibility = View.GONE + (currentFocus ?: btnDurationLabel).let { focused -> + focused.clearFocus() + val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(hsvDurationOptions.windowToken, 0) + } + } + } + + chipGroup.setOnCheckedChangeListener { _, checkedId -> + if (checkedId != -1) { + selectedDuration = when (checkedId) { + R.id.chip5s -> 5000L + R.id.chip10s -> 10000L + R.id.chip15s -> 15000L + R.id.chip30s -> 30000L + else -> DEFAULT_DURATION + } + prefs.edit().putLong(KEY_SELECTED_DURATION, selectedDuration).apply() + hsvDurationOptions.visibility = View.GONE + chipGroup.clearCheck() + } + } + findViewById