识别成功未提示及采集时间长问题优化
This commit is contained in:
@@ -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<ActivityLoginFaceBinding>(),
|
||||
ViewTreeObserver.OnGlobalLayoutListener {
|
||||
// private val recognizeViewModel by viewModels<RecognizeViewModel>()
|
||||
private var countDownTimer: CountDownTimer? = null
|
||||
|
||||
private val CAMERA_PERMISSION_REQUEST_CODE = 100
|
||||
private val REQUIRED_PERMISSIONS: Array<String> = arrayOf(
|
||||
@@ -101,47 +104,14 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
|
||||
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<ActivityLoginFaceBinding>(),
|
||||
//采集数据按钮
|
||||
retryFailCount = 0
|
||||
isCollectFace = true
|
||||
isCollecting.set(true)
|
||||
collectFace()
|
||||
}
|
||||
|
||||
@@ -202,6 +173,8 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
|
||||
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<ActivityLoginFaceBinding>(),
|
||||
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<ActivityLoginFaceBinding>(),
|
||||
*/
|
||||
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<ActivityLoginFaceBinding>(),
|
||||
)
|
||||
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<ActivityLoginFaceBinding>(),
|
||||
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<ActivityLoginFaceBinding>(),
|
||||
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<ActivityLoginFaceBinding>(),
|
||||
PlateUtils.open(this, item.equipmentBoxCode, false) {
|
||||
goInitActivity()
|
||||
}
|
||||
countDownTimer?.cancel()
|
||||
CountDownManager.cancelCountDown()
|
||||
}
|
||||
currentPlateInfo = item
|
||||
MyApp.plateList = items
|
||||
@@ -679,6 +561,10 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
|
||||
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<ActivityLoginFaceBinding>(),
|
||||
}
|
||||
|
||||
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<ActivityLoginFaceBinding>(),
|
||||
* 重置识别状态
|
||||
*/
|
||||
private fun resetRecognizeState() {
|
||||
binding.tvToInit.text = "15"
|
||||
binding.llCollectButton.gone()
|
||||
retryFailCount = 0
|
||||
binding.tvSimilarValue.text = ""
|
||||
@@ -794,10 +711,26 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
|
||||
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<ActivityLoginFaceBinding>(),
|
||||
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<ActivityLoginFaceBinding>(),
|
||||
// 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<ActivityLoginFaceBinding>(),
|
||||
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<ActivityLoginFaceBinding>(),
|
||||
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<ActivityLoginFaceBinding>(),
|
||||
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 // 恢复可点击
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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", "倒计时被手动取消")
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,10 @@ public class FaceRectView extends View {
|
||||
}
|
||||
}
|
||||
|
||||
public int getRectColor() {
|
||||
return paint.getColor();
|
||||
}
|
||||
|
||||
public void clearFaceInfo() {
|
||||
drawInfoList.clear();
|
||||
postInvalidate();
|
||||
|
||||
Reference in New Issue
Block a user