extract stopwatch logic to separated file

This commit is contained in:
Mysochenko Yuriy 2022-05-01 14:02:02 +03:00
parent b41253f1b1
commit 20a59bfd1e
4 changed files with 176 additions and 142 deletions

View File

@ -4,45 +4,25 @@ import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.graphics.Matrix import android.graphics.Matrix
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.SystemClock
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.simplemobiletools.clock.R import com.simplemobiletools.clock.R
import com.simplemobiletools.clock.activities.SimpleActivity import com.simplemobiletools.clock.activities.SimpleActivity
import com.simplemobiletools.clock.adapters.StopwatchAdapter import com.simplemobiletools.clock.adapters.StopwatchAdapter
import com.simplemobiletools.clock.extensions.config
import com.simplemobiletools.clock.extensions.formatStopwatchTime import com.simplemobiletools.clock.extensions.formatStopwatchTime
import com.simplemobiletools.clock.helpers.SORT_BY_LAP import com.simplemobiletools.clock.helpers.SORT_BY_LAP
import com.simplemobiletools.clock.helpers.SORT_BY_LAP_TIME import com.simplemobiletools.clock.helpers.SORT_BY_LAP_TIME
import com.simplemobiletools.clock.helpers.SORT_BY_TOTAL_TIME import com.simplemobiletools.clock.helpers.SORT_BY_TOTAL_TIME
import com.simplemobiletools.clock.helpers.Stopwatch
import com.simplemobiletools.clock.models.Lap import com.simplemobiletools.clock.models.Lap
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.SORT_DESCENDING import com.simplemobiletools.commons.helpers.SORT_DESCENDING
import kotlinx.android.synthetic.main.fragment_stopwatch.view.* import kotlinx.android.synthetic.main.fragment_stopwatch.view.*
class StopwatchFragment : Fragment() { class StopwatchFragment : Fragment() {
private val UPDATE_INTERVAL = 10L
private val WAS_RUNNING = "was_running"
private val TOTAL_TICKS = "total_ticks"
private val CURRENT_TICKS = "current_ticks"
private val LAP_TICKS = "lap_ticks"
private val CURRENT_LAP = "current_lap"
private val LAPS = "laps"
private val SORTING = "sorting"
private val updateHandler = Handler()
private var uptimeAtStart = 0L
private var totalTicks = 0
private var currentTicks = 0 // ticks that reset at pause
private var lapTicks = 0
private var currentLap = 1
private var isRunning = false
private var sorting = SORT_BY_LAP or SORT_DESCENDING
private var laps = ArrayList<Lap>()
private var storedTextColor = 0 private var storedTextColor = 0
@ -51,6 +31,8 @@ class StopwatchFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
storeStateVariables() storeStateVariables()
val sorting = requireContext().config.stopwatchLapsSort
Lap.sorting = sorting
view = (inflater.inflate(R.layout.fragment_stopwatch, container, false) as ViewGroup).apply { view = (inflater.inflate(R.layout.fragment_stopwatch, container, false) as ViewGroup).apply {
stopwatch_time.setOnClickListener { stopwatch_time.setOnClickListener {
togglePlayPause() togglePlayPause()
@ -78,20 +60,7 @@ class StopwatchFragment : Fragment() {
stopwatch_lap.setOnClickListener { stopwatch_lap.setOnClickListener {
stopwatch_sorting_indicators_holder.beVisible() stopwatch_sorting_indicators_holder.beVisible()
if (laps.isEmpty()) { Stopwatch.lap()
val lap = Lap(currentLap++, lapTicks * UPDATE_INTERVAL, totalTicks * UPDATE_INTERVAL)
laps.add(0, lap)
lapTicks = 0
} else {
laps.first().apply {
lapTime = lapTicks * UPDATE_INTERVAL
totalTime = totalTicks * UPDATE_INTERVAL
}
}
val lap = Lap(currentLap++, lapTicks * UPDATE_INTERVAL, totalTicks * UPDATE_INTERVAL)
laps.add(0, lap)
lapTicks = 0
updateLaps() updateLaps()
} }
@ -100,11 +69,10 @@ class StopwatchFragment : Fragment() {
changeSorting(it) changeSorting(it)
} }
} }
Lap.sorting = sorting
stopwatch_list.adapter = stopwatchAdapter stopwatch_list.adapter = stopwatchAdapter
} }
updateSortingIndicators() updateSortingIndicators(sorting)
return view return view
} }
@ -116,64 +84,25 @@ class StopwatchFragment : Fragment() {
if (storedTextColor != configTextColor) { if (storedTextColor != configTextColor) {
stopwatchAdapter.updateTextColor(configTextColor) stopwatchAdapter.updateTextColor(configTextColor)
} }
Stopwatch.addUpdateListener(updateListener)
updateLaps()
view.stopwatch_sorting_indicators_holder.beVisibleIf(Stopwatch.laps.isNotEmpty())
if (Stopwatch.laps.isNotEmpty()) {
updateSorting(Lap.sorting)
}
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
storeStateVariables() storeStateVariables()
} Stopwatch.removeUpdateListener(updateListener)
override fun onDestroy() {
super.onDestroy()
if (isRunning && activity?.isChangingConfigurations == false) {
context?.toast(R.string.stopwatch_stopped)
}
isRunning = false
updateHandler.removeCallbacks(updateRunnable)
} }
private fun storeStateVariables() { private fun storeStateVariables() {
storedTextColor = requireContext().getProperTextColor() storedTextColor = requireContext().getProperTextColor()
} }
override fun onSaveInstanceState(outState: Bundle) {
outState.apply {
putBoolean(WAS_RUNNING, isRunning)
putInt(TOTAL_TICKS, totalTicks)
putInt(CURRENT_TICKS, currentTicks)
putInt(LAP_TICKS, lapTicks)
putInt(CURRENT_LAP, currentLap)
putInt(SORTING, sorting)
putString(LAPS, Gson().toJson(laps))
super.onSaveInstanceState(this)
}
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
savedInstanceState?.apply {
isRunning = getBoolean(WAS_RUNNING, false)
totalTicks = getInt(TOTAL_TICKS, 0)
currentTicks = getInt(CURRENT_TICKS, 0)
lapTicks = getInt(LAP_TICKS, 0)
currentLap = getInt(CURRENT_LAP, 0)
sorting = getInt(SORTING, SORT_BY_LAP or SORT_DESCENDING)
val lapsToken = object : TypeToken<List<Lap>>() {}.type
laps = Gson().fromJson(getString(LAPS), lapsToken)
if (laps.isNotEmpty()) {
view.stopwatch_sorting_indicators_holder.beVisibleIf(laps.isNotEmpty())
updateSorting()
}
if (isRunning) {
uptimeAtStart = SystemClock.uptimeMillis() - currentTicks * UPDATE_INTERVAL
updateStopwatchState(false)
}
}
}
private fun setupViews() { private fun setupViews() {
val properPrimaryColor = requireContext().getProperPrimaryColor() val properPrimaryColor = requireContext().getProperPrimaryColor()
view.apply { view.apply {
@ -181,60 +110,30 @@ class StopwatchFragment : Fragment() {
stopwatch_play_pause.background = resources.getColoredDrawableWithColor(R.drawable.circle_background_filled, properPrimaryColor) stopwatch_play_pause.background = resources.getColoredDrawableWithColor(R.drawable.circle_background_filled, properPrimaryColor)
stopwatch_reset.applyColorFilter(requireContext().getProperTextColor()) stopwatch_reset.applyColorFilter(requireContext().getProperTextColor())
} }
updateIcons()
updateDisplayedText()
} }
private fun updateIcons() { private fun updateIcons(state: Stopwatch.State) {
val drawableId = if (isRunning) R.drawable.ic_pause_vector else R.drawable.ic_play_vector val drawableId = if (state == Stopwatch.State.RUNNING) R.drawable.ic_pause_vector else R.drawable.ic_play_vector
val iconColor = if (requireContext().getProperPrimaryColor() == Color.WHITE) Color.BLACK else Color.WHITE val iconColor = if (requireContext().getProperPrimaryColor() == Color.WHITE) Color.BLACK else Color.WHITE
view.stopwatch_play_pause.setImageDrawable(resources.getColoredDrawableWithColor(drawableId, iconColor)) view.stopwatch_play_pause.setImageDrawable(resources.getColoredDrawableWithColor(drawableId, iconColor))
} }
private fun togglePlayPause() { private fun togglePlayPause() {
isRunning = !isRunning Stopwatch.toggle(true)
updateStopwatchState(true)
} }
private fun updateStopwatchState(setUptimeAtStart: Boolean) {
updateIcons()
view.stopwatch_lap.beVisibleIf(isRunning)
if (isRunning) { private fun updateDisplayedText(totalTime: Long, lapTime: Long, useLongerMSFormat: Boolean) {
updateHandler.post(updateRunnable) view.stopwatch_time.text = totalTime.formatStopwatchTime(useLongerMSFormat)
view.stopwatch_reset.beVisible() if (Stopwatch.laps.isNotEmpty() && lapTime != -1L) {
if (setUptimeAtStart) { stopwatchAdapter.updateLastField(lapTime, totalTime)
uptimeAtStart = SystemClock.uptimeMillis()
}
} else {
val prevSessionsMS = (totalTicks - currentTicks) * UPDATE_INTERVAL
val totalDuration = SystemClock.uptimeMillis() - uptimeAtStart + prevSessionsMS
updateHandler.removeCallbacksAndMessages(null)
view.stopwatch_time.text = totalDuration.formatStopwatchTime(true)
currentTicks = 0
totalTicks--
}
}
private fun updateDisplayedText() {
view.stopwatch_time.text = (totalTicks * UPDATE_INTERVAL).formatStopwatchTime(false)
if (currentLap > 1) {
stopwatchAdapter.updateLastField(lapTicks * UPDATE_INTERVAL, totalTicks * UPDATE_INTERVAL)
} }
} }
private fun resetStopwatch() { private fun resetStopwatch() {
updateHandler.removeCallbacksAndMessages(null) Stopwatch.reset()
isRunning = false
currentTicks = 0
totalTicks = 0
currentLap = 1
lapTicks = 0
laps.clear()
updateIcons()
stopwatchAdapter.updateItems(laps)
updateLaps()
view.apply { view.apply {
stopwatch_reset.beGone() stopwatch_reset.beGone()
stopwatch_lap.beGone() stopwatch_lap.beGone()
@ -244,21 +143,22 @@ class StopwatchFragment : Fragment() {
} }
private fun changeSorting(clickedValue: Int) { private fun changeSorting(clickedValue: Int) {
sorting = if (sorting and clickedValue != 0) { val sorting = if (Lap.sorting and clickedValue != 0) {
sorting.flipBit(SORT_DESCENDING) Lap.sorting.flipBit(SORT_DESCENDING)
} else { } else {
clickedValue or SORT_DESCENDING clickedValue or SORT_DESCENDING
} }
updateSorting() updateSorting(sorting)
} }
private fun updateSorting() { private fun updateSorting(sorting: Int) {
updateSortingIndicators() updateSortingIndicators(sorting)
Lap.sorting = sorting Lap.sorting = sorting
requireContext().config.stopwatchLapsSort = sorting
updateLaps() updateLaps()
} }
private fun updateSortingIndicators() { private fun updateSortingIndicators(sorting: Int) {
var bitmap = requireContext().resources.getColoredBitmap(R.drawable.ic_sorting_triangle_vector, requireContext().getProperPrimaryColor()) var bitmap = requireContext().resources.getColoredBitmap(R.drawable.ic_sorting_triangle_vector, requireContext().getProperPrimaryColor())
view.apply { view.apply {
stopwatch_sorting_indicator_1.beInvisibleIf(sorting and SORT_BY_LAP == 0) stopwatch_sorting_indicator_1.beInvisibleIf(sorting and SORT_BY_LAP == 0)
@ -281,20 +181,18 @@ class StopwatchFragment : Fragment() {
} }
private fun updateLaps() { private fun updateLaps() {
stopwatchAdapter.updateItems(laps) stopwatchAdapter.updateItems(Stopwatch.laps)
} }
private val updateRunnable = object : Runnable { private val updateListener = object : Stopwatch.UpdateListener {
override fun run() { override fun onUpdate(totalTime: Long, lapTime: Long, useLongerMSFormat: Boolean) {
if (isRunning) { updateDisplayedText(totalTime, lapTime, useLongerMSFormat)
if (totalTicks % 10 == 0) { }
updateDisplayedText()
} override fun onStateChanged(state: Stopwatch.State) {
totalTicks++ updateIcons(state)
currentTicks++ view.stopwatch_lap.beVisibleIf(state == Stopwatch.State.RUNNING)
lapTicks++ view.stopwatch_reset.beVisibleIf(state != Stopwatch.State.STOPPED)
updateHandler.postAtTime(this, uptimeAtStart + currentTicks * UPDATE_INTERVAL)
}
} }
} }
} }

View File

@ -10,6 +10,7 @@ import com.simplemobiletools.clock.models.TimerState
import com.simplemobiletools.commons.extensions.getDefaultAlarmSound import com.simplemobiletools.commons.extensions.getDefaultAlarmSound
import com.simplemobiletools.commons.extensions.getDefaultAlarmTitle import com.simplemobiletools.commons.extensions.getDefaultAlarmTitle
import com.simplemobiletools.commons.helpers.BaseConfig import com.simplemobiletools.commons.helpers.BaseConfig
import com.simplemobiletools.commons.helpers.SORT_DESCENDING
class Config(context: Context) : BaseConfig(context) { class Config(context: Context) : BaseConfig(context) {
companion object { companion object {
@ -81,4 +82,8 @@ class Config(context: Context) : BaseConfig(context) {
var timerChannelId: String? var timerChannelId: String?
get() = prefs.getString(TIMER_CHANNEL_ID, null) get() = prefs.getString(TIMER_CHANNEL_ID, null)
set(id) = prefs.edit().putString(TIMER_CHANNEL_ID, id).apply() set(id) = prefs.edit().putString(TIMER_CHANNEL_ID, id).apply()
var stopwatchLapsSort: Int
get() = prefs.getInt(STOPWATCH_LAPS_SORT_BY, SORT_BY_LAP or SORT_DESCENDING)
set(stopwatchLapsSort) = prefs.edit().putInt(STOPWATCH_LAPS_SORT_BY, stopwatchLapsSort).apply()
} }

View File

@ -22,6 +22,7 @@ const val ALARM_LAST_CONFIG = "alarm_last_config"
const val TIMER_LAST_CONFIG = "timer_last_config" const val TIMER_LAST_CONFIG = "timer_last_config"
const val INCREASE_VOLUME_GRADUALLY = "increase_volume_gradually" const val INCREASE_VOLUME_GRADUALLY = "increase_volume_gradually"
const val ALARMS_SORT_BY = "alarms_sort_by" const val ALARMS_SORT_BY = "alarms_sort_by"
const val STOPWATCH_LAPS_SORT_BY = "stopwatch_laps_sort_by"
const val TABS_COUNT = 4 const val TABS_COUNT = 4
const val EDITED_TIME_ZONE_SEPARATOR = ":" const val EDITED_TIME_ZONE_SEPARATOR = ":"

View File

@ -0,0 +1,130 @@
package com.simplemobiletools.clock.helpers
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import com.simplemobiletools.clock.models.Lap
import java.util.concurrent.CopyOnWriteArraySet
private const val UPDATE_INTERVAL = 10L
object Stopwatch {
private val updateHandler = Handler(Looper.getMainLooper())
private var uptimeAtStart = 0L
private var totalTicks = 0
private var currentTicks = 0 // ticks that reset at pause
private var lapTicks = 0
private var currentLap = 1
val laps = ArrayList<Lap>()
var state = State.STOPPED
private set(value) {
field = value
for (listener in updateListeners) {
listener.onStateChanged(value)
}
}
private var updateListeners = CopyOnWriteArraySet<UpdateListener>()
fun reset() {
updateHandler.removeCallbacksAndMessages(null)
state = State.STOPPED
currentTicks = 0
totalTicks = 0
currentLap = 1
lapTicks = 0
laps.clear()
}
fun toggle(setUptimeAtStart: Boolean) {
if (state != State.RUNNING) {
state = State.RUNNING
updateHandler.post(updateRunnable)
if (setUptimeAtStart) {
uptimeAtStart = SystemClock.uptimeMillis()
}
} else {
state = State.PAUSED
val prevSessionsMS = (totalTicks - currentTicks) * UPDATE_INTERVAL
val totalDuration = SystemClock.uptimeMillis() - uptimeAtStart + prevSessionsMS
updateHandler.removeCallbacksAndMessages(null)
currentTicks = 0
totalTicks--
for (listener in updateListeners) {
listener.onUpdate(totalDuration, -1, true)
}
}
}
fun lap() {
if (laps.isEmpty()) {
val lap = Lap(currentLap++, lapTicks * UPDATE_INTERVAL, totalTicks * UPDATE_INTERVAL)
laps.add(0, lap)
lapTicks = 0
} else {
laps.first().apply {
lapTime = lapTicks * UPDATE_INTERVAL
totalTime = totalTicks * UPDATE_INTERVAL
}
}
val lap = Lap(currentLap++, lapTicks * UPDATE_INTERVAL, totalTicks * UPDATE_INTERVAL)
laps.add(0, lap)
lapTicks = 0
}
/**
* Add a update listener to the stopwatch. The listener gets the current state
* immediately after adding. To avoid memory leaks the listener should be removed
* from the stopwatch.
* @param updateListener the listener
*/
fun addUpdateListener(updateListener: UpdateListener) {
updateListeners.add(updateListener)
updateListener.onUpdate(
totalTicks * UPDATE_INTERVAL,
lapTicks * UPDATE_INTERVAL,
state != State.STOPPED
)
updateListener.onStateChanged(state)
}
/**
* Remove the listener from the stopwatch
* @param updateListener the listener
*/
fun removeUpdateListener(updateListener: UpdateListener) {
updateListeners.remove(updateListener)
}
private val updateRunnable = object : Runnable {
override fun run() {
if (state == State.RUNNING) {
if (totalTicks % 10 == 0) {
for (listener in updateListeners) {
listener.onUpdate(
totalTicks * UPDATE_INTERVAL,
lapTicks * UPDATE_INTERVAL,
false
)
}
}
totalTicks++
currentTicks++
lapTicks++
updateHandler.postAtTime(this, uptimeAtStart + currentTicks * UPDATE_INTERVAL)
}
}
}
enum class State {
RUNNING,
PAUSED,
STOPPED
}
interface UpdateListener {
fun onUpdate(totalTime: Long, lapTime: Long, useLongerMSFormat: Boolean)
fun onStateChanged(state: State)
}
}