perf: optimize stopwatch updates (#213)

* fix: bump stopwatch update interval to 100ms

The UI does not need to update 50 times a second.

* perf: optimize stopwatch updates

- Added a static id for live lap for cleaner logic
- Added `updateLiveLap` method to the stopwatch adapter. This is responsible for updating the live lap and maintaining its position in the list

* refactor: move Lap.kt to extensions package
This commit is contained in:
Naveen Singh
2025-06-20 19:40:03 +05:30
committed by GitHub
parent 3563c3d4d2
commit dafa9e0ad8
5 changed files with 50 additions and 22 deletions

View File

@@ -10,17 +10,22 @@ import org.fossify.clock.extensions.formatStopwatchTime
import org.fossify.clock.helpers.SORT_BY_LAP
import org.fossify.clock.helpers.SORT_BY_LAP_TIME
import org.fossify.clock.helpers.SORT_BY_TOTAL_TIME
import org.fossify.clock.extensions.isLive
import org.fossify.clock.models.Lap
import org.fossify.commons.adapters.MyRecyclerViewAdapter
import org.fossify.commons.views.MyRecyclerView
class StopwatchAdapter(
activity: SimpleActivity,
var laps: ArrayList<Lap>,
private var laps: ArrayList<Lap>,
recyclerView: MyRecyclerView,
itemClick: (Any) -> Unit,
) : MyRecyclerViewAdapter(activity, recyclerView, itemClick) {
init {
recyclerView.itemAnimator = null
}
override fun getActionMenuId() = 0
override fun prepareActionMode(menu: Menu) {}
@@ -60,9 +65,26 @@ class StopwatchAdapter(
finishActMode()
}
fun updateLiveLap(totalTime: Long, lapTime: Long) {
val oldIndex = laps.indexOfFirst { it.isLive() }
if (oldIndex == -1) return
laps[oldIndex].lapTime = lapTime
laps[oldIndex].totalTime = totalTime
laps.sort()
// the live lap might have changed position
val newIndex = laps.indexOfFirst { it.isLive() }
if (oldIndex == newIndex) {
notifyItemChanged(newIndex)
} else {
notifyItemMoved(oldIndex, newIndex)
notifyItemChanged(newIndex)
}
}
private fun setupView(view: View, lap: Lap) {
ItemLapBinding.bind(view).apply {
lapOrder.text = lap.id.toString()
lapOrder.text = if (lap.isLive()) laps.size.toString() else lap.id.toString()
lapOrder.setTextColor(textColor)
lapOrder.setOnClickListener {
itemClick(SORT_BY_LAP)

View File

@@ -0,0 +1,6 @@
package org.fossify.clock.extensions
import org.fossify.clock.helpers.STOPWATCH_LIVE_LAP_ID
import org.fossify.clock.models.Lap
fun Lap.isLive() = id == STOPWATCH_LIVE_LAP_ID

View File

@@ -17,6 +17,7 @@ import org.fossify.clock.extensions.formatStopwatchTime
import org.fossify.clock.helpers.SORT_BY_LAP
import org.fossify.clock.helpers.SORT_BY_LAP_TIME
import org.fossify.clock.helpers.SORT_BY_TOTAL_TIME
import org.fossify.clock.helpers.STOPWATCH_LIVE_LAP_ID
import org.fossify.clock.helpers.Stopwatch
import org.fossify.clock.models.Lap
import org.fossify.commons.dialogs.PermissionRequiredDialog
@@ -38,7 +39,7 @@ import org.fossify.commons.helpers.SORT_DESCENDING
class StopwatchFragment : Fragment() {
lateinit var stopwatchAdapter: StopwatchAdapter
private var stopwatchAdapter: StopwatchAdapter? = null
private lateinit var binding: FragmentStopwatchBinding
private var latestLapTime: Long = 0L
@@ -129,6 +130,12 @@ class StopwatchFragment : Fragment() {
)
stopwatchReset.applyColorFilter(requireContext().getProperTextColor())
}
stopwatchAdapter?.apply {
updatePrimaryColor()
updateBackgroundColor(requireContext().getProperBackgroundColor())
updateTextColor(requireContext().getProperTextColor())
}
}
private fun updateIcons(state: Stopwatch.State) {
@@ -233,37 +240,28 @@ class StopwatchFragment : Fragment() {
val allLaps = ArrayList(Stopwatch.laps)
if (Stopwatch.laps.isNotEmpty() && Stopwatch.state != Stopwatch.State.STOPPED) {
allLaps += Lap(
id = Stopwatch.laps.size + 1,
id = STOPWATCH_LIVE_LAP_ID,
lapTime = latestLapTime,
totalTime = latestTotalTime
)
}
allLaps.sort()
stopwatchAdapter.apply {
updatePrimaryColor()
updateBackgroundColor(requireContext().getProperBackgroundColor())
updateTextColor(requireContext().getProperTextColor())
updateItems(allLaps)
}
stopwatchAdapter?.updateItems(allLaps)
}
private val updateListener = object : Stopwatch.UpdateListener {
override fun onUpdate(totalTime: Long, lapTime: Long, useLongerMSFormat: Boolean) {
activity?.runOnUiThread {
binding.stopwatchTime.text = totalTime.formatStopwatchTime(useLongerMSFormat)
latestLapTime = lapTime
latestTotalTime = totalTime
updateLaps()
}
binding.stopwatchTime.text = totalTime.formatStopwatchTime(useLongerMSFormat)
latestLapTime = lapTime
latestTotalTime = totalTime
stopwatchAdapter?.updateLiveLap(totalTime, lapTime)
}
override fun onStateChanged(state: Stopwatch.State) {
activity?.runOnUiThread {
updateIcons(state)
binding.stopwatchLap.beVisibleIf(state == Stopwatch.State.RUNNING)
binding.stopwatchReset.beVisibleIf(state != Stopwatch.State.STOPPED)
}
updateIcons(state)
binding.stopwatchLap.beVisibleIf(state == Stopwatch.State.RUNNING)
binding.stopwatchReset.beVisibleIf(state != Stopwatch.State.STOPPED)
}
}
}

View File

@@ -83,6 +83,8 @@ const val SORT_BY_LAP = 1
const val SORT_BY_LAP_TIME = 2
const val SORT_BY_TOTAL_TIME = 4
const val STOPWATCH_LIVE_LAP_ID = Int.MAX_VALUE
// alarm and timer sorting
const val SORT_BY_CREATION_ORDER = 0
const val SORT_BY_ALARM_TIME = 1

View File

@@ -11,7 +11,7 @@ import kotlinx.coroutines.launch
import org.fossify.clock.models.Lap
import java.util.concurrent.CopyOnWriteArraySet
private const val UPDATE_INTERVAL_MS = 20L
private const val UPDATE_INTERVAL_MS = 100L
object Stopwatch {
private var startTime = 0L