秒表应用准确性咨询及安卓毫秒级显示实现方法求教
Hey there! Great question—let’s unpack this step by step.
是不是所有秒表应用(包括安卓内置)都只用10次/秒的更新频率?
Short answer: No. While it’s true that updating the UI more than 10 times per second (every 100ms) might be overkill for basic visibility, many stopwatch apps (including Android’s built-in one) use a higher update rate aligned with the screen’s refresh rate (e.g., 60Hz = ~16.67ms per frame).
The key here is separating precision of the underlying timer from UI update frequency:
- The actual time tracking always relies on high-precision sources (like
System.nanoTime()), so even if the UI doesn’t update every millisecond, the elapsed time calculation remains accurate. - UI updates are optimized to balance smoothness and performance—updating at the screen’s refresh rate ensures the display looks fluid, while avoiding unnecessary CPU/GPU usage from more frequent updates that the human eye can’t perceive.
Android’s built-in stopwatch, for example, shows millisecond values and updates the UI in sync with screen frames to keep the display smooth, even though the underlying timer tracks time far more precisely than the UI shows.
实现毫秒级精准显示的方案
Here’s a robust, Android-focused implementation approach that balances precision and performance:
1. Use a high-precision time source
Forget System.currentTimeMillis()—it can jump forward/backward if the system time is adjusted. Instead, use System.nanoTime(), which provides a monotonic (always increasing) timestamp ideal for calculating elapsed time.
2. Separate time tracking from UI updates
Track elapsed time in the background (or on a dedicated thread), but update the UI only on the main thread. Avoid relying on Timer or Handler.postDelayed() for fixed intervals—these can accumulate drift over time. Instead, sync updates with the screen’s refresh cycle using Choreographer (recommended) or use coroutines with dynamic delays.
3. Example implementation (Kotlin)
Option 1: Using Coroutines with screen-aligned delays
This approach uses coroutines to update the UI every ~16ms (for 60Hz screens), ensuring smoothness while keeping calculations precise:
import android.os.Bundle import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import kotlinx.coroutines.* import java.util.concurrent.TimeUnit class StopwatchActivity : AppCompatActivity() { private lateinit var stopwatchTextView: TextView private var isRunning = false private var startTimeNanos: Long = 0 private var elapsedNanos: Long = 0 private val mainScope = CoroutineScope(Dispatchers.Main) private var updateJob: Job? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_stopwatch) stopwatchTextView = findViewById(R.id.tv_stopwatch) } fun startStopwatch() { if (!isRunning) { isRunning = true startTimeNanos = System.nanoTime() - elapsedNanos updateJob = mainScope.launch { while (isRunning) { elapsedNanos = System.nanoTime() - startTimeNanos updateStopwatchDisplay() // Sync with 60Hz screen (~16ms per frame) delay(16) } } } } fun stopStopwatch() { isRunning = false updateJob?.cancel() } private fun updateStopwatchDisplay() { val totalMillis = TimeUnit.NANOSECONDS.toMillis(elapsedNanos) val hours = (totalMillis / 3600000).toInt() val minutes = ((totalMillis % 3600000) / 60000).toInt() val seconds = ((totalMillis % 60000) / 1000).toInt() val millis = (totalMillis % 1000).toInt() stopwatchTextView.text = String.format( "%02d:%02d:%02d.%03d", hours, minutes, seconds, millis ) } override fun onDestroy() { super.onDestroy() mainScope.cancel() } }
Option 2: Using Choreographer (screen refresh sync)
For even tighter alignment with the screen’s refresh cycle (no drift from fixed delays), use Choreographer:
import android.os.Bundle import android.view.Choreographer import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import java.util.concurrent.TimeUnit class StopwatchActivity : AppCompatActivity() { private lateinit var stopwatchTextView: TextView private var isRunning = false private var startTimeNanos: Long = 0 private var elapsedNanos: Long = 0 private var frameCallback: Choreographer.FrameCallback? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_stopwatch) stopwatchTextView = findViewById(R.id.tv_stopwatch) } fun startStopwatch() { if (!isRunning) { isRunning = true startTimeNanos = System.nanoTime() - elapsedNanos frameCallback = object : Choreographer.FrameCallback { override fun doFrame(frameTimeNanos: Long) { if (isRunning) { elapsedNanos = System.nanoTime() - startTimeNanos updateStopwatchDisplay() Choreographer.getInstance().postFrameCallback(this) } } } Choreographer.getInstance().postFrameCallback(frameCallback) } } fun stopStopwatch() { isRunning = false frameCallback?.let { Choreographer.getInstance().removeFrameCallback(it) } } private fun updateStopwatchDisplay() { // Same time formatting logic as above val totalMillis = TimeUnit.NANOSECONDS.toMillis(elapsedNanos) val hours = (totalMillis / 3600000).toInt() val minutes = ((totalMillis % 3600000) / 60000).toInt() val seconds = ((totalMillis % 60000) / 1000).toInt() val millis = (totalMillis % 1000).toInt() stopwatchTextView.text = String.format( "%02d:%02d:%02d.%03d", hours, minutes, seconds, millis ) } override fun onDestroy() { super.onDestroy() stopStopwatch() } }
Key Notes
- Precision First: The elapsed time calculation uses
System.nanoTime()to avoid drift from system time changes. - UI Optimization: Updating at the screen’s refresh rate ensures the display looks smooth without wasting resources.
- Avoid Fixed Delays: Using
Choreographereliminates cumulative drift that can happen withpostDelayed()or fixed coroutine delays.
内容的提问来源于stack exchange,提问作者Deeraj Theepshi




