Skip to content

Instantly share code, notes, and snippets.

@ElianFabian
Created January 8, 2024 17:10
Show Gist options
  • Save ElianFabian/d1ba89f4b440e64272d3771f9dbf47f1 to your computer and use it in GitHub Desktop.
Save ElianFabian/d1ba89f4b440e64272d3771f9dbf47f1 to your computer and use it in GitHub Desktop.
An implementation of an easy-to-use precise timer.
// Based on: https://stackoverflow.com/questions/23323823/android-countdowntimer-tick-is-not-accurate
abstract class SimplePreciseTimer @JvmOverloads constructor(
periodInMillis: Long = 1,
) : java.util.Timer(true) {
private var task = newTask()
private var hasStarted = false
private var isRunning = false
var periodInMillis = periodInMillis
set(period) {
require(periodInMillis > 0) {
"intervalInMillis $periodInMillis must be greater than 0."
}
field = period
if (isRunning) {
task.cancel()
task = newTask()
scheduleAtFixedRate(task, period, period)
}
}
abstract fun onTick()
fun play() {
if (hasStarted) {
task = newTask()
}
hasStarted = true
isRunning = true
scheduleAtFixedRate(task, periodInMillis, periodInMillis)
}
fun pause() {
task.cancel()
isRunning = false
}
fun dispose() {
isRunning = false
cancel()
purge()
}
private fun newTask() = object : TimerTask() {
override fun run() = onTick()
}
}
import kotlinx.coroutines.flow.StateFlow
interface Timer {
fun play()
fun pause()
fun restart()
val state: StateFlow<State>
var initialMillis: Long
var periodInMillis: Long
var minMillis: Long
var maxMillis: Long
var isIncreasing: Boolean
data class State(
val hasStarted: Boolean = false,
val isRunning: Boolean = false,
val initialMillis: Long,
val millis: Long,
val minMillis: Long,
val maxMillis: Long,
val isIncreasing: Boolean,
)
}
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import java.util.TimerTask
class TimerImpl(
intervalInMillis: Long,
override var initialMillis: Long = 0L,
isIncreasing: Boolean = true,
minMillis: Long = 0L,
maxMillis: Long = Long.MAX_VALUE,
) : Timer {
private val timerState = MutableStateFlow(
Timer.State(
initialMillis = initialMillis,
millis = initialMillis,
minMillis = minMillis,
maxMillis = maxMillis,
isIncreasing = isIncreasing,
)
)
private val preciseTimer = object : SimplePreciseTimer(intervalInMillis) {
override fun onTick() {
timerState.update { state ->
val positiveOrNegativeInterval = when {
state.isIncreasing -> +this.periodInMillis
else -> -this.periodInMillis
}
val newMillis = state.millis + positiveOrNegativeInterval
val areMillisOutOfRange = newMillis < state.minMillis || state.maxMillis < newMillis
if (areMillisOutOfRange) {
pause()
return
}
state.copy(millis = state.millis + positiveOrNegativeInterval)
}
}
}
override val state = timerState.asStateFlow()
override var periodInMillis: Long = 1L
set(period) {
preciseTimer.periodInMillis = period
field = period
}
override var minMillis = minMillis
set(min) {
val state = timerState.value
if (state.millis > min && state.isRunning) {
preciseTimer.play()
}
timerState.update {
it.copy(minMillis = min)
}
}
override var maxMillis = maxMillis
set(maxMillis) {
val state = timerState.value
if (maxMillis < state.maxMillis && state.isRunning) {
preciseTimer.play()
}
timerState.update {
it.copy(maxMillis = maxMillis)
}
}
override var isIncreasing
get() = timerState.value.isIncreasing
set(increasing) {
val millis = timerState.value.millis
val isRunning = state.value.isRunning
val hasStarted = state.value.hasStarted
if (hasStarted
&& isRunning
&& (increasing && millis <= minMillis || !increasing && maxMillis <= millis)
) {
preciseTimer.play()
}
timerState.update {
it.copy(isIncreasing = increasing)
}
}
override fun play() {
if (!timerState.value.hasStarted) {
timerState.update {
it.copy(hasStarted = true)
}
}
preciseTimer.play()
timerState.update {
it.copy(isRunning = true)
}
}
override fun pause() {
preciseTimer.pause()
timerState.update {
it.copy(isRunning = false)
}
}
override fun restart() {
preciseTimer.pause()
timerState.update {
it.copy(
hasStarted = false,
isRunning = false,
millis = it.initialMillis,
)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment