添加了副屏显示逻辑控制

This commit is contained in:
zxj
2025-08-01 15:57:12 +08:00
parent de4c5f1c75
commit c78279a66e
27 changed files with 817 additions and 191 deletions
-1
View File
@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
+2
View File
@@ -102,4 +102,6 @@ dependencies {
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.fragment.ktx)
// 图片加载
implementation(libs.glide)
}
@@ -2,11 +2,13 @@ package com.sw.dualscreen.activity
import android.content.Context
import android.graphics.Outline
import android.graphics.Typeface
import android.hardware.display.DisplayManager
import android.os.Bundle
import android.view.View
import android.view.ViewOutlineProvider
import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
@@ -17,27 +19,41 @@ import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.google.common.util.concurrent.ListenableFuture
import com.sw.dualscreen.R
import com.sw.dualscreen.adapter.GenericItemAdapter
import com.sw.dualscreen.adapter.GridSpacingItemDecoration
import com.sw.dualscreen.adapter.dpToPx
import com.sw.dualscreen.databinding.ActivityMainBinding
import com.sw.dualscreen.databinding.ItemFoodInfoBinding
import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.presentation.SecondaryScreenPresentation
import com.sw.dualscreen.sdk.SensorScaleUtils
import com.sw.dualscreen.utils.GlideUtils
import com.sw.dualscreen.view.CustomBottomSheetDialog
import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.AppUtil
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
import java.util.concurrent.Executors
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
private val viewModel by viewModels<UserViewModel>()
private lateinit var imageCapture: ImageCapture
private var imageCapture: ImageCapture? = null
private var isAnalyzing = true // 控制是否进行图像分析
private lateinit var presentation: SecondaryScreenPresentation
private val executor = Executors.newSingleThreadExecutor()
private lateinit var adapter: GenericItemAdapter<FoodInfo, ItemFoodInfoBinding>
private var checkedItem: FoodInfo? = null
private var imageAnalysis: ImageAnalysis? = null
private var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -54,6 +70,7 @@ class MainActivity : AppCompatActivity() {
private fun initData() {
viewModel.getEquipmentToken()
presentation.step1()
}
private fun initView() {
@@ -64,12 +81,20 @@ class MainActivity : AppCompatActivity() {
}
}
binding.previewView.clipToOutline = true
binding.tvToSearch.setOnClickListener {
val bottomSheetDialog = CustomBottomSheetDialog.newInstance()
val bottomSheetDialog = CustomBottomSheetDialog.newInstance(viewModel) {
updateCurrentFood(it)
}
bottomSheetDialog.show(supportFragmentManager, "CustomBottomSheetDialog")
}
binding.spinner.setItems(arrayOf("即放即取", "持续取餐"))
val spinnerAdapter = ArrayAdapter<String>(
this,
R.layout.item_for_custom_spinner,
arrayOf("即放即取", "持续取餐")
)
binding.spinner.adapter = spinnerAdapter
binding.spinner.setSelection(0)
binding.spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
@@ -84,13 +109,90 @@ class MainActivity : AppCompatActivity() {
}
}
adapter = createAdapter()
binding.recyclerview.layoutManager = GridLayoutManager(this, 2)
// 添加间距装饰(12dp)
binding.recyclerview.addItemDecoration(
GridSpacingItemDecoration(
spanCount = 2,
spacing = dpToPx(30),
includeEdge = false // 包含边缘间距
)
)
binding.recyclerview.adapter = adapter
binding.tvFoodName.setOnClickListener {
presentation.step3()
}
registerDataChange()
}
fun updateCurrentFood(foodInfo: FoodInfo?) {
presentation.updateFood(foodInfo)
if (foodInfo != null) {
binding.tvFoodName.text = foodInfo.foodName
binding.previewView.visibility = View.GONE
binding.ivImg.visibility = View.VISIBLE
GlideUtils.loadRoundCornerImage(this, foodInfo.imgUrl, binding.ivImg, 12)
pauseAnalysis()
} else {
binding.tvFoodName.text = "-"
binding.previewView.visibility = View.VISIBLE
binding.ivImg.visibility = View.GONE
resumeAnalysis()
viewModel.cleanIdentifiedFoodInfoList()
}
}
private fun registerDataChange() {
lifecycleScope.launch {
viewModel.identifiedFoodInfoList.collect { list ->
binding.recyclerview
adapter.updateData(list)
// presentation.step2()
}
}
viewModel.getIdentifiedFoodList()
// SensorScaleUtils.startScale { weight ->
// Timber.d("weight = $weight")
// if (weight > 0.0) {
// if (canIndentify) {
// viewModel.getIdentifiedFoodList()
// }
// }
// }
}
private fun createAdapter(): GenericItemAdapter<FoodInfo, ItemFoodInfoBinding> {
return GenericItemAdapter(
items = emptyList(),
bindingInflater = ItemFoodInfoBinding::inflate,
bindCallback = { item, position ->
this.tvName.text = item.foodName
if (item.id != checkedItem?.id) {
this.tvName.typeface = Typeface.defaultFromStyle(Typeface.NORMAL)
this.tvName.setTextColor(resources.getColor(R.color.identify_normal))
this.llRoot.setBackgroundResource(R.drawable.grid_item_normal)
} else {
this.tvName.typeface = Typeface.defaultFromStyle(Typeface.BOLD)
this.tvName.setTextColor(resources.getColor(R.color.identify_checked))
this.llRoot.setBackgroundResource(R.drawable.grid_item_checked)
}
//
this.llRoot.setOnClickListener {
Timber.d("itemClick ${item.foodName}, position = $position")
checkedItem = item
adapter.notifyDataSetChanged()
updateCurrentFood(item)
}
}
)
}
private fun setupCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
Timber.d("setupCamera")
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture!!.addListener({
val cameraProvider = cameraProviderFuture!!.get()
// 1. 创建预览用例
val preview = Preview.Builder()
@@ -100,17 +202,16 @@ class MainActivity : AppCompatActivity() {
}
// 2. 创建图像分析用例(用于副屏显示)
val imageAnalysis = ImageAnalysis.Builder()
imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
// .setTargetResolution(Size(640, 480)) // 降低分辨率减轻负担
.build()
.also { analysis ->
analysis.setAnalyzer(executor) { imageProxy ->
// 仅在步骤1时传递
if (isAnalyzing) {
val bitmap = imageProxy.toBitmap()
runOnUiThread {
presentation.updateImage(bitmap) // 更新副屏
}
presentation.updateImage(bitmap) // 更新副屏
}
imageProxy.close() // 必须关闭以释放资源
}
@@ -139,6 +240,48 @@ class MainActivity : AppCompatActivity() {
}, ContextCompat.getMainExecutor(this))
}
fun pauseAnalysis() {
Timber.d("pauseAnalysis")
isAnalyzing = false
imageAnalysis?.clearAnalyzer()
}
fun resumeAnalysis() {
Timber.d("resumeAnalysis isAnalyzing = $isAnalyzing")
if (isAnalyzing) return
isAnalyzing = true
imageAnalysis?.setAnalyzer(executor) { imageProxy ->
if (presentation.currentStep == 1) {
val bitmap = imageProxy.toBitmap()
presentation.updateImage(bitmap)
}
imageProxy.close()
}
}
/**
* 停止相机识别
*/
private fun shutdownCamera() {
Timber.d("shutdownCamera")
try {
// 1. 首先停止图像分析
imageAnalysis?.clearAnalyzer()
// 2. 解除所有绑定
cameraProviderFuture?.get()?.unbindAll()
// 3. 关闭相关资源
imageCapture = null
imageAnalysis = null
// 4. 停止副屏更新
isAnalyzing = false
} catch (e: Exception) {
Timber.e(e, "Error shutting down camera")
}
}
// 拍照功能
private fun takePhoto() {
// 临时停止分析以避免干扰
@@ -151,7 +294,7 @@ class MainActivity : AppCompatActivity() {
)
).build()
// 执行拍照
imageCapture.takePicture(
imageCapture?.takePicture(
outputFileOptions,
executor,
object : ImageCapture.OnImageSavedCallback {
@@ -191,8 +334,18 @@ class MainActivity : AppCompatActivity() {
viewModel = viewModel,
recognizeViewModel = viewModels<RecognizeViewModel>().value
)
presentation.setStepChangeCallback { step ->
if (step == 1) {
updateCurrentFood(null)
}
}
presentation.show()
}
}
override fun onDestroy() {
SensorScaleUtils.closeScale()
presentation.dismiss()
super.onDestroy()
}
}
@@ -56,7 +56,7 @@ val Float.sp: Float
)
// 添加扩展函数
fun ImageProxy.toBitmap(): Bitmap {
fun ImageProxy.toSafeBitmap(): Bitmap {
val yBuffer = planes[0].buffer // Y
val uBuffer = planes[1].buffer // U
val vBuffer = planes[2].buffer // V
@@ -0,0 +1,37 @@
package com.sw.dualscreen.ext
/**
* 姓名脱敏处理
* 2个字:张*
* 多于2个字:张*某
*/
fun String?.maskName(): String {
if (this == null || this.isEmpty()) return ""
return when {
length == 2 -> "${this[0]}*"
length > 2 -> "${this[0]}*${this[length - 1]}"
else -> this // 1个字的情况原样返回
}
}
/**
* 手机号脱敏处理
* 将第4-7位替换为*
* 例如:138****1234
*/
fun String?.maskPhone(): String {
if (this == null || this.isEmpty()) return ""
if (this.length < 11) {
return this
}
val start = 3 // 第4位(索引从0开始)
val end = 6 // 第7位
val sb = StringBuilder(this)
for (i in start..end) {
sb.setCharAt(i, '*')
}
return sb.toString()
}
@@ -5,11 +5,11 @@ import android.app.Presentation
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Outline
import android.graphics.Point
import android.hardware.Camera
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
@@ -17,16 +17,22 @@ import android.util.Log
import android.view.Display
import android.view.View
import android.view.ViewGroup
import android.view.ViewOutlineProvider
import android.view.ViewTreeObserver
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import com.arcsoft.face.ErrorInfo
import com.sw.dualscreen.R
import com.sw.dualscreen.activity.MainActivity
import com.sw.dualscreen.databinding.ItemUserNutritionBinding
import com.sw.dualscreen.databinding.PresentationSecondaryScreenBinding
import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.ext.maskName
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.utils.GlideUtils
import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.ConfigUtil
@@ -41,6 +47,7 @@ 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 kotlinx.coroutines.launch
import timber.log.Timber
/**
@@ -50,7 +57,7 @@ class SecondaryScreenPresentation(
val activity: MainActivity,
context: Context,
display: Display,
viewModel: UserViewModel,
val viewModel: UserViewModel,
private val onDismissListener: () -> Unit = {},
val recognizeViewModel: RecognizeViewModel
) : Presentation(context, display), ViewTreeObserver.OnGlobalLayoutListener {
@@ -60,7 +67,14 @@ class SecondaryScreenPresentation(
// private var cameraCaptureSession: CameraCaptureSession? = null
private lateinit var cameraManager: CameraManager
private var cameraIdList: Array<String> = arrayOf()
lateinit var previewView: PreviewView
// 当前食物信息
private var currentFood: FoodInfo? = null
private lateinit var itemUserNutritionBinding: ItemUserNutritionBinding
private lateinit var stepChangeCallback: (Int) -> Unit
// 当前步骤
var currentStep: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -68,9 +82,48 @@ class SecondaryScreenPresentation(
binding = PresentationSecondaryScreenBinding.inflate(layoutInflater)
setContentView(binding.root)
window?.setBackgroundDrawableResource(android.R.color.transparent)
initView()
// binding.includeUserHeat.ivHeat.showText("1070千卡")
registerDataChange()
}
previewView = binding.previewView
private fun registerDataChange() {
activity.lifecycleScope.launch {
viewModel.nutritionData.collect {
Timber.d("registerDataChange nutritionData = $it")
if (it == null) return@collect
itemUserNutritionBinding.tvUserName.text = it.userName.maskName()
itemUserNutritionBinding.tvRecommendHeat.text =
"推荐热量:${it.recommendMin}-${it.recommendMax}"
}
}
}
private fun initView() {
binding.ivImg.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(0, 0, view.width, view.height, 12f.dp)
}
}
binding.ivImg.clipToOutline = true
itemUserNutritionBinding = binding.includeUserNutrition
}
override fun onDisplayRemoved() {
super.onDisplayRemoved()
onDismissListener()
}
fun updateImage(bitmap: Bitmap) {
activity.runOnUiThread {
binding.ivImg.setImageBitmap(bitmap)
}
}
/**
* 启用虹软人脸失败
*/
private fun setupArcCamera() {
cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
// 获取摄像头列表
cameraIdList = cameraManager.cameraIdList
@@ -87,42 +140,95 @@ class SecondaryScreenPresentation(
initArcView()
openRectInfoDraw = true
openCamera(cameraIdList.last())
// binding.includeUserHeat.ivHeat.showText("1070千卡")
}
override fun onDisplayRemoved() {
super.onDisplayRemoved()
onDismissListener()
}
fun updateImage(bitmap: Bitmap) {
activity.runOnUiThread {
binding.ivImg.setImageBitmap(bitmap)
}
}
/**
* 用户1 显示相机预览
*/
fun step1(foodImg: Uri) {
fun step1() {
Timber.d("step1")
currentStep = 1
stepChangeCallback(currentStep)
binding.tvFoodInfo.text = "菜品识别中"
binding.flPreview.visibility = View.VISIBLE
// binding.previewView.visibility = View.VISIBLE
binding.ivImg.visibility = View.VISIBLE
binding.flFace.visibility = View.GONE
pauseCamera()
binding.tvBottomTip.visibility = View.VISIBLE
itemUserNutritionBinding.clNutritionData.visibility = View.GONE
}
/**
* 显示人脸识别
*/
fun step2() {
fun step2(foodInfo: FoodInfo) {
Timber.d("step2")
currentStep = 2
stepChangeCallback(currentStep)
currentFood = foodInfo
resumeCamera()
binding.tvFoodInfo.text = foodInfo.foodName
binding.flPreview.visibility = View.GONE
binding.flFace.visibility = View.VISIBLE
binding.ivFaceBg.visibility = View.VISIBLE
binding.tvFaceTip.visibility = View.VISIBLE
setupArcCamera()
}
fun updateFood(foodInfo: FoodInfo?) {
Timber.d("step2 updateFood")
currentFood = foodInfo
if (foodInfo != null) {
if (currentStep != 2) {
step2(foodInfo)
} else {
binding.tvFoodInfo.text = foodInfo.foodName
}
}
}
/**
* 显示识别出的菜品信息
*/
fun step3() {
Timber.d("step3")
currentStep = 3
stepChangeCallback(currentStep)
if (currentFood == null) {
ToastUtils.showToast("选中物品为空3")
return
}
binding.tvFoodInfo.text = currentFood!!.foodName
binding.flPreview.visibility = View.VISIBLE
// binding.previewView.visibility = View.GONE
binding.ivImg.visibility = View.VISIBLE
GlideUtils.loadRoundCornerImage(
context,
url = currentFood!!.imgUrl,
imageView = binding.ivImg,
radius = 12
)
binding.flFace.visibility = View.GONE
binding.ivFaceBg.visibility = View.GONE
binding.tvFaceTip.visibility = View.GONE
binding.tvBottomTip.visibility = View.GONE
itemUserNutritionBinding.clNutritionData.visibility = View.VISIBLE
viewModel.getUserNutritionData(
userId = "187c96f7bf294c08bc1d1e57b90a7210",
foodId = currentFood!!.id!!
)
}
private fun openCamera(cameraId: String) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) !=
PackageManager.PERMISSION_GRANTED
@@ -158,6 +264,10 @@ class SecondaryScreenPresentation(
}
}
fun setStepChangeCallback(callback: (Int) -> Unit) {
stepChangeCallback = callback
}
// 虹软人脸配置 ⬇
private var isRecognition = false
private var rgbCameraHelper: DualCameraHelper? = null
@@ -206,6 +316,9 @@ class SecondaryScreenPresentation(
})
recognizeViewModel.recognizeNotice.observe(activity, Observer { notice: String? ->
Timber.i("recognizeNotice observe notice = $notice")
if (currentStep == 3 && notice == "leave") {
step1()
}
})
recognizeViewModel.recognizeUserId.observe(activity, Observer { userId: String? ->
@@ -65,7 +65,7 @@ class RemoteRepository constructor(
*/
suspend fun getRestInfoFoodsByType(
restId: String = GlobalData.restId,
type: Int = 1,
type: Int = 0,
foodName: String,
): ApiResponse<List<FoodInfo>> {
return safeApiCall {
@@ -1,11 +1,10 @@
package com.sw.plate.sdk
package com.sw.dualscreen.sdk
import android.util.Log
import com.sw.plate.utils.ThreadUtils
import com.sw.plate.utils.ToastUtils
import com.wabon.wbintelligenthardwaresdk.api.SensorScale
import com.wabon.wbintelligenthardwaresdk.api.SensorScale.OnScaleResult
import kotlinx.coroutines.delay
import timber.log.Timber
typealias Callback = (Double) -> Unit
@@ -65,16 +64,19 @@ object SensorScaleUtils {
}
this.callback = callback
init()
mSensorScale?.zero {
Timber.d("startScale: zero")
}
mSensorScale?.openScale(serialPort, baudRate) { open ->
isOpened = open
Log.d(TAG, "isOpened = $isOpened")
Timber.d("isOpened = $isOpened")
if (open) {
if (autoScale) {
ThreadUtils.launchOnIo {
delay(1000)
Thread({
Thread.sleep(1000)
// 打开后需要等待后才能调用,否则会 1001 SDK未初始化
mSensorScale?.startContinuousRead()
}
}).start()
}
}
}
@@ -0,0 +1,25 @@
package com.sw.dualscreen.utils
class Debouncer(private val delayMillis: Long) {
private var lastActionTime = 0L
/**
* 执行防抖操作
* @param action 要执行的操作
* @return Boolean 是否执行了操作 (true=已执行, false=被防抖)
*/
fun debounce(action: () -> Unit): Boolean {
val currentTime = System.currentTimeMillis()
if (currentTime - lastActionTime >= delayMillis) {
lastActionTime = currentTime
action()
return true
}
return false
}
// 重置防抖计时
fun reset() {
lastActionTime = 0L
}
}
@@ -0,0 +1,236 @@
package com.sw.dualscreen.utils
import android.content.Context
import android.graphics.Bitmap
import android.widget.ImageView
import androidx.annotation.DrawableRes
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.resource.bitmap.CircleCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.Target
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
/**
* Glide图片加载工具类
*/
object GlideUtils {
// 默认配置
private val defaultOptions = RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.skipMemoryCache(false)
/**
* 普通加载图片
* @param context 上下文
* @param url 图片地址
* @param imageView 目标ImageView
* @param placeholderResId 占位图资源ID
* @param errorResId 错误图资源ID
*/
fun loadImage(
context: Context,
url: Any?,
imageView: ImageView,
@DrawableRes placeholderResId: Int = 0,
@DrawableRes errorResId: Int = 0
) {
val requestBuilder = Glide.with(context)
.load(url)
.apply(defaultOptions)
applyPlaceholder(requestBuilder, placeholderResId, errorResId)
.transition(DrawableTransitionOptions.withCrossFade())
.into(imageView)
}
/**
* 加载圆形图片
* @param context 上下文
* @param url 图片地址
* @param imageView 目标ImageView
* @param placeholderResId 占位图资源ID
* @param errorResId 错误图资源ID
*/
fun loadCircleImage(
context: Context,
url: Any?,
imageView: ImageView,
@DrawableRes placeholderResId: Int = 0,
@DrawableRes errorResId: Int = 0
) {
val requestBuilder = Glide.with(context)
.load(url)
.apply(defaultOptions)
.transform(CircleCrop())
applyPlaceholder(requestBuilder, placeholderResId, errorResId)
.into(imageView)
}
/**
* 加载圆角图片
* @param context 上下文
* @param url 图片地址
* @param imageView 目标ImageView
* @param radius 圆角半径(px)
* @param placeholderResId 占位图资源ID
* @param errorResId 错误图资源ID
*/
fun loadRoundCornerImage(
context: Context,
url: Any?,
imageView: ImageView,
radius: Int,
@DrawableRes placeholderResId: Int = 0,
@DrawableRes errorResId: Int = 0
) {
val requestBuilder = Glide.with(context)
.load(url)
.apply(defaultOptions)
.transform(RoundedCorners(radius))
applyPlaceholder(requestBuilder, placeholderResId, errorResId)
.into(imageView)
}
/**
* 加载图片并获取Bitmap
* @param context 上下文
* @param url 图片地址
* @param callback 加载回调
*/
fun loadImageAsBitmap(
context: Context,
url: Any?,
callback: (Bitmap?) -> Unit
) {
Glide.with(context)
.asBitmap()
.load(url)
.apply(defaultOptions)
.addListener(object : RequestListener<Bitmap> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Bitmap?>,
isFirstResource: Boolean
): Boolean {
callback(null)
return false
}
override fun onResourceReady(
resource: Bitmap,
model: Any,
target: Target<Bitmap?>?,
dataSource: DataSource,
isFirstResource: Boolean
): Boolean {
callback(resource)
return false
}
})
.submit()
}
/**
* 清除内存缓存
* @param context 上下文
*/
fun clearMemoryCache(context: Context) {
Glide.get(context).clearMemory()
}
/**
* 清除磁盘缓存
* @param context 上下文
*/
fun clearDiskCache(context: Context) {
CoroutineScope(Dispatchers.IO).launch {
Glide.get(context).clearDiskCache()
}
}
/**
* 获取缓存文件
* @param context 上下文
* @param url 图片地址
* @param callback 回调
*/
fun getImageCacheFile(
context: Context,
url: String,
callback: (File?) -> Unit
) {
Glide.with(context)
.downloadOnly()
.load(url)
.addListener(object : RequestListener<File> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<File?>,
isFirstResource: Boolean
): Boolean {
callback(null)
return false
}
override fun onResourceReady(
resource: File,
model: Any,
target: Target<File?>?,
dataSource: DataSource,
isFirstResource: Boolean
): Boolean {
callback(resource)
return false
}
})
.submit()
}
/**
* 暂停请求
* @param context 上下文
*/
fun pauseRequests(context: Context) {
Glide.with(context).pauseRequests()
}
/**
* 恢复请求
* @param context 上下文
*/
fun resumeRequests(context: Context) {
Glide.with(context).resumeRequests()
}
// 私有方法:应用占位图和错误图
private fun <T> applyPlaceholder(
requestBuilder: RequestBuilder<T>,
@DrawableRes placeholderResId: Int,
@DrawableRes errorResId: Int
): RequestBuilder<T> {
return requestBuilder.apply {
if (placeholderResId != 0) {
placeholder(placeholderResId)
}
if (errorResId != 0) {
error(errorResId)
}
}
}
}
@@ -1,14 +1,14 @@
package com.sw.dualscreen.view
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.sw.dualscreen.R
import com.sw.dualscreen.adapter.GenericItemAdapter
@@ -17,8 +17,15 @@ import com.sw.dualscreen.adapter.dpToPx
import com.sw.dualscreen.databinding.BottomSheetDialogBinding
import com.sw.dualscreen.databinding.ItemSearchFoodInfoBinding
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.viewmodel.UserViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
class CustomBottomSheetDialog : BottomSheetDialogFragment() {
class CustomBottomSheetDialog(
val viewModel: UserViewModel,
val itemClickCallback: (FoodInfo) -> Unit
) : BottomSheetDialogFragment() {
private lateinit var binding: BottomSheetDialogBinding
private lateinit var adapter: GenericItemAdapter<FoodInfo, ItemSearchFoodInfoBinding>
private var checkedItem: FoodInfo? = null
@@ -34,9 +41,12 @@ class CustomBottomSheetDialog : BottomSheetDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val debouncer = Debouncer(3000)
initView()
binding.ivSearch.setOnClickListener {
debouncer.debounce {
viewModel.searchByFoodName(binding.etSearch.text.toString())
}
}
}
@@ -63,8 +73,11 @@ class CustomBottomSheetDialog : BottomSheetDialogFragment() {
}
companion object {
fun newInstance(): CustomBottomSheetDialog {
return CustomBottomSheetDialog()
fun newInstance(
viewModel: UserViewModel,
itemClickCallback: (FoodInfo) -> Unit
): CustomBottomSheetDialog {
return CustomBottomSheetDialog(viewModel, itemClickCallback)
}
}
@@ -75,28 +88,20 @@ class CustomBottomSheetDialog : BottomSheetDialogFragment() {
binding.recyclerview.addItemDecoration(
GridSpacingItemDecoration(
spanCount = 2,
spacing = requireContext().dpToPx(6),
spacing = requireContext().dpToPx(30),
includeEdge = false // 包含边缘间距
)
)
binding.recyclerview.adapter = adapter
binding.recyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
val totalItems = layoutManager.itemCount
registerDataChange()
}
// if (!viewModel.isLoading && viewModel.canLoadMore
// && lastVisibleItem >= totalItems - 3
// ) {
// viewModel.getSearchMemberList(
// param = lastText,
// pageNum = viewModel.currentPage
// )
// }
private fun registerDataChange() {
lifecycleScope.launch {
viewModel.searchFoodInfoList.collect {
adapter.updateData(it)
}
})
}
}
private fun createAdapter(): GenericItemAdapter<FoodInfo, ItemSearchFoodInfoBinding> {
@@ -104,20 +109,30 @@ class CustomBottomSheetDialog : BottomSheetDialogFragment() {
items = emptyList(),
bindingInflater = ItemSearchFoodInfoBinding::inflate,
bindCallback = { item, position ->
// this.tvName.text = item.name.maskName()
// this.tvPhone.text = item.phone.maskPhone()
// if (item.id != checkedItem?.id) {
// this.llRoot.setBackgroundResource(R.drawable.grid_item_bind)
// } else {
// this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind)
// }
//
// this.llRoot.setOnClickListener {
// Timber.d("itemClick ${item.name}, position = $position")
// checkedItem = item
// adapter.notifyDataSetChanged()
// }
this.tvName.text = item.foodName
if (item.id != checkedItem?.id) {
this.tvName.typeface = Typeface.defaultFromStyle(Typeface.NORMAL)
this.tvName.setTextColor(resources.getColor(R.color.search_normal))
this.llRoot.setBackgroundResource(R.drawable.grid_search_item_normal)
} else {
this.tvName.typeface = Typeface.defaultFromStyle(Typeface.BOLD)
this.tvName.setTextColor(resources.getColor(R.color.search_checked))
this.llRoot.setBackgroundResource(R.drawable.grid_search_item_checked)
}
this.llRoot.setOnClickListener {
Timber.d("itemClick ${item.foodName}, position = $position")
checkedItem = item
// viewModel.updateCurrentItem(item)
itemClickCallback(item)
adapter.notifyDataSetChanged()
}
}
)
}
override fun dismiss() {
viewModel.cleanSearchFoodInfoList()
super.dismiss()
}
}
@@ -27,16 +27,12 @@ class UserViewModel : BaseViewModel() {
private val faceApi: FaceApi = FaceApi()
// 识别出的列表
private val _identifiedFoodInfoList = MutableStateFlow<List<FoodInfo>?>(null)
val identifiedFoodInfoList: StateFlow<List<FoodInfo>?> = _identifiedFoodInfoList
private val _identifiedFoodInfoList = MutableStateFlow<List<FoodInfo>>(emptyList())
val identifiedFoodInfoList: StateFlow<List<FoodInfo>> = _identifiedFoodInfoList
// 搜索出的食物列表
private val _foodInfoList = MutableStateFlow<List<FoodInfo>?>(null)
val foodInfoList: StateFlow<List<FoodInfo>?> = _foodInfoList
// 当前食物信息
private val _currentFoodInfo = MutableStateFlow<FoodInfo?>(null)
val currentFoodInfo: StateFlow<FoodInfo?> = _currentFoodInfo
private val _searchFoodInfoList = MutableStateFlow<List<FoodInfo>>(emptyList())
val searchFoodInfoList: StateFlow<List<FoodInfo>> = _searchFoodInfoList
// 就餐数据
private val _nutritionData = MutableStateFlow<NutritionData?>(null)
@@ -46,6 +42,7 @@ class UserViewModel : BaseViewModel() {
* 获取token
*/
fun getEquipmentToken(qrcodeId: String = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e") {
Timber.d("getEquipmentToken")
launchWithLoading {
val response = repository.getEquipmentToken(qrcodeId)
if (parseResponse(response)) {
@@ -131,35 +128,49 @@ class UserViewModel : BaseViewModel() {
Timber.d("resetUserInfo")
}
fun identifiedFoodList() {
fun getIdentifiedFoodList() {
Timber.d("identifiedFoodList")
launchWithLoading {
delay(5000)
_identifiedFoodInfoList.value = List(6) {
FoodInfo(
id = "185225109358109491${it}",
foodName = "豆芽炒粉条$it",
imgUrl = "http://101.201.149.156:9002/temp/20221119/62dc2d17-6853-4277-96ed-a4ef4c8654ea_1668840050258.png"
imgUrl = "http://101.201.149.156:9002/temp/20230912/f54b2d76-45e1-4c6b-a7a0-5fd1eace99d1_1694484601879.png"
)
}
// _currentFoodInfo.value = _identifiedFoodInfoList.value[0]
}
}
fun cleanIdentifiedFoodInfoList() {
Timber.d("cleanIdentifiedFoodInfoList")
_identifiedFoodInfoList.value = emptyList<FoodInfo>()
}
/**
* 搜索食物
*/
fun searchByFoodName(foodName: String) {
Timber.d("searchByFoodName foodName = $foodName")
launchWithLoading {
val response = repository.getRestInfoFoodsByType(foodName = foodName)
if (parseResponse(response)) {
_foodInfoList.value = response.result
_searchFoodInfoList.value = response.result ?: emptyList()
}
}
}
fun cleanSearchFoodInfoList() {
Timber.d("cleanSearchFoodInfoList")
_searchFoodInfoList.value = emptyList<FoodInfo>()
}
/**
* 获取用户就餐数据
*/
fun getUserNutritionData(userId: String, foodId: String) {
Timber.d("getUserNutritionData userId = ${userId}, foodId = $foodId")
launchWithLoading {
val response = repository.getUserNutritionData(userId = userId, foodId = foodId)
if (parseResponse(response)) {
@@ -172,6 +183,7 @@ class UserViewModel : BaseViewModel() {
* 提交就餐数据
*/
fun postUserNutritionData(param: UserNutritionParam) {
Timber.d("postUserNutritionData param = $param")
launchWithLoading {
val response = repository.postUserNutritionData(param)
if (parseResponse(response)) {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 952 KiB

@@ -8,5 +8,5 @@
android:topRightRadius="12dp" />
<stroke
android:width="2dp"
android:color="#ffece7d0" />
android:color="#33343B" />
</shape>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

@@ -5,4 +5,7 @@
android:bottomRightRadius="12dp"
android:topLeftRadius="12dp"
android:topRightRadius="12dp" />
<stroke
android:color="#ECE7D0"
android:width="2dp" />
</shape>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 填充颜色 -->
<solid android:color="@color/black" />
<stroke
android:width="2dp"
android:color="#33343B" />
<!-- 矩形的圆角半径 -->
<corners android:radius="12dp" />
</shape>
+14 -7
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
@@ -21,21 +20,22 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:padding="28dp"
android:background="@drawable/bg_detection_selector">
<com.sw.dualscreen.view.CustomSpinner
<Spinner
android:id="@+id/spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:spinnerTextColor="#ffece7d0"
app:spinnerTextSize="26sp" />
style="@style/customSpinnerStyle"
android:layout_gravity="end"
android:padding="0dp"
android:popupBackground="@drawable/shape_for_custom_spinner" />
</LinearLayout>
</FrameLayout>
<TextView
android:id="@+id/tv_goods"
android:id="@+id/tv_food_name"
style="@style/text_title_blob"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -46,7 +46,7 @@
android:layout_width="match_parent"
android:layout_height="531dp"
android:layout_marginHorizontal="60dp"
android:layout_marginTop="120dp"
android:layout_marginTop="110dp"
android:layout_marginBottom="90dp"
android:visibility="visible">
@@ -55,6 +55,13 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_img"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="食物图预览"
android:visibility="gone" />
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
+2 -3
View File
@@ -6,7 +6,7 @@
android:background="@drawable/grid_item_checked"
android:gravity="center"
android:orientation="vertical"
android:paddingVertical="42dp">
android:paddingVertical="40dp">
<TextView
android:id="@+id/tv_name"
@@ -16,7 +16,6 @@
android:maxLines="1"
android:ellipsize="end"
android:textColor="#ff031127"
android:textSize="36sp"
android:textStyle="bold" />
android:textSize="36sp" />
</LinearLayout>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:gravity="center"
android:paddingHorizontal="40dp"
android:paddingVertical="24dp"
android:singleLine="true"
android:text="下拉列表项样式"
android:textColor="#ECE7D0"
android:textSize="26dp" />
@@ -6,7 +6,7 @@
android:background="@drawable/grid_search_item_checked"
android:gravity="center"
android:orientation="vertical"
android:paddingVertical="42dp">
android:paddingVertical="35dp">
<TextView
android:id="@+id/tv_name"
@@ -16,7 +16,6 @@
android:maxLines="1"
android:ellipsize="end"
android:textColor="#FF9900"
android:textSize="30sp"
android:textStyle="bold" />
android:textSize="30sp" />
</LinearLayout>
@@ -1,26 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/cl_nutritionData"
android:layout_width="540dp"
android:layout_height="487dp"
android:background="@drawable/rounded_border_transparent"
android:visibility="visible"
android:padding="28dp">
<TextView
android:id="@+id/tv_user_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="夏*秋"
tools:text="夏*秋"
android:textColor="#ffece7d0"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_heat"
android:id="@+id/tv_recommend_heat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="推荐热量:680kcal"
tools:text="推荐热量:680kcal"
android:textColor="#ffece7d0"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
@@ -87,9 +89,9 @@
<com.sw.dualscreen.view.CustomImageView
android:id="@+id/iv_staple_food"
android:layout_width="87dp"
android:src="#99CC99"
android:layout_height="100dp"
android:layout_gravity="bottom|center"
android:layout_height="100dp" />
android:src="#99CC99" />
<ImageView
android:layout_width="wrap_content"
@@ -106,8 +108,8 @@
<com.sw.dualscreen.view.CustomImageView
android:id="@+id/iv_fruits_vegetables"
android:layout_width="87dp"
android:layout_gravity="bottom|center"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:layout_gravity="bottom|center" />
<ImageView
android:layout_width="wrap_content"
@@ -124,8 +126,8 @@
<com.sw.dualscreen.view.CustomImageView
android:id="@+id/iv_meat_eggs"
android:layout_width="87dp"
android:layout_gravity="bottom|center"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:layout_gravity="bottom|center" />
<ImageView
android:layout_width="wrap_content"
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
@@ -9,11 +9,42 @@
android:orientation="vertical"
tools:context=".activity.MainActivity">
<LinearLayout
<!--人脸识别-->
<FrameLayout
android:id="@+id/fl_face"
android:layout_width="376dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingTop="1dp"
android:visibility="gone">
<TextureView
android:id="@+id/dual_camera_texture_preview_rgb"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="visible" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<ImageView
android:id="@+id/iv_face_bg"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@drawable/bg_face"
android:contentDescription="相机遮盖层"
android:visibility="visible" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="80dp"
android:paddingTop="80dp">
android:orientation="vertical"
android:padding="80dp">
<LinearLayout
android:layout_width="match_parent"
@@ -24,55 +55,19 @@
android:visibility="visible">
<TextView
android:id="@+id/tv_goods"
android:id="@+id/tv_food_info"
style="@style/text_title_blob_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="羊肉臊子荞面饸饹" />
tools:text="羊肉臊子荞面饸饹" />
</LinearLayout>
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="40dp"
android:visibility="visible">
<FrameLayout
android:id="@+id/fl_face"
android:layout_width="376dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingTop="1dp"
android:visibility="gone">
<TextureView
android:id="@+id/dual_camera_texture_preview_rgb"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="visible" />
<com.sw.plate.utils.arcface.FaceRectView
android:id="@+id/dual_camera_face_rect_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<ImageView
android:id="@+id/iv_face_bg"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@drawable/bg_avatar"
android:visibility="gone" />
<TextView
android:id="@+id/tv_face_tip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="376dp"
android:layout_marginTop="566dp"
android:text="请正视屏幕"
android:textColor="#ffece7d0"
android:textSize="24sp"
@@ -82,21 +77,21 @@
android:id="@+id/fl_preview"
android:layout_width="match_parent"
android:layout_height="420dp"
android:visibility="visible"
android:paddingHorizontal="80dp">
android:layout_marginTop="174dp"
android:visibility="visible">
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- <androidx.camera.view.PreviewView-->
<!-- android:id="@+id/previewView"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent"-->
<!-- android:visibility="gone" />-->
<ImageView
android:id="@+id/iv_img"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible"
android:contentDescription="img" />
android:contentDescription="img"
android:visibility="visible" />
<View
android:layout_width="match_parent"
@@ -107,30 +102,22 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:paddingHorizontal="80dp"
android:paddingBottom="80dp">
android:layout_height="486dp"
android:layout_marginTop="633dp"
android:background="@drawable/rounded_border_transparent"
android:gravity="center">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="486dp"
android:background="@drawable/rounded_border_transparent"
android:gravity="center">
<TextView
android:id="@+id/tv_bottom_tip"
style="@style/text_100_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="先识别 后取餐"
android:visibility="visible" />
<TextView
android:id="@+id/tv_bottom_tip"
style="@style/text_100_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="先识别 后取餐"
android:visibility="gone" />
<include
android:id="@+id/include_user_heat"
layout="@layout/item_user_heat"
android:visibility="visible" />
</LinearLayout>
<include
android:id="@+id/include_user_nutrition"
layout="@layout/item_user_nutrition" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
</FrameLayout>
+5
View File
@@ -7,4 +7,9 @@
<color name="beige_ECE7D0">#ECE7D0</color>
<color name="blue_6498DD">#6498DD</color>
<color name="black_031127">#031127</color>
<color name="identify_checked">#031127</color>
<color name="identify_normal">#6498DD</color>
<color name="search_checked">#FF9900</color>
<color name="search_normal">#666666</color>
</resources>
+6
View File
@@ -60,4 +60,10 @@
<item name="android:textSize">26sp</item>
<item name="android:textColor">#ffece7d0</item>
</style>
<style name="customSpinnerStyle" parent="android:Widget.ListView.DropDown">
<item name="android:textAlignment">center</item>
<item name="divider">@color/white</item>
<item name="android:dividerHeight">1dp</item>
</style>
</resources>
+2
View File
@@ -1,5 +1,6 @@
[versions]
agp = "8.10.1"
glide = "4.16.0"
kotlin = "2.0.21"
coreKtx = "1.10.1"
junit = "4.13.2"
@@ -24,6 +25,7 @@ lifecycleViewmodelKtx = "2.8.3"
lifecycleRuntimeKtx = "2.8.3"
[libraries]
glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
@@ -422,7 +422,7 @@ public class FaceHelper implements FaceListener {
if (!contained) {
RecognizeInfo recognizeInfo = recognizeInfoMap.remove(key);
if (recognizeInfo != null) {
recognizeCallback.onNoticeChanged("");
recognizeCallback.onNoticeChanged("leave");
// 人脸离开时,通知特征提取线程,避免一直等待活体结果
synchronized (recognizeInfo.getWaitLock()) {
recognizeInfo.getWaitLock().notifyAll();