添加了副屏显示逻辑控制

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"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK"> <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.activity.ktx)
implementation(libs.androidx.fragment.ktx) implementation(libs.androidx.fragment.ktx)
// 图片加载
implementation(libs.glide)
} }
@@ -2,11 +2,13 @@ package com.sw.dualscreen.activity
import android.content.Context import android.content.Context
import android.graphics.Outline import android.graphics.Outline
import android.graphics.Typeface
import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.ViewOutlineProvider import android.view.ViewOutlineProvider
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@@ -17,27 +19,41 @@ import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.google.common.util.concurrent.ListenableFuture 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.ActivityMainBinding
import com.sw.dualscreen.databinding.ItemFoodInfoBinding
import com.sw.dualscreen.ext.dp import com.sw.dualscreen.ext.dp
import com.sw.dualscreen.model.response.FoodInfo
import com.sw.dualscreen.presentation.SecondaryScreenPresentation 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.view.CustomBottomSheetDialog
import com.sw.dualscreen.viewmodel.UserViewModel import com.sw.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.AppUtil import com.sw.plate.utils.AppUtil
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.util.concurrent.Executors import java.util.concurrent.Executors
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
private val viewModel by viewModels<UserViewModel>() private val viewModel by viewModels<UserViewModel>()
private lateinit var imageCapture: ImageCapture private var imageCapture: ImageCapture? = null
private var isAnalyzing = true // 控制是否进行图像分析 private var isAnalyzing = true // 控制是否进行图像分析
private lateinit var presentation: SecondaryScreenPresentation private lateinit var presentation: SecondaryScreenPresentation
private val executor = Executors.newSingleThreadExecutor() 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -54,6 +70,7 @@ class MainActivity : AppCompatActivity() {
private fun initData() { private fun initData() {
viewModel.getEquipmentToken() viewModel.getEquipmentToken()
presentation.step1()
} }
private fun initView() { private fun initView() {
@@ -64,12 +81,20 @@ class MainActivity : AppCompatActivity() {
} }
} }
binding.previewView.clipToOutline = true binding.previewView.clipToOutline = true
binding.tvToSearch.setOnClickListener { binding.tvToSearch.setOnClickListener {
val bottomSheetDialog = CustomBottomSheetDialog.newInstance() val bottomSheetDialog = CustomBottomSheetDialog.newInstance(viewModel) {
updateCurrentFood(it)
}
bottomSheetDialog.show(supportFragmentManager, "CustomBottomSheetDialog") 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 { binding.spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected( override fun onItemSelected(
parent: AdapterView<*>?, 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() { private fun setupCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this) Timber.d("setupCamera")
cameraProviderFuture.addListener({ cameraProviderFuture = ProcessCameraProvider.getInstance(this)
val cameraProvider = cameraProviderFuture.get() cameraProviderFuture!!.addListener({
val cameraProvider = cameraProviderFuture!!.get()
// 1. 创建预览用例 // 1. 创建预览用例
val preview = Preview.Builder() val preview = Preview.Builder()
@@ -100,18 +202,17 @@ class MainActivity : AppCompatActivity() {
} }
// 2. 创建图像分析用例(用于副屏显示) // 2. 创建图像分析用例(用于副屏显示)
val imageAnalysis = ImageAnalysis.Builder() imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
// .setTargetResolution(Size(640, 480)) // 降低分辨率减轻负担 // .setTargetResolution(Size(640, 480)) // 降低分辨率减轻负担
.build() .build()
.also { analysis -> .also { analysis ->
analysis.setAnalyzer(executor) { imageProxy -> analysis.setAnalyzer(executor) { imageProxy ->
// 仅在步骤1时传递
if (isAnalyzing) { if (isAnalyzing) {
val bitmap = imageProxy.toBitmap() val bitmap = imageProxy.toBitmap()
runOnUiThread {
presentation.updateImage(bitmap) // 更新副屏 presentation.updateImage(bitmap) // 更新副屏
} }
}
imageProxy.close() // 必须关闭以释放资源 imageProxy.close() // 必须关闭以释放资源
} }
} }
@@ -139,6 +240,48 @@ class MainActivity : AppCompatActivity() {
}, ContextCompat.getMainExecutor(this)) }, 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() { private fun takePhoto() {
// 临时停止分析以避免干扰 // 临时停止分析以避免干扰
@@ -151,7 +294,7 @@ class MainActivity : AppCompatActivity() {
) )
).build() ).build()
// 执行拍照 // 执行拍照
imageCapture.takePicture( imageCapture?.takePicture(
outputFileOptions, outputFileOptions,
executor, executor,
object : ImageCapture.OnImageSavedCallback { object : ImageCapture.OnImageSavedCallback {
@@ -191,8 +334,18 @@ class MainActivity : AppCompatActivity() {
viewModel = viewModel, viewModel = viewModel,
recognizeViewModel = viewModels<RecognizeViewModel>().value recognizeViewModel = viewModels<RecognizeViewModel>().value
) )
presentation.setStepChangeCallback { step ->
if (step == 1) {
updateCurrentFood(null)
}
}
presentation.show() 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 yBuffer = planes[0].buffer // Y
val uBuffer = planes[1].buffer // U val uBuffer = planes[1].buffer // U
val vBuffer = planes[2].buffer // V 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.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Outline
import android.graphics.Point import android.graphics.Point
import android.hardware.Camera import android.hardware.Camera
import android.hardware.camera2.CameraDevice import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager import android.hardware.camera2.CameraManager
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics import android.util.DisplayMetrics
@@ -17,16 +17,22 @@ import android.util.Log
import android.view.Display import android.view.Display
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewOutlineProvider
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import com.arcsoft.face.ErrorInfo import com.arcsoft.face.ErrorInfo
import com.sw.dualscreen.R import com.sw.dualscreen.R
import com.sw.dualscreen.activity.MainActivity import com.sw.dualscreen.activity.MainActivity
import com.sw.dualscreen.databinding.ItemUserNutritionBinding
import com.sw.dualscreen.databinding.PresentationSecondaryScreenBinding 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.dualscreen.viewmodel.UserViewModel
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import com.sw.plate.utils.arcface.ConfigUtil 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.FacePreviewInfo
import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration import com.sw.plate.utils.arcface.face.model.RecognizeConfiguration
import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel import com.sw.plate.utils.arcface.viewmodel.RecognizeViewModel
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
/** /**
@@ -50,7 +57,7 @@ class SecondaryScreenPresentation(
val activity: MainActivity, val activity: MainActivity,
context: Context, context: Context,
display: Display, display: Display,
viewModel: UserViewModel, val viewModel: UserViewModel,
private val onDismissListener: () -> Unit = {}, private val onDismissListener: () -> Unit = {},
val recognizeViewModel: RecognizeViewModel val recognizeViewModel: RecognizeViewModel
) : Presentation(context, display), ViewTreeObserver.OnGlobalLayoutListener { ) : Presentation(context, display), ViewTreeObserver.OnGlobalLayoutListener {
@@ -60,7 +67,14 @@ class SecondaryScreenPresentation(
// private var cameraCaptureSession: CameraCaptureSession? = null // private var cameraCaptureSession: CameraCaptureSession? = null
private lateinit var cameraManager: CameraManager private lateinit var cameraManager: CameraManager
private var cameraIdList: Array<String> = arrayOf() 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -68,9 +82,48 @@ class SecondaryScreenPresentation(
binding = PresentationSecondaryScreenBinding.inflate(layoutInflater) binding = PresentationSecondaryScreenBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
window?.setBackgroundDrawableResource(android.R.color.transparent) 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 cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
// 获取摄像头列表 // 获取摄像头列表
cameraIdList = cameraManager.cameraIdList cameraIdList = cameraManager.cameraIdList
@@ -87,41 +140,94 @@ class SecondaryScreenPresentation(
initArcView() initArcView()
openRectInfoDraw = true openRectInfoDraw = true
openCamera(cameraIdList.last()) 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 显示相机预览 * 用户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() { 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) { private fun openCamera(cameraId: String) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) !=
@@ -158,6 +264,10 @@ class SecondaryScreenPresentation(
} }
} }
fun setStepChangeCallback(callback: (Int) -> Unit) {
stepChangeCallback = callback
}
// 虹软人脸配置 ⬇ // 虹软人脸配置 ⬇
private var isRecognition = false private var isRecognition = false
private var rgbCameraHelper: DualCameraHelper? = null private var rgbCameraHelper: DualCameraHelper? = null
@@ -206,6 +316,9 @@ class SecondaryScreenPresentation(
}) })
recognizeViewModel.recognizeNotice.observe(activity, Observer { notice: String? -> recognizeViewModel.recognizeNotice.observe(activity, Observer { notice: String? ->
Timber.i("recognizeNotice observe notice = $notice") Timber.i("recognizeNotice observe notice = $notice")
if (currentStep == 3 && notice == "leave") {
step1()
}
}) })
recognizeViewModel.recognizeUserId.observe(activity, Observer { userId: String? -> recognizeViewModel.recognizeUserId.observe(activity, Observer { userId: String? ->
@@ -65,7 +65,7 @@ class RemoteRepository constructor(
*/ */
suspend fun getRestInfoFoodsByType( suspend fun getRestInfoFoodsByType(
restId: String = GlobalData.restId, restId: String = GlobalData.restId,
type: Int = 1, type: Int = 0,
foodName: String, foodName: String,
): ApiResponse<List<FoodInfo>> { ): ApiResponse<List<FoodInfo>> {
return safeApiCall { return safeApiCall {
@@ -1,11 +1,10 @@
package com.sw.plate.sdk package com.sw.dualscreen.sdk
import android.util.Log import android.util.Log
import com.sw.plate.utils.ThreadUtils
import com.sw.plate.utils.ToastUtils import com.sw.plate.utils.ToastUtils
import com.wabon.wbintelligenthardwaresdk.api.SensorScale import com.wabon.wbintelligenthardwaresdk.api.SensorScale
import com.wabon.wbintelligenthardwaresdk.api.SensorScale.OnScaleResult import com.wabon.wbintelligenthardwaresdk.api.SensorScale.OnScaleResult
import kotlinx.coroutines.delay import timber.log.Timber
typealias Callback = (Double) -> Unit typealias Callback = (Double) -> Unit
@@ -65,16 +64,19 @@ object SensorScaleUtils {
} }
this.callback = callback this.callback = callback
init() init()
mSensorScale?.zero {
Timber.d("startScale: zero")
}
mSensorScale?.openScale(serialPort, baudRate) { open -> mSensorScale?.openScale(serialPort, baudRate) { open ->
isOpened = open isOpened = open
Log.d(TAG, "isOpened = $isOpened") Timber.d("isOpened = $isOpened")
if (open) { if (open) {
if (autoScale) { if (autoScale) {
ThreadUtils.launchOnIo { Thread({
delay(1000) Thread.sleep(1000)
// 打开后需要等待后才能调用,否则会 1001 SDK未初始化 // 打开后需要等待后才能调用,否则会 1001 SDK未初始化
mSensorScale?.startContinuousRead() 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 package com.sw.dualscreen.view
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.sw.dualscreen.R import com.sw.dualscreen.R
import com.sw.dualscreen.adapter.GenericItemAdapter 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.BottomSheetDialogBinding
import com.sw.dualscreen.databinding.ItemSearchFoodInfoBinding import com.sw.dualscreen.databinding.ItemSearchFoodInfoBinding
import com.sw.dualscreen.model.response.FoodInfo 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 binding: BottomSheetDialogBinding
private lateinit var adapter: GenericItemAdapter<FoodInfo, ItemSearchFoodInfoBinding> private lateinit var adapter: GenericItemAdapter<FoodInfo, ItemSearchFoodInfoBinding>
private var checkedItem: FoodInfo? = null private var checkedItem: FoodInfo? = null
@@ -34,9 +41,12 @@ class CustomBottomSheetDialog : BottomSheetDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val debouncer = Debouncer(3000)
initView()
binding.ivSearch.setOnClickListener { binding.ivSearch.setOnClickListener {
debouncer.debounce {
viewModel.searchByFoodName(binding.etSearch.text.toString())
}
} }
} }
@@ -63,8 +73,11 @@ class CustomBottomSheetDialog : BottomSheetDialogFragment() {
} }
companion object { companion object {
fun newInstance(): CustomBottomSheetDialog { fun newInstance(
return CustomBottomSheetDialog() viewModel: UserViewModel,
itemClickCallback: (FoodInfo) -> Unit
): CustomBottomSheetDialog {
return CustomBottomSheetDialog(viewModel, itemClickCallback)
} }
} }
@@ -75,28 +88,20 @@ class CustomBottomSheetDialog : BottomSheetDialogFragment() {
binding.recyclerview.addItemDecoration( binding.recyclerview.addItemDecoration(
GridSpacingItemDecoration( GridSpacingItemDecoration(
spanCount = 2, spanCount = 2,
spacing = requireContext().dpToPx(6), spacing = requireContext().dpToPx(30),
includeEdge = false // 包含边缘间距 includeEdge = false // 包含边缘间距
) )
) )
binding.recyclerview.adapter = adapter binding.recyclerview.adapter = adapter
binding.recyclerview.addOnScrollListener(object : RecyclerView.OnScrollListener() { registerDataChange()
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
// if (!viewModel.isLoading && viewModel.canLoadMore private fun registerDataChange() {
// && lastVisibleItem >= totalItems - 3 lifecycleScope.launch {
// ) { viewModel.searchFoodInfoList.collect {
// viewModel.getSearchMemberList( adapter.updateData(it)
// param = lastText, }
// pageNum = viewModel.currentPage
// )
// }
} }
})
} }
private fun createAdapter(): GenericItemAdapter<FoodInfo, ItemSearchFoodInfoBinding> { private fun createAdapter(): GenericItemAdapter<FoodInfo, ItemSearchFoodInfoBinding> {
@@ -104,20 +109,30 @@ class CustomBottomSheetDialog : BottomSheetDialogFragment() {
items = emptyList(), items = emptyList(),
bindingInflater = ItemSearchFoodInfoBinding::inflate, bindingInflater = ItemSearchFoodInfoBinding::inflate,
bindCallback = { item, position -> bindCallback = { item, position ->
// this.tvName.text = item.name.maskName() this.tvName.text = item.foodName
// this.tvPhone.text = item.phone.maskPhone() if (item.id != checkedItem?.id) {
// if (item.id != checkedItem?.id) { this.tvName.typeface = Typeface.defaultFromStyle(Typeface.NORMAL)
// this.llRoot.setBackgroundResource(R.drawable.grid_item_bind) this.tvName.setTextColor(resources.getColor(R.color.search_normal))
// } else { this.llRoot.setBackgroundResource(R.drawable.grid_search_item_normal)
// this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind) } else {
// } this.tvName.typeface = Typeface.defaultFromStyle(Typeface.BOLD)
// this.tvName.setTextColor(resources.getColor(R.color.search_checked))
// this.llRoot.setOnClickListener { this.llRoot.setBackgroundResource(R.drawable.grid_search_item_checked)
// Timber.d("itemClick ${item.name}, position = $position") }
// checkedItem = item
// adapter.notifyDataSetChanged() 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 faceApi: FaceApi = FaceApi()
// 识别出的列表 // 识别出的列表
private val _identifiedFoodInfoList = MutableStateFlow<List<FoodInfo>?>(null) private val _identifiedFoodInfoList = MutableStateFlow<List<FoodInfo>>(emptyList())
val identifiedFoodInfoList: StateFlow<List<FoodInfo>?> = _identifiedFoodInfoList val identifiedFoodInfoList: StateFlow<List<FoodInfo>> = _identifiedFoodInfoList
// 搜索出的食物列表 // 搜索出的食物列表
private val _foodInfoList = MutableStateFlow<List<FoodInfo>?>(null) private val _searchFoodInfoList = MutableStateFlow<List<FoodInfo>>(emptyList())
val foodInfoList: StateFlow<List<FoodInfo>?> = _foodInfoList val searchFoodInfoList: StateFlow<List<FoodInfo>> = _searchFoodInfoList
// 当前食物信息
private val _currentFoodInfo = MutableStateFlow<FoodInfo?>(null)
val currentFoodInfo: StateFlow<FoodInfo?> = _currentFoodInfo
// 就餐数据 // 就餐数据
private val _nutritionData = MutableStateFlow<NutritionData?>(null) private val _nutritionData = MutableStateFlow<NutritionData?>(null)
@@ -46,6 +42,7 @@ class UserViewModel : BaseViewModel() {
* 获取token * 获取token
*/ */
fun getEquipmentToken(qrcodeId: String = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e") { fun getEquipmentToken(qrcodeId: String = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e") {
Timber.d("getEquipmentToken")
launchWithLoading { launchWithLoading {
val response = repository.getEquipmentToken(qrcodeId) val response = repository.getEquipmentToken(qrcodeId)
if (parseResponse(response)) { if (parseResponse(response)) {
@@ -131,35 +128,49 @@ class UserViewModel : BaseViewModel() {
Timber.d("resetUserInfo") Timber.d("resetUserInfo")
} }
fun identifiedFoodList() { fun getIdentifiedFoodList() {
Timber.d("identifiedFoodList")
launchWithLoading { launchWithLoading {
delay(5000) delay(5000)
_identifiedFoodInfoList.value = List(6) { _identifiedFoodInfoList.value = List(6) {
FoodInfo( FoodInfo(
id = "185225109358109491${it}", id = "185225109358109491${it}",
foodName = "豆芽炒粉条$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) { fun searchByFoodName(foodName: String) {
Timber.d("searchByFoodName foodName = $foodName")
launchWithLoading { launchWithLoading {
val response = repository.getRestInfoFoodsByType(foodName = foodName) val response = repository.getRestInfoFoodsByType(foodName = foodName)
if (parseResponse(response)) { 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) { fun getUserNutritionData(userId: String, foodId: String) {
Timber.d("getUserNutritionData userId = ${userId}, foodId = $foodId")
launchWithLoading { launchWithLoading {
val response = repository.getUserNutritionData(userId = userId, foodId = foodId) val response = repository.getUserNutritionData(userId = userId, foodId = foodId)
if (parseResponse(response)) { if (parseResponse(response)) {
@@ -172,6 +183,7 @@ class UserViewModel : BaseViewModel() {
* 提交就餐数据 * 提交就餐数据
*/ */
fun postUserNutritionData(param: UserNutritionParam) { fun postUserNutritionData(param: UserNutritionParam) {
Timber.d("postUserNutritionData param = $param")
launchWithLoading { launchWithLoading {
val response = repository.postUserNutritionData(param) val response = repository.postUserNutritionData(param)
if (parseResponse(response)) { if (parseResponse(response)) {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 952 KiB

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