refactor(face): 重构倒计时工具,统一使用 CountDownUtil 实例化方案

- CountDownUtil 由 object 单例改为 class,各调用方持有独立实例,互不干扰
- 修复 onCompletion 取消时误触发 onFinish 的 bug
- LoginByFaceActivity 新增 recognizeCountDownUtil / leftCountDownUtil 两个实例
- 移除 CountDownManager 及其所有调用,消除两套倒计时并存的混乱

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 15:50:36 +08:00
co-authored by Claude Sonnet 4.6
parent f99538890c
commit 0a110020b9
3 changed files with 46 additions and 192 deletions
@@ -49,7 +49,6 @@ import com.sw.platecabinet.ext.visible
import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.UserFaceModel import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.utils.BitmapSaver import com.sw.platecabinet.utils.BitmapSaver
import com.sw.platecabinet.utils.CountDownManager
import com.sw.platecabinet.utils.CountDownUtil import com.sw.platecabinet.utils.CountDownUtil
import com.sw.platecabinet.utils.Debouncer import com.sw.platecabinet.utils.Debouncer
import com.sw.platecabinet.utils.LogFileUtil import com.sw.platecabinet.utils.LogFileUtil
@@ -169,6 +168,12 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
private val leftDebouncer = Debouncer(30 * 1000) private val leftDebouncer = Debouncer(30 * 1000)
/** 识别超时倒计时(15秒无操作跳回初始页) */
private val recognizeCountDownUtil = CountDownUtil()
/** 用户离开画面后的倒计时(llCollectButton 显示状态下无人脸时触发) */
private val leftCountDownUtil = CountDownUtil()
private var registerHandler: RegisterCallbackHandler? = null private var registerHandler: RegisterCallbackHandler? = null
private fun isFitData(userFaceInfo: UserFaceInfo?): Boolean { private fun isFitData(userFaceInfo: UserFaceInfo?): Boolean {
@@ -331,7 +336,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
} }
private fun recognizeFail() { private fun recognizeFail() {
CountDownManager.cancelCountDown() recognizeCountDownUtil.cancelCountDown()
binding.tvToInit.text = "" binding.tvToInit.text = ""
binding.tvSimilarValue.text = "" binding.tvSimilarValue.text = ""
binding.llCollectButton.visible() binding.llCollectButton.visible()
@@ -389,7 +394,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
PlateUtils.open(this, item.equipmentBoxCode, false) { PlateUtils.open(this, item.equipmentBoxCode, false) {
goInitActivity() goInitActivity()
} }
CountDownManager.cancelCountDown() recognizeCountDownUtil.cancelCountDown()
} }
currentPlateInfo = item currentPlateInfo = item
MyApp.plateList = items MyApp.plateList = items
@@ -699,8 +704,9 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
resumeCamera() resumeCamera()
registerHandler = RegisterCallbackHandler() registerHandler = RegisterCallbackHandler()
CountDownManager.startCountDown( recognizeCountDownUtil.startCountDown(
totalSeconds = 15, total = 15,
lifecycleScope = lifecycleScope,
onTick = { remaining -> onTick = { remaining ->
loadLogInfo("resetRecognizeState剩余时间:$remaining") loadLogInfo("resetRecognizeState剩余时间:$remaining")
if (judgeRecognizeState()) return@startCountDown if (judgeRecognizeState()) return@startCountDown
@@ -725,7 +731,7 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
override fun onPause() { override fun onPause() {
pauseCamera() pauseCamera()
super.onPause() super.onPause()
CountDownManager.cancelCountDown() recognizeCountDownUtil.cancelCountDown()
} }
private fun pauseCamera() { private fun pauseCamera() {
@@ -883,19 +889,18 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
private var isCollectFace = false private var isCollectFace = false
private fun leftCountDown() { private fun leftCountDown() {
CountDownUtil.startCountDown(total = 15, lifecycleScope = lifecycleScope, onStart = { leftCountDownUtil.startCountDown(total = 15, lifecycleScope = lifecycleScope, onStart = {
loadLogInfo("onPreview-----------leftCountDown-------------") loadLogInfo("onPreview-----------leftCountDown-------------")
}, onTick = { }, onTick = {
loadLogInfo("leftCountDown.onTick:$it") loadLogInfo("leftCountDown.onTick:$it")
if (facePreviewInfoList.isNullOrEmpty().not()) { if (facePreviewInfoList.isNullOrEmpty().not()) {
loadLogInfo("leftCountDown.onTick-cancel()") loadLogInfo("leftCountDown.onTick-cancel()")
CountDownUtil.cancelCountDown() leftCountDownUtil.cancelCountDown()
return@startCountDown return@startCountDown
} }
}, onFinish = { }, onFinish = {
CountDownUtil.cancelCountDown()
if (facePreviewInfoList.isNullOrEmpty().not()) { if (facePreviewInfoList.isNullOrEmpty().not()) {
loadLogInfo("leftCountDown.onFinish-cancel()") loadLogInfo("leftCountDown.onFinish-人脸未离开,不跳转")
return@startCountDown return@startCountDown
} }
goInitActivity() goInitActivity()
@@ -1,135 +0,0 @@
package com.sw.platecabinet.utils
import android.os.CountDownTimer
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.Log
/**
* 无状态残留的CountDownTimer封装(解决时间残留问题)
* @param millisInFuture 总时长(毫秒)
* @param countDownInterval 倒计时间隔(毫秒)
* @param onTick 倒计时回调(剩余毫秒)
* @param onFinish 结束回调
*/
class SafeCountDownTimer(
private val millisInFuture: Long,
private val countDownInterval: Long,
private val onTick: (Long) -> Unit,
private val onFinish: () -> Unit
) {
// 独立Handler,避免与其他CountDownTimer复用消息队列
private val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
synchronized(this@SafeCountDownTimer) {
// 仅当timer未取消时处理消息,避免残留消息触发
if (!isCancelled) {
super.handleMessage(msg)
}
}
}
}
// 核心:标记是否已取消,避免残留消息触发
@Volatile
private var isCancelled = false
// 实际的CountDownTimer实例
private var timer: CountDownTimer? = null
/**
* 启动倒计时(启动前自动清空旧状态)
*/
fun start() {
// 第一步:强制取消旧实例+清空所有残留消息(核心!)
cancel()
// 第二步:重置取消标记
isCancelled = false
// 第三步:新建CountDownTimer实例,绑定独立Handler
timer = object : CountDownTimer(millisInFuture, countDownInterval) {
override fun onTick(millisUntilFinished: Long) {
// 双重校验:仅未取消时触发回调
if (!isCancelled) {
onTick.invoke(millisUntilFinished)
}
}
override fun onFinish() {
if (!isCancelled) {
onFinish.invoke()
// 结束后主动清空,避免残留
cancel()
}
}
}
// 第四步:启动新实例
timer?.start()
Log.d("SafeCountDownTimer", "倒计时启动,总时长:$millisInFuture 毫秒")
}
/**
* 取消倒计时(彻底清空所有状态)
*/
fun cancel() {
// 标记为已取消,拦截后续消息
isCancelled = true
// 取消timer实例
timer?.cancel()
timer = null
// 清空Handler所有残留消息(核心:解决旧消息触发问题)
handler.removeCallbacksAndMessages(null)
Log.d("SafeCountDownTimer", "倒计时已取消,清空所有残留状态")
}
}
// ========== 封装成工具类,方便全局调用 ==========
object CountDownManager {
// 保存当前倒计时实例,避免重复启动
private var currentTimer: SafeCountDownTimer? = null
/**
* 启动/重启倒计时(适配人脸识别防抖场景)
* @param totalSeconds 总秒数(如15
* @param onTick 每秒回调剩余秒数
* @param onFinish 结束回调
*/
fun startCountDown(
totalSeconds: Int = 15,
onTick: (Int) -> Unit = {},
onFinish: () -> Unit
) {
// 先取消旧倒计时
cancelCountDown()
// 新建安全倒计时实例
currentTimer = SafeCountDownTimer(
millisInFuture = totalSeconds * 1000L,
countDownInterval = 1000L,
onTick = { millis ->
// 转换为秒数回调
val sec = (millis / 1000).toInt() + 1
onTick.invoke(sec)
},
onFinish = {
onFinish.invoke()
}
)
// 启动倒计时
currentTimer?.start()
}
/**
* 取消当前倒计时
*/
fun cancelCountDown() {
currentTimer?.cancel()
currentTimer = null
}
}
@@ -1,33 +1,26 @@
package com.sw.platecabinet.utils package com.sw.platecabinet.utils
import android.util.Log
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import android.util.Log
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.ranges.downTo
/** /**
* 人脸识别防抖倒计时工具类(单例+防重复+无死循环 * 基于协程的倒计时工具类(实例化使用,各调用方持有独立实例,互不干扰
* 修复:onCompletion 区分正常结束与取消,手动取消时不触发 onFinish
*/ */
object CountDownUtil { class CountDownUtil {
// 原子布尔值:标记是否正在倒计时(防重复启动)
private val isCounting = AtomicBoolean(false)
// 当前倒计时Job(用于取消) private val isCounting = AtomicBoolean(false)
private var currentJob: Job? = null private var currentJob: Job? = null
/** /**
* 启动/重启防抖倒计时(同一时间仅一个倒计时运行 * 启动倒计时(若已在运行则忽略,需重启请先调用 cancelCountDown
* @param totalSeconds 倒计时总秒数(如15 * @param total 总秒数
* @param lifecycleScope Activity/FragmentlifecycleScope(必须) * @param lifecycleScope Activity/FragmentlifecycleScope
* @param onStart 启动回调(主线程)
* @param onTick 每秒回调剩余秒数(主线程) * @param onTick 每秒回调剩余秒数(主线程)
* @param onFinish 倒计时结束回调(主线程) * @param onFinish 正常结束回调(主线程,手动取消时不触发
*/ */
fun startCountDown( fun startCountDown(
total: Int = 15, total: Int = 15,
@@ -36,46 +29,37 @@ object CountDownUtil {
onTick: (Int) -> Unit = {}, onTick: (Int) -> Unit = {},
onFinish: () -> Unit onFinish: () -> Unit
) { ) {
// 1. 防重复:如果正在倒计时,先取消旧任务 if (isCounting.get()) return
if (isCounting.get()) {
//cancelCountDown()
return
}
// 2. 标记为正在倒计时
isCounting.set(true) isCounting.set(true)
// 3. 启动新倒计时
currentJob = flow { currentJob = flow {
Log.d("CountDownUtil", "倒计时启动,总时长:$total")
for (i in total downTo 1) { for (i in total downTo 1) {
emit(i) emit(i)
delay(1000) // IO线程等待1秒,无死循环 delay(1000L)
} }
} }
.flowOn(Dispatchers.IO) // 上游IO线程,避免阻塞主线程 .flowOn(Dispatchers.IO)
.onStart { .onStart {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) { onStart() }
onStart.invoke()
}
} }
.onEach { remaining -> .onEach { remaining ->
// 主线程回调剩余时间 withContext(Dispatchers.Main) { onTick(remaining) }
withContext(Dispatchers.Main) {
onTick.invoke(remaining)
}
} }
.onCompletion { .onCompletion { cause ->
// 倒计时结束:重置状态+回调
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
isCounting.set(false) // 重置标记 isCounting.set(false)
currentJob = null // 置空Job currentJob = null
Log.d("CountDownUtil", "倒计时正常结束") // cause == null 为正常结束,有异常(含 CancellationException)时不触发 onFinish
onFinish.invoke() if (cause == null) {
Log.d("CountDownUtil", "倒计时正常结束")
onFinish()
} else {
Log.d("CountDownUtil", "倒计时被取消:${cause.message}")
}
} }
} }
.catch { e -> .catch { e ->
// 捕获异常:避免崩溃,重置状态
Log.e("CountDownUtil", "倒计时异常:${e.message}", e) Log.e("CountDownUtil", "倒计时异常:${e.message}", e)
isCounting.set(false) isCounting.set(false)
currentJob = null currentJob = null
@@ -84,12 +68,12 @@ object CountDownUtil {
} }
/** /**
* 取消倒计时(手动停止 * 取消倒计时(不触发 onFinish
*/ */
fun cancelCountDown() { fun cancelCountDown() {
currentJob?.cancel() // 取消Job currentJob?.cancel()
currentJob = null // 置空,避免残留 currentJob = null
isCounting.set(false) // 重置标记 isCounting.set(false)
Log.d("CountDownUtil", "倒计时被手动取消") Log.d("CountDownUtil", "倒计时取消")
} }
} }