Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43bb282e74 | ||
|
|
d8ead34f0b |
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@drawable/ic_timer"
|
android:icon="@drawable/ic_timer"
|
||||||
@@ -16,5 +16,4 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
|
import android.graphics.RectF
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
|
||||||
@@ -14,12 +15,24 @@ class CountdownCanvasView @JvmOverloads constructor(
|
|||||||
) : View(context, attrs, defStyleAttr) {
|
) : View(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
private var textToDraw: String = ""
|
private var textToDraw: String = ""
|
||||||
|
private var progressFraction: Float = 1.0f
|
||||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
color = Color.parseColor("#6750A4") // Using the same primary purple
|
color = Color.parseColor("#6750A4")
|
||||||
textSize = 180f // Roughly equivalent to 72sp depending on density
|
textSize = 180f
|
||||||
textAlign = Paint.Align.CENTER
|
textAlign = Paint.Align.CENTER
|
||||||
isFakeBoldText = false
|
isFakeBoldText = false
|
||||||
}
|
}
|
||||||
|
private val ringPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = Color.parseColor("#6750A4")
|
||||||
|
style = Paint.Style.STROKE
|
||||||
|
strokeWidth = 16f
|
||||||
|
}
|
||||||
|
private val ringBgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = Color.parseColor("#D0BCFF")
|
||||||
|
style = Paint.Style.STROKE
|
||||||
|
strokeWidth = 16f
|
||||||
|
}
|
||||||
|
private val rectF = RectF()
|
||||||
|
|
||||||
fun setText(text: String) {
|
fun setText(text: String) {
|
||||||
if (textToDraw != text) {
|
if (textToDraw != text) {
|
||||||
@@ -28,15 +41,34 @@ class CountdownCanvasView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setProgress(fraction: Float) {
|
||||||
|
progressFraction = fraction.coerceIn(0f, 1f)
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
override fun onDraw(canvas: Canvas) {
|
||||||
super.onDraw(canvas)
|
super.onDraw(canvas)
|
||||||
|
|
||||||
if (textToDraw.isEmpty()) return
|
if (textToDraw.isEmpty() || rectF.width() <= 0f) return
|
||||||
|
|
||||||
|
val cx = rectF.centerX()
|
||||||
|
val cy = rectF.centerY()
|
||||||
|
|
||||||
|
// Draw full background ring (always visible track)
|
||||||
|
val bgRect = RectF(rectF.left, rectF.top, rectF.right, rectF.bottom)
|
||||||
|
canvas.drawArc(bgRect, -90f, 360f, false, ringBgPaint)
|
||||||
|
// Draw progress arc on top (remaining time, starts at 12 o'clock)
|
||||||
|
canvas.drawArc(rectF, -90f, 360f * progressFraction, false, ringPaint)
|
||||||
|
|
||||||
|
// Draw text
|
||||||
val fontMetrics = paint.fontMetrics
|
val fontMetrics = paint.fontMetrics
|
||||||
val x = width / 2f
|
val textY = (cy - (fontMetrics.descent + fontMetrics.ascent) / 2f)
|
||||||
val y = (height / 2f) - ((fontMetrics.descent + fontMetrics.ascent) / 2f)
|
canvas.drawText(textToDraw, cx, textY, paint)
|
||||||
|
|
||||||
canvas.drawText(textToDraw, x, y, paint)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,28 @@ 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.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.media.ToneGenerator
|
import android.media.ToneGenerator
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.CountDownTimer
|
import android.os.CountDownTimer
|
||||||
|
import android.os.Vibrator
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
|
|
||||||
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 cvCountdown: CountdownCanvasView
|
||||||
private lateinit var btnStop: Button
|
private lateinit var btnStop: Button
|
||||||
private lateinit var btnRestart: Button
|
private lateinit var btnRestart: Button
|
||||||
private lateinit var btnDismiss: Button
|
private lateinit var btnDismiss: Button
|
||||||
private var timer: CountDownTimer? = null
|
private var timer: CountDownTimer? = null
|
||||||
private var toneGenerator: ToneGenerator? = null
|
private var toneGenerator: ToneGenerator? = null
|
||||||
|
private var vibrator: Vibrator? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -38,6 +45,7 @@ class CountdownDialog(private val context: Context, private val durationMillis:
|
|||||||
btnRestart = findViewById(R.id.btnRestart)
|
btnRestart = findViewById(R.id.btnRestart)
|
||||||
btnDismiss = findViewById(R.id.btnDismiss)
|
btnDismiss = findViewById(R.id.btnDismiss)
|
||||||
toneGenerator = ToneGenerator(AudioManager.STREAM_ALARM, 100)
|
toneGenerator = ToneGenerator(AudioManager.STREAM_ALARM, 100)
|
||||||
|
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||||
|
|
||||||
btnStop.setOnClickListener { cancelTimer() }
|
btnStop.setOnClickListener { cancelTimer() }
|
||||||
btnRestart.setOnClickListener { restartTimer() }
|
btnRestart.setOnClickListener { restartTimer() }
|
||||||
@@ -47,22 +55,34 @@ class CountdownDialog(private val context: Context, private val durationMillis:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun startTimer() {
|
private fun startTimer() {
|
||||||
|
cvCountdown.setProgress(1f)
|
||||||
timer = object : CountDownTimer(durationMillis, 1000) {
|
timer = object : CountDownTimer(durationMillis, 1000) {
|
||||||
override fun onTick(millisUntilFinished: Long) {
|
override fun onTick(millisUntilFinished: Long) {
|
||||||
val secondsLeft = (millisUntilFinished / 1000).toInt() + 1
|
val progress = millisUntilFinished.toFloat() / durationMillis
|
||||||
if (secondsLeft < 10) {
|
cvCountdown.setProgress(progress)
|
||||||
cvCountdown.setText(secondsLeft.toString())
|
|
||||||
} else {
|
if (millisUntilFinished >= durationMillis) {
|
||||||
cvCountdown.setText(context.getString(R.string.countdown_text))
|
cvCountdown.setText(context.getString(R.string.countdown_text))
|
||||||
|
} else {
|
||||||
|
val secondsLeft = (millisUntilFinished / 1000).toInt() + 1
|
||||||
|
cvCountdown.setText(secondsLeft.toString())
|
||||||
}
|
}
|
||||||
if (secondsLeft in 1..3) {
|
|
||||||
|
if (millisUntilFinished <= 3000 && millisUntilFinished > 1000) {
|
||||||
playTone(ToneGenerator.TONE_PROP_BEEP)
|
playTone(ToneGenerator.TONE_PROP_BEEP)
|
||||||
|
if (prefs.getBoolean("vibration", true)) {
|
||||||
|
vibrator?.vibrate(50)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFinish() {
|
override fun onFinish() {
|
||||||
cvCountdown.setText(context.getString(R.string.timer_zero))
|
cvCountdown.setText(context.getString(R.string.timer_zero))
|
||||||
|
cvCountdown.setProgress(0f)
|
||||||
playTone(ToneGenerator.TONE_SUP_CONFIRM, 500)
|
playTone(ToneGenerator.TONE_SUP_CONFIRM, 500)
|
||||||
|
if (prefs.getBoolean("vibration", true)) {
|
||||||
|
vibrator?.vibrate(200)
|
||||||
|
}
|
||||||
btnStop.visibility = View.GONE
|
btnStop.visibility = View.GONE
|
||||||
btnRestart.visibility = View.GONE
|
btnRestart.visibility = View.GONE
|
||||||
btnDismiss.visibility = View.VISIBLE
|
btnDismiss.visibility = View.VISIBLE
|
||||||
|
|||||||
@@ -1,18 +1,88 @@
|
|||||||
package com.example.countdowntimer
|
package com.example.countdowntimer
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
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
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
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
|
||||||
|
|
||||||
|
private fun durationToChipId(duration: Long): Int = when (duration) {
|
||||||
|
5000L -> R.id.chip5s
|
||||||
|
15000L -> R.id.chip15s
|
||||||
|
30000L -> R.id.chip30s
|
||||||
|
else -> R.id.chip10s
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun chipIdToDuration(chipId: Int): Long = when (chipId) {
|
||||||
|
R.id.chip5s -> 5000L
|
||||||
|
R.id.chip10s -> 10000L
|
||||||
|
R.id.chip15s -> 15000L
|
||||||
|
R.id.chip30s -> 30000L
|
||||||
|
else -> DEFAULT_DURATION
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateDurationLabel() {
|
||||||
|
btnDurationLabel.text = "${selectedDuration / 1000}s"
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
updateDurationLabel()
|
||||||
|
|
||||||
|
// Restore selected chip
|
||||||
|
val chipId = durationToChipId(selectedDuration)
|
||||||
|
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 = chipIdToDuration(checkedId)
|
||||||
|
prefs.edit().putLong(KEY_SELECTED_DURATION, selectedDuration).apply()
|
||||||
|
updateDurationLabel()
|
||||||
|
hsvDurationOptions.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
findViewById<Button>(R.id.btnStart).setOnClickListener {
|
findViewById<Button>(R.id.btnStart).setOnClickListener {
|
||||||
CountdownDialog(this, TIMER_DURATION_MS).show()
|
CountdownDialog(this, selectedDuration, prefs).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#6750A4"
|
||||||
|
android:pathData="M7.41,8.59L12,13.17l4.59,-4.58L18,10l-6,6 -6,-6z"/>
|
||||||
|
</vector>
|
||||||
@@ -110,12 +110,67 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnDurationLabel"
|
||||||
|
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="32dp"
|
||||||
|
android:text="@string/start_timer"
|
||||||
|
android:drawableEnd="@drawable/ic_keyboard_arrow_down"
|
||||||
|
app:cornerRadius="20dp" />
|
||||||
|
|
||||||
|
<HorizontalScrollView
|
||||||
|
android:id="@+id/hsvDurationOptions"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:scrollbars="none"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<com.google.android.material.chip.ChipGroup
|
||||||
|
android:id="@+id/chipGroup"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:singleSelection="true">
|
||||||
|
|
||||||
|
<com.google.android.material.chip.Chip
|
||||||
|
android:id="@+id/chip5s"
|
||||||
|
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="5s" />
|
||||||
|
|
||||||
|
<com.google.android.material.chip.Chip
|
||||||
|
android:id="@+id/chip10s"
|
||||||
|
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checked="true"
|
||||||
|
android:text="10s" />
|
||||||
|
|
||||||
|
<com.google.android.material.chip.Chip
|
||||||
|
android:id="@+id/chip15s"
|
||||||
|
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="15s" />
|
||||||
|
|
||||||
|
<com.google.android.material.chip.Chip
|
||||||
|
android:id="@+id/chip30s"
|
||||||
|
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="30s" />
|
||||||
|
</com.google.android.material.chip.ChipGroup>
|
||||||
|
</HorizontalScrollView>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnStart"
|
android:id="@+id/btnStart"
|
||||||
style="@style/Widget.Material3.Button"
|
style="@style/Widget.Material3.Button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="32dp"
|
android:layout_marginTop="16dp"
|
||||||
android:text="@string/start_timer"
|
android:text="@string/start_timer"
|
||||||
app:cornerRadius="20dp"
|
app:cornerRadius="20dp"
|
||||||
app:icon="@drawable/ic_play_arrow" />
|
app:icon="@drawable/ic_play_arrow" />
|
||||||
|
|||||||
@@ -18,16 +18,14 @@
|
|||||||
|
|
||||||
<com.example.countdowntimer.CountdownCanvasView
|
<com.example.countdowntimer.CountdownCanvasView
|
||||||
android:id="@+id/cvCountdown"
|
android:id="@+id/cvCountdown"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintHeight_default="wrap"
|
|
||||||
app:layout_constrainedHeight="true"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/btnStop"
|
app:layout_constraintBottom_toTopOf="@id/btnStop"
|
||||||
|
app:layout_constraintDimensionRatio="1:1"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
android:minWidth="120dp"
|
android:layout_margin="16dp" />
|
||||||
android:minHeight="120dp" />
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/btnStop"
|
android:id="@+id/btnStop"
|
||||||
|
|||||||
@@ -7,4 +7,8 @@
|
|||||||
<string name="instruction_volume">Ensure your bong is loaded</string>
|
<string name="instruction_volume">Ensure your bong is loaded</string>
|
||||||
<string name="instruction_position">Find a comfortable position</string>
|
<string name="instruction_position">Find a comfortable position</string>
|
||||||
<string name="instruction_distractions">Light your bowl and take your hit before you press the Start Timer button. HOLD YOUR HIT IN!</string>
|
<string name="instruction_distractions">Light your bowl and take your hit before you press the Start Timer button. HOLD YOUR HIT IN!</string>
|
||||||
|
<string name="pick_duration">Pick Duration</string>
|
||||||
|
<string name="settings">Settings</string>
|
||||||
|
<string name="vibration">Vibration</string>
|
||||||
|
<string name="sounds">Sounds</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user