添加了界面及逻辑

This commit is contained in:
zxj
2025-07-22 15:48:12 +08:00
parent 9d14b5499b
commit 5ad82eb224
40 changed files with 2157 additions and 182 deletions
@@ -2,12 +2,16 @@ package com.sw.platecabinet.activity
import android.app.Dialog
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.view.View
import android.view.WindowInsetsController
import android.view.WindowManager
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.viewbinding.ViewBinding
import com.sw.inbound.utils.DateTimeUtils
import com.sw.platecabinet.R
@@ -19,7 +23,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import timber.log.Timber
/**
* activity 基类
@@ -30,54 +33,123 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
protected lateinit var context: Context
private var timeJob: Job? = null
private var mDialogWaiting: CustomDialog? = null
private val permissionHelpers = mutableMapOf<Int, PermissionHelper>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
context = this
disableSystemUICompletely()
// 确保内容延伸到导航栏区域
WindowCompat.setDecorFitsSystemWindows(window, false)
binding = inflateViewBinding()
headerBinding = inflateTitleBinding()
setContentView(binding.root)
updateTime()
registerDataChange()
initialize()
}
fun updateTime() {
headerBinding?.let {
headerBinding!!.tvRightTime.setClickListeners(
it.tvLeftDate.setClickListeners(
onDoubleClick = {
MainActivity.start(context, pageType = PageType.SETTING_LIST)
onLeftDoubleClick()
}
)
it.tvRightTime.setClickListeners(
onDoubleClick = {
onRightDoubleClick()
}
)
val scope = CoroutineScope(Dispatchers.Main)
timeJob = scope.launch {
DateTimeUtils.realTimeChineseDateFlow()
.collect { (date, time) ->
headerBinding!!.tvLeftDate.text = date
headerBinding!!.tvRightTime.text = time
it.tvLeftDate.text = date
it.tvRightTime.text = time
}
}
}
}
open fun onLeftDoubleClick() {
}
open fun onRightDoubleClick() {
MainActivity.start(context, pageType = PageType.SETTING_LIST)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// 统一处理权限拒绝情况
val helper = permissionHelpers[requestCode]
val allGranted = PermissionHelper.handlePermissionResult(
this,
requestCode,
permissions,
grantResults
) { permanentlyDeniedPermissions ->
// 权限被永久拒绝的统一处理
Timber.e("用户永久拒绝权限 $permanentlyDeniedPermissions")
grantResults,
permissionHelper = helper
)
if (allGranted) {
permissionHelpers.remove(requestCode)
}
}
/**
* 注册PermissionHelper以便处理结果
*/
fun registerPermissionHelper(helper: PermissionHelper) {
permissionHelpers[helper.requestCode] = helper
}
override fun onResume() {
super.onResume()
// 关键点2:防止某些场景下系统栏恢复
enforceImmersiveMode()
}
private fun disableSystemUICompletely() {
// 禁用系统手势(Android 10+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.systemBarsBehavior =
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
// 全屏+隐藏导航栏(所有版本通用)
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
)
// 禁止窗口扩展至系统栏区域(彻底锁定)
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
}
private fun enforceImmersiveMode() {
// 持续强制隐藏系统栏(防止手势触发)
window.decorView.postDelayed({
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
)
}, 100) // 延迟100ms确保覆盖手势触发
}
/**
* 显示等待提示框
*/
@@ -105,6 +177,8 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
protected abstract fun initialize()
protected open fun registerDataChange() {}
override fun onDestroy() {
timeJob?.cancel()
super.onDestroy()
@@ -2,30 +2,53 @@ package com.sw.platecabinet.activity
import android.Manifest
import android.content.Intent
import android.graphics.Outline
import android.graphics.Point
import android.hardware.Camera
import android.os.Build
import android.util.DisplayMetrics
import android.view.View
import android.view.ViewOutlineProvider
import android.view.ViewGroup
import android.view.ViewTreeObserver
import androidx.activity.viewModels
import androidx.camera.core.CameraSelector
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import com.google.common.util.concurrent.ListenableFuture
import androidx.annotation.RequiresApi
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import com.arcsoft.face.ErrorInfo
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.ConfigUtil
import com.sw.plate.utils.arcface.ErrorCodeUtil
import com.sw.plate.utils.arcface.FaceRectTransformer
import com.sw.plate.utils.arcface.FaceRectView
import com.sw.plate.utils.arcface.FaceRectView.DrawInfo
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.model.FacePreviewInfo
import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import com.sw.platecabinet.R
import com.sw.platecabinet.databinding.ActivityLoginFaceBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.utils.PermissionHelper
import com.sw.platecabinet.viewmodel.UserViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
/**
* 人脸识别
*/
class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>() {
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>(),
ViewTreeObserver.OnGlobalLayoutListener {
private val viewModel by viewModels<UserViewModel>()
private val recognizeViewModel by viewModels<RecognizeViewModel>()
private val CAMERA_PERMISSION_REQUEST_CODE = 100
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
private val REQUIRED_PERMISSIONS: Array<String> = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
override fun inflateViewBinding(): ActivityLoginFaceBinding {
return ActivityLoginFaceBinding.inflate(layoutInflater)
@@ -38,7 +61,10 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>() {
override fun initialize() {
// 请求权限
checkCameraPermission()
initView()
initArcViewModel()
initArcView()
openRectInfoDraw = true
recognizeViewModel.setDrawRectInfoTextValue(true)
viewModel.generateToken()
binding.llToPwd.setOnClickListener {
val intent = Intent(this, LoginByPwdActivity::class.java)
@@ -46,53 +72,314 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>() {
}
}
private fun checkCameraPermission() {
PermissionHelper.with(this, REQUIRED_PERMISSIONS, CAMERA_PERMISSION_REQUEST_CODE)
.onGranted {
// 权限已授予
startCamera()
override fun registerDataChange() {
lifecycleScope.launch {
viewModel.currentUserInfo.collect {
Timber.d("currentUserInfo it = $it")
if (it != null) {
ToastUtils.showToast("登录成功")
MainActivity.start(context, pageType = PageType.PLATE_CABINET_FULL)
finish()
}
}
.checkAndRequest()
}
private fun initView() {
binding.previewView.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
// outline.setRoundRect(0, 0, view.width, view.height, 12f.dp)
outline.setOval(0, 0, view.width, view.height)
}
lifecycleScope.launch {
viewModel.showLoading.collect {
if (it) {
showWaitingDialog("")
} else {
hideWaitingDialog()
}
}
}
binding.previewView.clipToOutline = true
}
private fun startCamera() {
Timber.d("startCamera")
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
// 创建预览用例
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(binding.previewView.surfaceProvider)
private fun checkCameraPermission() {
val permissionHelper =
PermissionHelper.with(this, REQUIRED_PERMISSIONS, CAMERA_PERMISSION_REQUEST_CODE)
.onGranted {
// 权限已授予
recognizeViewModel.init()
initRgbCamera()
resumeCamera()
}
.build()
registerPermissionHelper(permissionHelper)
permissionHelper.checkAndRequest()
}
// 选择后置摄像头
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
private var isRecognition = false
private var rgbCameraHelper: DualCameraHelper? = null
private var rgbFaceRectTransformer: FaceRectTransformer? = null
private val livenessType = LivenessType.RGB
private var openRectInfoDraw = false
try {
// 解绑所有用例
cameraProvider.unbindAll()
private fun initArcViewModel() {
// 绑定到生命周期
cameraProvider.bindToLifecycle(
this, cameraSelector, preview
recognizeViewModel.setLiveType(livenessType)
recognizeViewModel.ftInitCode.observe(this, Observer { ftInitCode: Int? ->
if (ftInitCode != ErrorInfo.MOK) {
val error: String? = getString(
R.string.specific_engine_init_failed, "ftEngine",
ftInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(ftInitCode!!)
)
} catch (exc: Exception) {
Timber.e(exc, "Use case binding failed")
Timber.e("ftInitCode observe = $error")
ToastUtils.showToast(error)
}
}, ContextCompat.getMainExecutor(this))
})
recognizeViewModel.frInitCode.observe(this, Observer { frInitCode: Int? ->
if (frInitCode != ErrorInfo.MOK) {
val error: String? = getString(
R.string.specific_engine_init_failed, "frEngine",
frInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(frInitCode!!)
)
Timber.e("frInitCode observe = $error")
ToastUtils.showToast(error)
}
})
recognizeViewModel.flInitCode.observe(this, Observer { flInitCode: Int? ->
if (flInitCode != ErrorInfo.MOK) {
val error: String? = getString(
R.string.specific_engine_init_failed, "flEngine",
flInitCode, ErrorCodeUtil.arcFaceErrorCodeToFieldName(flInitCode!!)
)
Timber.e("flInitCode observe = $error")
ToastUtils.showToast(error)
}
})
recognizeViewModel.recognizeConfiguration
.observe(this, Observer { recognizeConfiguration: RecognizeConfiguration? ->
Timber.i("recognizeConfiguration: observe = ${recognizeConfiguration.toString()}")
})
recognizeViewModel.recognizeNotice.observe(this, Observer { notice: String? ->
Timber.i("recognizeNotice observe notice = $notice")
})
recognizeViewModel.recognizeUserId.observe(this, Observer { userId: String? ->
Timber.i("recognizeUserId observe userId = $userId")
viewModel.getUserInfoById(memberId = userId?.toInt() ?: 0)
})
}
private fun initArcView() {
//在布局结束后才做初始化操作
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().addOnGlobalLayoutListener(this)
recognizeViewModel.getCompareResultList().getValue()
}
override fun onDestroy() {
if (rgbCameraHelper != null) {
rgbCameraHelper!!.release()
rgbCameraHelper = null
}
recognizeViewModel.destroy()
super.onDestroy()
}
/**
* 调整View的宽高,使2个预览同时显示
*
* @param previewView 显示预览数据的view
* @param faceRectView 画框的view
* @param previewSize 预览大小
* @param displayOrientation 相机旋转角度
* @return 调整后的LayoutParams
*/
private fun adjustPreviewViewSize(
rgbPreview: View,
previewView: View,
faceRectView: FaceRectView,
previewSize: Camera.Size,
displayOrientation: Int,
scale: Float
): ViewGroup.LayoutParams {
val layoutParams = previewView.layoutParams
val measuredWidth = previewView.measuredWidth
val measuredHeight = previewView.measuredHeight
var ratio = (previewSize.height.toFloat()) / previewSize.width.toFloat()
if (ratio > 1) {
ratio = 1 / ratio
}
if (displayOrientation % 180 == 0) {
layoutParams.width = measuredWidth
layoutParams.height = (measuredWidth * ratio).toInt()
} else {
layoutParams.height = measuredHeight
layoutParams.width = (measuredHeight * ratio).toInt()
}
if (scale < 1f) {
val rgbParam = rgbPreview.getLayoutParams()
layoutParams.width = (rgbParam.width * scale).toInt()
layoutParams.height = (rgbParam.height * scale).toInt()
} else {
layoutParams.width = (layoutParams.width * scale).toInt()
layoutParams.height = (layoutParams.height * scale).toInt()
}
val metrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(metrics)
if (layoutParams.width >= metrics.widthPixels) {
val viewRatio = layoutParams.width / (metrics.widthPixels.toFloat())
layoutParams.width = (layoutParams.width / viewRatio).toInt()
layoutParams.height = (layoutParams.height / viewRatio).toInt()
}
if (layoutParams.height >= metrics.heightPixels) {
val viewRatio = layoutParams.height / (metrics.heightPixels.toFloat())
layoutParams.width = (layoutParams.width / viewRatio).toInt()
layoutParams.height = (layoutParams.height / viewRatio).toInt()
}
previewView.setLayoutParams(layoutParams)
faceRectView.setLayoutParams(layoutParams)
return layoutParams
}
private fun initRgbCamera() {
val cameraListener: CameraListener = object : CameraListener {
override fun onCameraOpened(
camera: Camera,
cameraId: Int,
displayOrientation: Int,
isMirror: Boolean
) {
runOnUiThread({
val previewSizeRgb = camera.getParameters().getPreviewSize()
val layoutParams = adjustPreviewViewSize(
binding.dualCameraTexturePreviewRgb,
binding.dualCameraTexturePreviewRgb, binding.dualCameraFaceRectView,
previewSizeRgb, displayOrientation, 1.0f
)
rgbFaceRectTransformer = FaceRectTransformer(
previewSizeRgb.width,
previewSizeRgb.height,
layoutParams.width,
layoutParams.height,
displayOrientation,
cameraId,
isMirror,
ConfigUtil.isDrawRgbRectHorizontalMirror(context),
ConfigUtil.isDrawRgbRectVerticalMirror(context)
)
recognizeViewModel.onRgbCameraOpened(camera)
recognizeViewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer)
})
}
@RequiresApi(api = Build.VERSION_CODES.Q)
override fun onPreview(nv21: ByteArray?, camera: Camera?) {
if (!isRecognition) {
return
}
binding.dualCameraFaceRectView.clearFaceInfo()
val facePreviewInfoList: MutableList<FacePreviewInfo?>? =
recognizeViewModel.onPreviewFrame(nv21, true)
if (facePreviewInfoList != null && rgbFaceRectTransformer != null) {
drawPreviewInfo(facePreviewInfoList)
}
recognizeViewModel.clearLeftFace(facePreviewInfoList)
}
override fun onCameraClosed() {
Timber.i("onCameraClosed: ")
}
override fun onCameraError(e: java.lang.Exception) {
Timber.i("onCameraError: %s", e.message)
e.printStackTrace()
}
override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
Timber.i("onCameraConfigurationChanged: threadName = ${Thread.currentThread().name}")
if (rgbFaceRectTransformer != null) {
rgbFaceRectTransformer!!.cameraDisplayOrientation = displayOrientation
}
Timber.i("onCameraConfigurationChanged: $cameraID $displayOrientation")
}
}
val previewConfig: PreviewConfig = recognizeViewModel.previewConfig
rgbCameraHelper = DualCameraHelper.Builder()
.previewViewSize(
Point(
binding.dualCameraTexturePreviewRgb.measuredWidth,
binding.dualCameraTexturePreviewRgb.measuredHeight
)
)
.rotation(windowManager.defaultDisplay.rotation)
.additionalRotation(90) // 角度
.previewSize(recognizeViewModel.loadPreviewSize())
.specificCameraId(previewConfig.rgbCameraId)
.isMirror(true)
.previewOn(binding.dualCameraTexturePreviewRgb)
.cameraListener(cameraListener)
.build()
rgbCameraHelper!!.init()
rgbCameraHelper!!.start()
}
/**
* 绘制RGB、IR画面的实时人脸信息
*
* @param facePreviewInfoList RGB画面的实时人脸信息
*/
private fun drawPreviewInfo(facePreviewInfoList: MutableList<FacePreviewInfo?>) {
if (rgbFaceRectTransformer != null) {
val rgbDrawInfoList: MutableList<DrawInfo?>? = recognizeViewModel.getDrawInfo(
facePreviewInfoList,
LivenessType.RGB,
openRectInfoDraw
)
// 识别成功
binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList)
}
}
fun openRectInfoDraw(view: View?) {
openRectInfoDraw = !openRectInfoDraw
recognizeViewModel.setDrawRectInfoTextValue(openRectInfoDraw)
}
override fun onGlobalLayout() {
binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(this)
recognizeViewModel.init()
initRgbCamera()
resumeCamera()
// countDownTimer.start();
}
// @Override
// protected void onResume() {
// super.onResume();
// resumeCamera();
// }
private fun resumeCamera() {
isRecognition = true
if (rgbCameraHelper != null && rgbCameraHelper!!.isStopped) {
rgbCameraHelper!!.start()
}
}
protected override fun onPause() {
pauseCamera()
super.onPause()
}
private fun pauseCamera() {
isRecognition = false
recognizeViewModel.onPreviewFrame(ByteArray(1382400), true)
}
}
@@ -1,10 +1,18 @@
package com.sw.platecabinet.activity
import android.content.Intent
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.databinding.ActivityLoginByPwdBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.viewmodel.UserViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() {
private val viewModel by viewModels<UserViewModel>()
override fun inflateViewBinding(): ActivityLoginByPwdBinding {
return ActivityLoginByPwdBinding.inflate(layoutInflater)
}
@@ -15,7 +23,13 @@ class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() {
override fun initialize() {
binding.btnLogin.setOnClickListener {
MainActivity.start(context, pageType = PageType.PLATE_CABINET_FULL)
val phone = binding.etPhone.text.toString()
val pwd = binding.etPwd.text.toString()
if (phone.isEmpty() || pwd.isEmpty()) {
ToastUtils.showToast("手机或校验码不能为空")
return@setOnClickListener
}
viewModel.loginWithPwd(phone = phone, password = pwd)
}
binding.tvFaceRec.setOnClickListener {
val intent = Intent(this, LoginByFaceActivity::class.java)
@@ -23,4 +37,26 @@ class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() {
}
}
override fun registerDataChange() {
lifecycleScope.launch {
viewModel.currentUserInfo.collect {
Timber.d("currentUserInfo it = $it")
if (it != null) {
ToastUtils.showToast("登录成功")
MainActivity.start(context, pageType = PageType.PLATE_CABINET_FULL)
finish()
}
}
}
lifecycleScope.launch {
viewModel.showLoading.collect {
if (it) {
showWaitingDialog("")
} else {
hideWaitingDialog()
}
}
}
}
}
@@ -15,6 +15,7 @@ import com.sw.platecabinet.utils.FragmentHelper
class MainActivity : BaseActivity<ActivityMainBinding>() {
private lateinit var fragmentHelper: FragmentHelper
private var pageType = PageType.SETTING_LIST
override fun inflateViewBinding(): ActivityMainBinding {
return ActivityMainBinding.inflate(layoutInflater)
@@ -43,7 +44,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
override fun initialize() {
val param = intent.getStringExtra(PARAM_PAGE_TYPE)
val equipmentUserInfo = intent.getParcelableExtra<EquipmentUserInfo>(PARAM_EQUIPMENT_INFO)
val pageType = param?.let { PageType.valueOf(param) } ?: PageType.SETTING_LIST
pageType = param?.let { PageType.valueOf(param) } ?: PageType.SETTING_LIST
fragmentHelper = FragmentHelper(supportFragmentManager, R.id.fragment_container)
val page = when (pageType) {
PageType.SETTING_LIST -> SettingListFragment()
@@ -54,6 +55,16 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
}
fragmentHelper.addFragment(page)
}
override fun onLeftDoubleClick() {
if (pageType == PageType.SETTING_LIST) {
finish()
}
}
override fun onRightDoubleClick() {
}
}
/**