Created
January 8, 2024 17:10
-
-
Save ElianFabian/d1ba89f4b440e64272d3771f9dbf47f1 to your computer and use it in GitHub Desktop.
An implementation of an easy-to-use precise timer.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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