From cc88dccaa69ac3d195917f4722d89f7211a209d4 Mon Sep 17 00:00:00 2001 From: lvmeng <848755140@qq.com> Date: Sat, 28 Feb 2026 10:03:45 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=86=E5=88=AB=E6=88=90=E5=8A=9F=E6=9C=AA?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E5=8F=8A=E9=87=87=E9=9B=86=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E9=95=BF=E9=97=AE=E9=A2=98=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/LoginByFaceActivity.kt | 383 ++++++------------ .../com/sw/platecabinet/ext/TextViewExt.kt | 31 +- .../sw/platecabinet/utils/CountDownManager.kt | 135 ++++++ .../sw/platecabinet/utils/CountDownUtil.kt | 95 +++++ .../sw/plate/utils/arcface/FaceRectView.java | 4 + 5 files changed, 386 insertions(+), 262 deletions(-) create mode 100644 app/src/main/java/com/sw/platecabinet/utils/CountDownManager.kt create mode 100644 app/src/main/java/com/sw/platecabinet/utils/CountDownUtil.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 6b0473e..61ef886 100644 --- a/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt +++ b/app/src/main/java/com/sw/platecabinet/activity/LoginByFaceActivity.kt @@ -6,13 +6,14 @@ import android.graphics.Bitmap import android.graphics.Point import android.hardware.Camera import android.os.Build -import android.os.CountDownTimer import android.util.DisplayMetrics import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver import androidx.annotation.RequiresApi +import androidx.core.view.isVisible import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope import com.arcsoft.face.ErrorInfo import com.google.gson.Gson import com.sw.inbound.utils.DateTimeUtils @@ -27,6 +28,7 @@ import com.sw.plate.utils.arcface.PreviewConfig import com.sw.plate.utils.arcface.camera.CameraListener import com.sw.plate.utils.arcface.camera.DualCameraHelper import com.sw.plate.utils.arcface.face.constants.LivenessType +import com.sw.plate.utils.arcface.face.constants.RecognizeColor import com.sw.plate.utils.arcface.face.model.CompareResult import com.sw.plate.utils.arcface.face.model.FacePreviewInfo import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration @@ -45,6 +47,8 @@ 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 import com.sw.platecabinet.utils.PermissionHelper @@ -57,6 +61,7 @@ import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import timber.log.Timber import java.io.File +import java.util.Arrays import java.util.concurrent.atomic.AtomicBoolean /** @@ -64,8 +69,6 @@ import java.util.concurrent.atomic.AtomicBoolean */ class LoginByFaceActivity : BaseActivity(), ViewTreeObserver.OnGlobalLayoutListener { - // private val recognizeViewModel by viewModels() - private var countDownTimer: CountDownTimer? = null private val CAMERA_PERMISSION_REQUEST_CODE = 100 private val REQUIRED_PERMISSIONS: Array = arrayOf( @@ -101,47 +104,14 @@ class LoginByFaceActivity : BaseActivity(), override fun initialize() { instance = this userViewModel.activeEngine() -// addSocketListener() - initCountTime() + initArcViewModel() initArcView() openRectInfoDraw = true recognizeViewModel.setDrawRectInfoTextValue(true) - //viewModel.generateToken() - -// binding.llToPwd.setOnClickListener { -// val intent = Intent(this, LoginByPwdActivity::class.java) -// startActivity(intent) -// } - -// TaskManager.startTask() EventBus.getDefault().register(this) -// binding.btnCollectFace.setOnClickListener { collectFace() } - - -// binding.rvLogInfo.let { -// it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true) -// it.adapter = logAdapter -// } -// -// binding.btnLogState.let { -// it.tag = false -// it.setImageResource(R.drawable.ic_log_show) -// it.setOnClickListener { v -> -// binding.rvLogInfo.run { -// val state = it.tag.toString().toBooleanStrictOrNull() ?: false -// if (state) visible() else gone() -// } -// val state = it.tag.toString().toBooleanStrictOrNull() ?: false -// it.tag = state -// it.setImageResource( -// if (state) R.drawable.ic_log_hide else R.drawable.ic_log_show -// ) -// } -// } - //开启人脸增量数据定时任务 startFaceTask { loadLogInfo("collectFace增量接口数据用户id:${Gson().toJson(it)}") @@ -159,6 +129,7 @@ class LoginByFaceActivity : BaseActivity(), //采集数据按钮 retryFailCount = 0 isCollectFace = true + isCollecting.set(true) collectFace() } @@ -202,6 +173,8 @@ class LoginByFaceActivity : BaseActivity(), private val livenessType = LivenessType.RGB private var openRectInfoDraw = false private val regDebouncer = Debouncer(3000) + private val leftDebouncer = Debouncer(30*1000) + private var registerHandler: RegisterCallbackHandler? = null private fun isFitData(userFaceInfo: UserFaceInfo?): Boolean { @@ -220,7 +193,7 @@ class LoginByFaceActivity : BaseActivity(), recognizeViewModel.prepareRegister() recognizeViewModel.setOnRegisterFinishedCallback { facePreviewInfo, userFaceInfo -> registerHandler?.setCallback { - if (!isCollectFace) { + if (!isCollectFace || !isCollecting.get()) { return@setCallback } if (this.userFaceInfo == null && isFitData(userFaceInfo)) { @@ -312,7 +285,6 @@ class LoginByFaceActivity : BaseActivity(), */ private val faceSuccessThreshold by lazy { ConfigUtil.getRecognizeThreshold(this) } - private val openPlateDebouncer = Debouncer(5000) private val isCollecting = AtomicBoolean(false) @@ -326,29 +298,11 @@ class LoginByFaceActivity : BaseActivity(), ) if (similar >= faceSuccessThreshold) { val userId = result.faceEntity?.userName - if (userId.isNullOrBlank()) { - loadLogInfo("collectFace,recognizeUserId 识别成功但 userId 为空,忽略") - return - } - - loadLogInfo( - "collectFace,recognizeUserId 识别成功 userId=$userId, time=${DateTimeUtils.getDateTimeString()}" - ) - - // 成功后必须重置状态 - retryFailCount = 0 - isCollecting.set(false) - isCollectFace = false - currentUserId = userId - - // 防抖,防止重复开闸 - openPlateDebouncer.debounce { - openPlate(userId) - } + recognizeSuccess(userId) return } - if (isCollectFace) { + if (isCollectFace || isCollecting.get()) { loadLogInfo("collectFace,recognizeUserId 正在采集中,忽略失败结果 similar=$similar") return } @@ -372,8 +326,30 @@ class LoginByFaceActivity : BaseActivity(), loadLogInfo("collectFace,recognizeUserId 中间态重试,retryFailCount=$retryFailCount") } + private fun recognizeSuccess(userId: String?) { + if (userId.isNullOrBlank()) { + loadLogInfo("collectFace,recognizeUserId 识别成功但 userId 为空,忽略") + return + } + + loadLogInfo( + "collectFace,recognizeUserId 识别成功 userId=$userId, time=${DateTimeUtils.getDateTimeString()}" + ) + + // 成功后必须重置状态 + retryFailCount = 0 + isCollecting.set(false) + isCollectFace = false + currentUserId = userId + + // 防抖,防止重复开闸 + openPlateDebouncer.debounce { + openPlate(userId) + } + } + private fun recognizeFail() { - countDownTimer?.cancel() + CountDownManager.cancelCountDown() binding.tvToInit.text = "" binding.tvSimilarValue.text = "" binding.llCollectButton.visible() @@ -381,103 +357,9 @@ class LoginByFaceActivity : BaseActivity(), binding.btnCollect.run { visible() text = "新用户采集" -// if (tag == "1") { -// binding.btnRetry.gone() -// text = "重新采集" -// tag = "1" -// } else { -// binding.btnRetry.visible() -// "新用户采集" -// tag = "1" -// } } } - /** - * 根据相似度similar>设定阈值0.8作为识别成功判断, - * 如果similar<0.3作为识别失败判断, - * 如果similar>=0.3&&similar<=0.8时,暂时不作为失败情况,再识别5次如果similar<0.8则作为识别失败处理 - */ - private fun loadFaceRecognizeResult2(result: CompareResult?) { - // 无有效结果直接忽略 - val similar = result?.similar ?: return - - binding.tvSimilarValue.text = "$similar,${retryFailCount}次" - loadLogInfo( - "collectFace,recognizeUserId 阈值 success=$faceSuccessThreshold, " + - "fail=$faceFailThreshold, similar=$similar, retry=$retryFailCount" - ) - - if (similar >= faceSuccessThreshold) { - val userId = result.faceEntity?.userName - if (userId.isNullOrBlank()) { - loadLogInfo("collectFace,recognizeUserId 识别成功但 userId 为空,忽略") - return - } - - loadLogInfo( - "collectFace,recognizeUserId 识别成功 userId=$userId, time=${DateTimeUtils.getDateTimeString()}" - ) - - // 成功后必须重置状态 - retryFailCount = 0 - isCollecting.set(false) - isCollectFace = false - currentUserId = userId - - // 防抖,防止重复开闸 - openPlateDebouncer.debounce { - openPlate(userId) - } - return - } - - if (isCollectFace) { - loadLogInfo("collectFace,recognizeUserId 正在采集中,忽略失败结果 similar=$similar") - return - } - - if (similar <= faceFailThreshold || retryFailCount >= maxFailCount) { - loadLogInfo( - "collectFace,recognizeUserId 识别失败,开始采集 similar=$similar, " + - "retry=$retryFailCount, time=${DateTimeUtils.getDateTimeString()}" - ) - if (!isCollecting.compareAndSet(false, true)) { - loadLogInfo("collectFace 已在采集中,忽略重复触发") - return - } - - retryFailCount = 0 - isCollectFace = true - collectFace()//采集人脸 - return - } - - retryFailCount++ - loadLogInfo("collectFace,recognizeUserId 中间态重试,retryFailCount=$retryFailCount") - } - - /** - * 根据相似度similar>设定阈值0.8作为识别是否成功来加载人脸识别结果 - */ - private fun loadFaceRecognizeResult(result: CompareResult?) { - val similar = result?.similar ?: 0f - binding.tvSimilarValue.text = "$similar" - val similarPass = result?.isSimilarPass ?: false - if (similarPass.not()) { - loadLogInfo("collectFace,recognizeUserId 未识别到人脸信息,开始采集") - isCollectFace = true - collectFace() - return - } - currentUserId = result?.faceEntity?.userName - if (tempUserId != currentUserId) { - SoundPoolUtil.getInstance().play("success", 0) - } -// plateEnable = true - Debouncer(5000).debounce { openPlate(currentUserId) } - } - /** * 查询餐盘数据 */ @@ -524,7 +406,7 @@ class LoginByFaceActivity : BaseActivity(), PlateUtils.open(this, item.equipmentBoxCode, false) { goInitActivity() } - countDownTimer?.cancel() + CountDownManager.cancelCountDown() } currentPlateInfo = item MyApp.plateList = items @@ -679,6 +561,10 @@ class LoginByFaceActivity : BaseActivity(), binding.btnCollect.tag = "" binding.tvSimilarValue.text = "" retryFailCount = 0 + + if (binding.llCollectButton.isVisible) { + leftDebouncer.debounce { leftCountDown() } + } } recognizeViewModel.clearLeftFace(facePreviewInfoList) } @@ -750,15 +636,45 @@ class LoginByFaceActivity : BaseActivity(), } private fun resumeCamera() { - isRecognition = true +// isRecognition = true if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) { loadLogInfo("collectFace,resumeCamera: ") + isRecognition = true rgbCameraHelper!!.start() } else { - recognizeViewModel.onPreviewFrame(ByteArray(1382400), true) + //recognizeViewModel.onPreviewFrame(ByteArray(1382400), true) + + recognizeViewModel.onPreviewFrame(emptyFrame, true) + binding.dualCameraFaceRectView.clearFaceInfo() + recognizeViewModel.clearLeftFace(facePreviewInfoList) + binding.root.postDelayed({ + isRecognition = true + }, 500) } } + /** + * 构造符合分辨率的 NV21 格式全黑帧 + * @param width 图像宽度(如 1920) + * @param height 图像高度(如 1080) + * @return 合法的全黑帧字节数组 + */ + fun createBlackNV21Frame(width: Int, height: Int): ByteArray { + val frameSize = width * height + val nv21Data = ByteArray(frameSize * 3 / 2) // NV21 格式长度 = 宽×高×1.5 + + // Y 分量(亮度)设为 0(全黑),UV 分量设为 128(默认值) + // 1. 填充 Y 分量(前 frameSize 个字节) + Arrays.fill(nv21Data, 0, frameSize, 0.toByte()) + // 2. 填充 UV 分量(后 frameSize/2 个字节) + Arrays.fill(nv21Data, frameSize, nv21Data.size, 128.toByte()) + + return nv21Data + } + + // 调用示例:构造 1920×1080 的全黑帧(长度 1382400) + val emptyFrame = createBlackNV21Frame(1920, 1080) + // 传递“空帧” recognizeViewModel.onPreviewFrame(emptyFrame, true) /** * 临时用户id @@ -784,6 +700,7 @@ class LoginByFaceActivity : BaseActivity(), * 重置识别状态 */ private fun resetRecognizeState() { + binding.tvToInit.text = "15" binding.llCollectButton.gone() retryFailCount = 0 binding.tvSimilarValue.text = "" @@ -794,10 +711,26 @@ class LoginByFaceActivity : BaseActivity(), tempUserId = "${System.currentTimeMillis()}" resumeCamera() registerHandler = RegisterCallbackHandler() - countDownTimer?.let { - it.cancel() - it.start() - } + + CountDownManager.startCountDown( + totalSeconds = 15, + onTick = { remaining -> + loadLogInfo("resetRecognizeState剩余时间:$remaining 秒") + if (judgeRecognizeState()) return@startCountDown + loadLogInfo("initCountTime onTick = $remaining") + binding.tvToInit.text = "$remaining" + }, + onFinish = { + loadLogInfo("resetRecognizeState倒计时结束") + if (judgeRecognizeState()) return@startCountDown + if (facePreviewInfoList.isNullOrEmpty()) { + goInitActivity() + } else { + loadLogInfo("recognizeFail---initCountTime") + recognizeFail() + } + } + ) } private var isFirstOpen = true @@ -805,12 +738,12 @@ class LoginByFaceActivity : BaseActivity(), protected override fun onPause() { pauseCamera() super.onPause() - countDownTimer?.cancel() + CountDownManager.cancelCountDown() } private fun pauseCamera() { isRecognition = false - recognizeViewModel.onPreviewFrame(ByteArray(1382400), true) +// recognizeViewModel.onPreviewFrame(emptyFrame, true) } override fun handleLoginSuccess(equipmentUserInfo: EquipmentUserInfo, isAdmin: Boolean) { @@ -818,22 +751,17 @@ class LoginByFaceActivity : BaseActivity(), // goInitActivity() } - private val totalTimeInMillis = 15 * 1000L - fun initCountTime() { - countDownTimer = object : CountDownTimer(totalTimeInMillis, 1000) { - override fun onTick(millisUntilFinished: Long) { - loadLogInfo("initCountTime onTick = ${(millisUntilFinished / 1000).toInt()}") - binding.tvToInit.text = "${(millisUntilFinished / 1000).toInt() + 1}" - } - - override fun onFinish() { - if (facePreviewInfoList.isNullOrEmpty()) { - goInitActivity() - } else { - recognizeFail() - } + private fun judgeRecognizeState(): Boolean { + val rectColor = binding.dualCameraFaceRectView.rectColor + if (rectColor == RecognizeColor.COLOR_SUCCESS && currentUserId.isNullOrBlank().not() && facePreviewInfoList.isNullOrEmpty().not()) { + //人未离开,再次识别 + loadLogInfo("userId=$currentUserId") + openPlateDebouncer.debounce { + openPlate(currentUserId) } + return true } + return false } fun goInitActivity() { @@ -874,12 +802,6 @@ class LoginByFaceActivity : BaseActivity(), uploadFaceDebouncer.debounce { addUserFace(faceData, imageFile) } -// // 给某个客户端发送 -// val jsonObject = JSONObject().also { -// it.put("type", "faceFeature") -// it.put("content", faceData) -// } -// tcpClient?.send(jsonObject) } private fun addUserFace(faceData: String, imageFile: File) { @@ -950,7 +872,7 @@ class LoginByFaceActivity : BaseActivity(), loadLogInfo("collectFace,saveFaceInfo: ----------10,人脸数据保存完成-----------") recognizeViewModel.refreshFaceList() loadLogInfo("collectFace,saveFaceInfo: ----------11,刷新人脸数据-----------") - runOnUiThread { recognizeViewModel.onPreviewFrame(ByteArray(1382400), true) } + runOnUiThread { recognizeViewModel.onPreviewFrame(emptyFrame, true) } loadLogInfo("---------------------------------") collectCount = 0 } catch (e: Exception) { @@ -964,78 +886,25 @@ class LoginByFaceActivity : BaseActivity(), private var collectCount = 0 -// private var tcpClient: TcpClient? = null -// -// private fun addSocketListener() { -//// val ipAddress = NetworkUtils.getIPAddress(true) -//// loadLogInfo( "addSocketListener,ipAddress: $ipAddress"); -// tcpClient = TcpClient( -// "192.168.1.95", -// 5000, -// GlobalData.deviceId, // clientId -// 5000, // connectTimeoutMs -// 10 * 1000, // heartbeatIntervalMs -// 30 * 1000 // heartbeatTimeoutMs -// ) -// -// tcpClient?.setListener(object : TcpClientListenerImpl() { -// override fun onSendSuccess(json: JSONObject?) { -// super.onSendSuccess(json) -// loadLogInfo("collectFace,TcpClientListener发送成功:$json") -//// toast("发送成功") -// } -// -// override fun onSendFailed(json: JSONObject?, e: java.lang.Exception?) { -// super.onSendFailed(json, e) -// ToastUtils.showToast("发送失败${e?.message}") -// loadLogInfo("collectFace,TcpClientListener人脸数据发送失败:${e?.message}") -// } -// -// override fun onMessage(json: JSONObject?) { -// super.onMessage(json) -// loadLogInfo("collectFace,TcpClientListener收到消息:$json") -// val type = json?.getInt("type") -// // 根据服务端消息类型处理删除新增临时用户人脸数据--------------------- -// when (type) { -// LanServer.TYPE_ADD_FACE_DATA -> { -// //采集设备id自己生成,不接收服务端的,在本地只判断是否存在同一人脸数据 -// loadLogInfo("collectFace,TcpClientListener收到消息,新增用户:采集设备id自己生成,不接收服务端的,在本地只判断是否存在同一人脸数据") -// } -// -// LanServer.TYPE_CLEAR_FACE_DATA -> { -// clearFaceData() -// } -// -// else -> {} -// } -// } -// }) -// tcpClient?.start() -// -// try { -// LanServer.getInstance().let { -// it.setListener(object : LanServerListenerImpl() { -// override fun onMessageReceived(clientId: String?, message: JSONObject?) { -// super.onMessageReceived(clientId, message) -// ToastUtils.showToast("收到消息clientId=$clientId") -// } -// }) -// it.start() -// } -// } catch (e: Exception) { -// e.printStackTrace() -// } -// } - - private fun clearFaceData() { - Thread { - val faceDao = FaceDatabase.getInstance(this).faceDao() - faceDao.deleteTempUserFaceData() - recognizeViewModel.refreshFaceList() - loadLogInfo("collectFace,TcpClientListener收到消息,已删除并刷新人脸数据") - }.start() - } - private var isCollectFace = false - + private fun leftCountDown() { + CountDownUtil.startCountDown(total = 15, lifecycleScope = lifecycleScope, onStart = { + loadLogInfo("onPreview-----------leftCountDown-------------") + }, onTick = { + loadLogInfo("leftCountDown.onTick:$it") + if (facePreviewInfoList.isNullOrEmpty().not()) { + loadLogInfo("leftCountDown.onTick-cancel()") + CountDownUtil.cancelCountDown() + return@startCountDown + } + }, onFinish = { + if (facePreviewInfoList.isNullOrEmpty().not()) { + loadLogInfo("leftCountDown.onFinish-cancel()") + CountDownUtil.cancelCountDown() + return@startCountDown + } + CountDownUtil.cancelCountDown() + goInitActivity() + }) + } } \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/ext/TextViewExt.kt b/app/src/main/java/com/sw/platecabinet/ext/TextViewExt.kt index 6347da0..030c0ea 100644 --- a/app/src/main/java/com/sw/platecabinet/ext/TextViewExt.kt +++ b/app/src/main/java/com/sw/platecabinet/ext/TextViewExt.kt @@ -71,13 +71,34 @@ fun View.clickWithCoroutines( } } +//fun View.clickWithDebounce(delay: Long = 500, action: () -> Unit) { +// var job: Job? = null +// setOnClickListener { +// job?.cancel() +// job = CoroutineScope(Dispatchers.Main).launch { +// delay(delay) +// action() +// } +// } +//} + +/** + * 极简版防重复点击扩展函数 + * @param delay 防抖时间(默认300ms) + * @param action 点击执行逻辑 + */ fun View.clickWithDebounce(delay: Long = 500, action: () -> Unit) { - var job: Job? = null setOnClickListener { - job?.cancel() - job = CoroutineScope(Dispatchers.Main).launch { - delay(delay) - action() + // 用View的tag存储是否可点击的状态(默认可点击) + if (tag as? Boolean ?: true) { + tag = false // 标记为不可点击 + action() // 立即执行点击逻辑 + + // 启动协程,延迟后恢复可点击状态 + CoroutineScope(Dispatchers.Main).launch { + delay(delay) + tag = true // 恢复可点击 + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/sw/platecabinet/utils/CountDownManager.kt b/app/src/main/java/com/sw/platecabinet/utils/CountDownManager.kt new file mode 100644 index 0000000..67c3dce --- /dev/null +++ b/app/src/main/java/com/sw/platecabinet/utils/CountDownManager.kt @@ -0,0 +1,135 @@ +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 new file mode 100644 index 0000000..258ed94 --- /dev/null +++ b/app/src/main/java/com/sw/platecabinet/utils/CountDownUtil.kt @@ -0,0 +1,95 @@ +package com.sw.platecabinet.utils + +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 + +/** + * 人脸识别防抖倒计时工具类(单例+防重复+无死循环) + */ +object CountDownUtil { + // 原子布尔值:标记是否正在倒计时(防重复启动) + private val isCounting = AtomicBoolean(false) + + // 当前倒计时Job(用于取消) + private var currentJob: Job? = null + + /** + * 启动/重启防抖倒计时(同一时间仅一个倒计时运行) + * @param totalSeconds 倒计时总秒数(如15) + * @param lifecycleScope Activity/Fragment的lifecycleScope(必须) + * @param onTick 每秒回调剩余秒数(主线程) + * @param onFinish 倒计时结束回调(主线程) + */ + fun startCountDown( + total: Int = 15, + lifecycleScope: CoroutineScope, + onStart: () -> Unit = {}, + onTick: (Int) -> Unit = {}, + onFinish: () -> Unit + ) { + // 1. 防重复:如果正在倒计时,先取消旧任务 + if (isCounting.get()) { + //cancelCountDown() + return + } + + // 2. 标记为正在倒计时 + isCounting.set(true) + + // 3. 启动新倒计时 + currentJob = flow { + Log.d("CountDownUtil", "倒计时启动,总时长:$total 秒") + for (i in total downTo 1) { + emit(i) + delay(1000) // IO线程等待1秒,无死循环 + } + } + .flowOn(Dispatchers.IO) // 上游IO线程,避免阻塞主线程 + .onStart { + withContext(Dispatchers.Main) { + onStart.invoke() + } + } + .onEach { remaining -> + // 主线程回调剩余时间 + withContext(Dispatchers.Main) { + onTick.invoke(remaining) + } + } + .onCompletion { + // 倒计时结束:重置状态+回调 + withContext(Dispatchers.Main) { + isCounting.set(false) // 重置标记 + currentJob = null // 置空Job + Log.d("CountDownUtil", "倒计时正常结束") + onFinish.invoke() + } + } + .catch { e -> + // 捕获异常:避免崩溃,重置状态 + Log.e("CountDownUtil", "倒计时异常:${e.message}", e) + isCounting.set(false) + currentJob = null + } + .launchIn(lifecycleScope) + } + + /** + * 取消倒计时(手动停止) + */ + fun cancelCountDown() { + currentJob?.cancel() // 取消Job + currentJob = null // 置空,避免残留 + isCounting.set(false) // 重置标记 + Log.d("CountDownUtil", "倒计时被手动取消") + } +} \ No newline at end of file diff --git a/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceRectView.java b/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceRectView.java index 93eaf5f..15c37d3 100644 --- a/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceRectView.java +++ b/lib_face/src/main/java/com/sw/plate/utils/arcface/FaceRectView.java @@ -46,6 +46,10 @@ public class FaceRectView extends View { } } + public int getRectColor() { + return paint.getColor(); + } + public void clearFaceInfo() { drawInfoList.clear(); postInvalidate();