You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

秒表应用准确性咨询及安卓毫秒级显示实现方法求教

关于毫秒级秒表实现的疑问与解决方案

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 Choreographer eliminates cumulative drift that can happen with postDelayed() or fixed coroutine delays.

内容的提问来源于stack exchange,提问作者Deeraj Theepshi

火山引擎 最新活动