From 0a110020b9439cc9e7a2b746ef3a82498aca17ca Mon Sep 17 00:00:00 2001 From: lvmeng <848755140@qq.com> Date: Fri, 6 Mar 2026 15:50:36 +0800 Subject: [PATCH] =?UTF-8?q?refactor(face):=20=E9=87=8D=E6=9E=84=E5=80=92?= =?UTF-8?q?=E8=AE=A1=E6=97=B6=E5=B7=A5=E5=85=B7=EF=BC=8C=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20CountDownUtil=20=E5=AE=9E=E4=BE=8B?= =?UTF-8?q?=E5=8C=96=E6=96=B9=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CountDownUtil 由 object 单例改为 class,各调用方持有独立实例,互不干扰 - 修复 onCompletion 取消时误触发 onFinish 的 bug - LoginByFaceActivity 新增 recognizeCountDownUtil / leftCountDownUtil 两个实例 - 移除 CountDownManager 及其所有调用,消除两套倒计时并存的混乱 Co-Authored-By: Claude Sonnet 4.6 --- .../activity/LoginByFaceActivity.kt | 25 ++-- .../sw/platecabinet/utils/CountDownManager.kt | 135 ------------------ .../sw/platecabinet/utils/CountDownUtil.kt | 78 ++++------ 3 files changed, 46 insertions(+), 192 deletions(-) delete mode 100644 app/src/main/java/com/sw/platecabinet/utils/CountDownManager.kt diff --git a/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt b/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt index 4932476..7b32599 100644 --- a/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt +++ b/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt @@ -49,7 +49,6 @@ import com.sw.platecabinet.ext.visible import com.sw.platecabinet.model.response.EquipmentUserInfo import com.sw.platecabinet.model.response.UserFaceModel import com.sw.platecabinet.utils.BitmapSaver -import com.sw.platecabinet.utils.CountDownManager import com.sw.platecabinet.utils.CountDownUtil import com.sw.platecabinet.utils.Debouncer import com.sw.platecabinet.utils.LogFileUtil @@ -169,6 +168,12 @@ class LoginByFaceActivity : BaseActivity(), private val leftDebouncer = Debouncer(30 * 1000) + /** 识别超时倒计时(15秒无操作跳回初始页) */ + private val recognizeCountDownUtil = CountDownUtil() + + /** 用户离开画面后的倒计时(llCollectButton 显示状态下无人脸时触发) */ + private val leftCountDownUtil = CountDownUtil() + private var registerHandler: RegisterCallbackHandler? = null private fun isFitData(userFaceInfo: UserFaceInfo?): Boolean { @@ -331,7 +336,7 @@ class LoginByFaceActivity : BaseActivity(), } private fun recognizeFail() { - CountDownManager.cancelCountDown() + recognizeCountDownUtil.cancelCountDown() binding.tvToInit.text = "" binding.tvSimilarValue.text = "" binding.llCollectButton.visible() @@ -389,7 +394,7 @@ class LoginByFaceActivity : BaseActivity(), PlateUtils.open(this, item.equipmentBoxCode, false) { goInitActivity() } - CountDownManager.cancelCountDown() + recognizeCountDownUtil.cancelCountDown() } currentPlateInfo = item MyApp.plateList = items @@ -699,8 +704,9 @@ class LoginByFaceActivity : BaseActivity(), resumeCamera() registerHandler = RegisterCallbackHandler() - CountDownManager.startCountDown( - totalSeconds = 15, + recognizeCountDownUtil.startCountDown( + total = 15, + lifecycleScope = lifecycleScope, onTick = { remaining -> loadLogInfo("resetRecognizeState剩余时间:$remaining 秒") if (judgeRecognizeState()) return@startCountDown @@ -725,7 +731,7 @@ class LoginByFaceActivity : BaseActivity(), override fun onPause() { pauseCamera() super.onPause() - CountDownManager.cancelCountDown() + recognizeCountDownUtil.cancelCountDown() } private fun pauseCamera() { @@ -883,19 +889,18 @@ class LoginByFaceActivity : BaseActivity(), private var isCollectFace = false private fun leftCountDown() { - CountDownUtil.startCountDown(total = 15, lifecycleScope = lifecycleScope, onStart = { + leftCountDownUtil.startCountDown(total = 15, lifecycleScope = lifecycleScope, onStart = { loadLogInfo("onPreview-----------leftCountDown-------------") }, onTick = { loadLogInfo("leftCountDown.onTick:$it") if (facePreviewInfoList.isNullOrEmpty().not()) { loadLogInfo("leftCountDown.onTick-cancel()") - CountDownUtil.cancelCountDown() + leftCountDownUtil.cancelCountDown() return@startCountDown } }, onFinish = { - CountDownUtil.cancelCountDown() if (facePreviewInfoList.isNullOrEmpty().not()) { - loadLogInfo("leftCountDown.onFinish-cancel()") + loadLogInfo("leftCountDown.onFinish-人脸未离开,不跳转") return@startCountDown } goInitActivity() diff --git a/app/src/main/java/com/sw/platecabinet/utils/CountDownManager.kt b/app/src/main/java/com/sw/platecabinet/utils/CountDownManager.kt deleted file mode 100644 index 67c3dce..0000000 --- a/app/src/main/java/com/sw/platecabinet/utils/CountDownManager.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/utils/CountDownUtil.kt b/app/src/main/java/com/sw/platecabinet/utils/CountDownUtil.kt index 258ed94..4c209fa 100644 --- a/app/src/main/java/com/sw/platecabinet/utils/CountDownUtil.kt +++ b/app/src/main/java/com/sw/platecabinet/utils/CountDownUtil.kt @@ -1,33 +1,26 @@ package com.sw.platecabinet.utils +import android.util.Log import kotlinx.coroutines.* 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 kotlin.ranges.downTo /** - * 人脸识别防抖倒计时工具类(单例+防重复+无死循环) + * 基于协程的倒计时工具类(实例化使用,各调用方持有独立实例,互不干扰) + * 修复:onCompletion 区分正常结束与取消,手动取消时不触发 onFinish */ -object CountDownUtil { - // 原子布尔值:标记是否正在倒计时(防重复启动) - private val isCounting = AtomicBoolean(false) +class CountDownUtil { - // 当前倒计时Job(用于取消) + private val isCounting = AtomicBoolean(false) private var currentJob: Job? = null /** - * 启动/重启防抖倒计时(同一时间仅一个倒计时运行) - * @param totalSeconds 倒计时总秒数(如15) - * @param lifecycleScope Activity/Fragment的lifecycleScope(必须) + * 启动倒计时(若已在运行则忽略,需重启请先调用 cancelCountDown) + * @param total 总秒数 + * @param lifecycleScope Activity/Fragment 的 lifecycleScope + * @param onStart 启动回调(主线程) * @param onTick 每秒回调剩余秒数(主线程) - * @param onFinish 倒计时结束回调(主线程) + * @param onFinish 正常结束回调(主线程,手动取消时不触发) */ fun startCountDown( total: Int = 15, @@ -36,46 +29,37 @@ object CountDownUtil { onTick: (Int) -> Unit = {}, onFinish: () -> Unit ) { - // 1. 防重复:如果正在倒计时,先取消旧任务 - if (isCounting.get()) { - //cancelCountDown() - return - } + if (isCounting.get()) return - // 2. 标记为正在倒计时 isCounting.set(true) - // 3. 启动新倒计时 currentJob = flow { - Log.d("CountDownUtil", "倒计时启动,总时长:$total 秒") for (i in total downTo 1) { emit(i) - delay(1000) // IO线程等待1秒,无死循环 + delay(1000L) } } - .flowOn(Dispatchers.IO) // 上游IO线程,避免阻塞主线程 + .flowOn(Dispatchers.IO) .onStart { - withContext(Dispatchers.Main) { - onStart.invoke() - } + withContext(Dispatchers.Main) { onStart() } } .onEach { remaining -> - // 主线程回调剩余时间 - withContext(Dispatchers.Main) { - onTick.invoke(remaining) - } + withContext(Dispatchers.Main) { onTick(remaining) } } - .onCompletion { - // 倒计时结束:重置状态+回调 + .onCompletion { cause -> withContext(Dispatchers.Main) { - isCounting.set(false) // 重置标记 - currentJob = null // 置空Job - Log.d("CountDownUtil", "倒计时正常结束") - onFinish.invoke() + isCounting.set(false) + currentJob = null + // cause == null 为正常结束,有异常(含 CancellationException)时不触发 onFinish + if (cause == null) { + Log.d("CountDownUtil", "倒计时正常结束") + onFinish() + } else { + Log.d("CountDownUtil", "倒计时被取消:${cause.message}") + } } } .catch { e -> - // 捕获异常:避免崩溃,重置状态 Log.e("CountDownUtil", "倒计时异常:${e.message}", e) isCounting.set(false) currentJob = null @@ -84,12 +68,12 @@ object CountDownUtil { } /** - * 取消倒计时(手动停止) + * 取消倒计时(不触发 onFinish) */ fun cancelCountDown() { - currentJob?.cancel() // 取消Job - currentJob = null // 置空,避免残留 - isCounting.set(false) // 重置标记 - Log.d("CountDownUtil", "倒计时被手动取消") + currentJob?.cancel() + currentJob = null + isCounting.set(false) + Log.d("CountDownUtil", "倒计时已取消") } -} \ No newline at end of file +}