1. 功能需求解析
在Android应用开发中,定时器功能是常见的基础需求。这个项目要实现的是一个具备暂停和继续功能的倒计时器,并在计时结束时触发回调。这种功能在健身应用(组间休息计时)、学习应用(番茄钟)、游戏(技能冷却)等场景中都有广泛应用。
核心功能点包括:
- 精确的倒计时功能(n秒)
- 暂停和继续操作
- 计时结束回调
- 线程安全的计时管理
2. 技术方案选型
2.1 计时器实现方案对比
在Kotlin中实现定时器主要有以下几种方式:
Handler + Runnable
- 优点:轻量级,直接使用Android消息机制
- 缺点:需要手动处理线程切换
CountDownTimer
- 优点:Android原生API,封装完善
- 缺点:不支持暂停后继续
Coroutine + Flow
- 优点:响应式编程,协程天然支持取消
- 缺点:需要理解协程概念
RxJava Timer
- 优点:强大的响应式操作符
- 缺点:引入较重依赖
提示:对于需要暂停/继续的场景,推荐使用协程方案,因其天然支持取消和恢复操作。
2.2 最终方案:协程实现
我们选择协程方案,主要因为:
- 现代Android开发推荐使用协程处理异步任务
- 协程的取消/恢复机制完美匹配暂停/继续需求
- 与ViewModel等架构组件集成良好
3. 核心实现详解
3.1 基础计时器实现
class CountdownTimer( private val totalTime: Long, // 总计时毫秒数 private val interval: Long = 1000L, // 更新间隔 private val onTick: (Long) -> Unit, // 每秒回调 private val onFinish: () -> Unit // 结束回调 ) { private var job: Job? = null private var remainingTime = totalTime fun start() { job = CoroutineScope(Dispatchers.Main).launch { while (remainingTime > 0) { delay(interval) remainingTime -= interval onTick(remainingTime) } onFinish() } } }关键点说明:
- 使用
Dispatchers.Main确保回调在主线程执行 remainingTime记录剩余时间,支持暂停后继续delay()是协程的挂起函数,不会阻塞线程
3.2 暂停与继续功能
扩展上述类,添加暂停/继续功能:
private var isPaused = false private var pauseTime: Long = 0L fun pause() { if (job?.isActive == true) { isPaused = true pauseTime = System.currentTimeMillis() job?.cancel() } } fun resume() { if (isPaused) { val pausedDuration = System.currentTimeMillis() - pauseTime remainingTime -= pausedDuration isPaused = false start() // 重新开始计时 } }注意事项:
- 暂停时记录系统时间,用于计算暂停时长
- 恢复时调整剩余时间,确保总时长准确
- 每次暂停都需要创建新的协程job
3.3 生命周期管理
在Android中必须正确处理生命周期:
// 在Activity/Fragment中 private lateinit var timer: CountdownTimer override fun onStart() { super.onStart() timer = CountdownTimer(...) timer.start() } override fun onStop() { super.onStop() timer.pause() // 或直接取消 timer.cancel() }或者在ViewModel中使用:
class TimerViewModel : ViewModel() { private val timer = CountdownTimer(...) fun startTimer() = timer.start() fun pauseTimer() = timer.pause() override fun onCleared() { timer.cancel() } }4. 高级功能扩展
4.1 状态保存与恢复
处理配置变更(如屏幕旋转)时保存状态:
// 在ViewModel中 private var savedRemainingTime: Long = 0L fun saveState() { savedRemainingTime = timer.getRemainingTime() } fun restoreState() { timer.setRemainingTime(savedRemainingTime) }4.2 精确计时补偿
解决系统延迟导致的计时误差:
var lastTickTime = System.currentTimeMillis() while (remainingTime > 0) { val currentTime = System.currentTimeMillis() val realInterval = currentTime - lastTickTime lastTickTime = currentTime remainingTime -= realInterval onTick(remainingTime.coerceAtLeast(0)) val delayTime = interval - (System.currentTimeMillis() - currentTime) if (delayTime > 0) delay(delayTime) }4.3 后台计时处理
使用前台服务保持精确计时:
class TimerService : Service() { private val timer = CountdownTimer(...) override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { startForeground(NOTIFICATION_ID, createNotification()) timer.start() return START_STICKY } private fun createNotification(): Notification { // 创建带有计时信息的通知 } }5. 常见问题与解决方案
5.1 计时不准确
现象:计时结束时实际时间与预期不符原因:系统延迟累积解决:实现误差补偿机制(见4.2节)
5.2 暂停后继续时间错误
现象:暂停后继续,剩余时间计算错误原因:未正确处理系统时间差解决:
fun resume() { if (isPaused) { val pausedDuration = System.currentTimeMillis() - pauseTime remainingTime = (remainingTime - pausedDuration).coerceAtLeast(0) // 其余逻辑... } }5.3 内存泄漏
现象:Activity销毁后回调仍在执行解决:
// 在Activity中 override fun onDestroy() { timer.cancel() super.onDestroy() } // 或者在Timer类中添加 fun cancel() { job?.cancel() }5.4 后台计时限制
现象:应用进入后台后计时停止解决:
- 使用前台服务(见4.3节)
- 使用WorkManager处理长时间计时
- 记录暂停时间,恢复时重新计算
6. 性能优化建议
减少UI更新频率:
// 只在秒数变化时更新UI var lastSeconds = -1L onTick = { remaining -> val seconds = remaining / 1000 if (seconds != lastSeconds) { updateUI(seconds) lastSeconds = seconds } }使用轻量级回调:
// 避免在回调中执行耗时操作 onTick = { remaining -> viewBinding.tvTimer.text = formatTime(remaining) }协程上下文优化:
// 对于计算密集型操作 CoroutineScope(Dispatchers.Default).launch { // 计算逻辑 withContext(Dispatchers.Main) { // 更新UI } }对象复用:
// 重用Formatter对象 private val timeFormatter = SimpleDateFormat("mm:ss", Locale.getDefault()) fun formatTime(ms: Long): String { return timeFormatter.format(Date(ms)) }
7. 完整实现代码
class AdvancedCountdownTimer( totalTime: Long, interval: Long = 1000L, private val onTick: (Long) -> Unit, private val onFinish: () -> Unit ) { private var job: Job? = null private var remainingTime = totalTime private var isPaused = false private var pauseTime: Long = 0L private var lastTickTime = 0L fun start() { if (job?.isActive == true) return lastTickTime = System.currentTimeMillis() job = CoroutineScope(Dispatchers.Main).launch { while (remainingTime > 0) { val currentTime = System.currentTimeMillis() val realInterval = currentTime - lastTickTime lastTickTime = currentTime remainingTime -= realInterval onTick(remainingTime.coerceAtLeast(0)) val delayTime = interval - (System.currentTimeMillis() - currentTime) if (delayTime > 0) delay(delayTime) } onFinish() } } fun pause() { if (job?.isActive == true) { isPaused = true pauseTime = System.currentTimeMillis() job?.cancel() } } fun resume() { if (isPaused) { val pausedDuration = System.currentTimeMillis() - pauseTime remainingTime -= pausedDuration isPaused = false start() } } fun cancel() { job?.cancel() remainingTime = totalTime } fun getRemainingTime() = remainingTime }使用示例:
val timer = AdvancedCountdownTimer( totalTime = 30000L, // 30秒 onTick = { remaining -> binding.tvTimer.text = "剩余: ${remaining / 1000}秒" }, onFinish = { Toast.makeText(this, "计时结束!", Toast.LENGTH_SHORT).show() } ) binding.btnStart.setOnClickListener { timer.start() } binding.btnPause.setOnClickListener { timer.pause() } binding.btnResume.setOnClickListener { timer.resume() }8. 测试要点
基础功能测试:
- 正常计时是否准确
- 暂停后继续是否保持总时长
- 结束回调是否触发
边界条件测试:
- 剩余1秒时暂停
- 多次快速暂停/继续
- 计时结束瞬间暂停
异常情况测试:
- 后台运行测试
- 低电量模式
- 配置变更(屏幕旋转)
性能测试:
- 长时间运行内存占用
- 频繁暂停/继续的响应速度
- 多计时器并行运行
9. 替代方案对比
当标准实现不满足需求时,可以考虑:
WorkManager方案:
- 适合需要持久化的长时间计时
- 保证计时任务最终完成
- 但实时性较差
AlarmManager方案:
- 适合精确的跨进程计时
- 可以唤醒设备
- 但API较复杂
Ticker + StateFlow:
val ticker = ticker(1000L) val timeFlow = flow { var remaining = totalTime while (remaining > 0) { ticker.receive() remaining -= 1000L emit(remaining) } }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), totalTime)- 响应式编程风格
- 适合Compose项目
10. 实际应用建议
UI显示优化:
- 使用动画平滑过渡数字变化
- 添加进度条直观显示剩余时间比例
- 不同时段使用颜色区分(如最后5秒变红)
声音反馈:
fun playTickSound() { val soundPool = SoundPool.Builder().build() val soundId = soundPool.load(context, R.raw.tick, 1) soundPool.play(soundId, 1f, 1f, 0, 0, 1f) }振动反馈:
fun vibrateOnFinish() { val vibrator = context.getSystemService<Vibrator>() vibrator?.vibrate(VibrationEffect.createOneShot(500, 255)) }多平台适配:
- 使用KMM共享计时逻辑
- 针对不同平台实现原生UI
- 统一状态管理
在实现过程中,我发现正确处理协程的生命周期是最关键的,特别是在Android这种频繁发生配置变更的环境中。一个好的做法是将计时器逻辑放在ViewModel中,这样可以在屏幕旋转时保持计时状态。另外,对于需要精确到秒的计时场景,建议加上误差补偿机制,虽然会增加一些代码复杂度,但能显著提升用户体验。