Files
StallDualScreen/app/src/main/java/com/sw/dualscreen/activity/MainActivity.kt
T

431 lines
16 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.sw.dualscreen.activity
import android.content.Context
import android.graphics.Outline
import android.graphics.Typeface
import android.hardware.display.DisplayManager
import android.net.Uri
import android.text.TextUtils
import android.view.View
import android.view.ViewOutlineProvider
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.activity.viewModels
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCapture
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.objbox.FoodModule
import com.sw.dualscreen.presentation.SecondaryScreenPresentation
import com.sw.dualscreen.sdk.SensorScaleUtils
import com.sw.dualscreen.utils.AssetsTool
import com.sw.dualscreen.utils.BitmapCropper
import com.sw.dualscreen.utils.BitmapSaver
import com.sw.dualscreen.utils.Debouncer
import com.sw.dualscreen.utils.GlideUtils
import com.sw.dualscreen.utils.GsonUtils
import com.sw.dualscreen.utils.ImageUtil
import com.sw.dualscreen.view.CustomBottomSheetDialog
import com.sw.dualscreen.viewmodel.BaseViewModel
import com.sw.dualscreen.viewmodel.UserViewModel
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 : BaseActivity<ActivityMainBinding>() {
private val viewModel by viewModels<UserViewModel>()
private var imageCapture: ImageCapture? = null
private var isAnalyzing = true // 控制是否进行图像分析
private var presentation: SecondaryScreenPresentation? = null
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
private var canIdentify: Boolean = false
private var bottomSheetDialog: CustomBottomSheetDialog? = null
private var lastWeight: Double = 0.0
private var lastPhotoUri: Uri? = null // 最后拍照的图片
private var debouncer = Debouncer(2000)
override fun getViewModel(): BaseViewModel {
return viewModel
}
override fun inflateViewBinding(): ActivityMainBinding {
return ActivityMainBinding.inflate(layoutInflater)
}
override fun initialize() {
super.initialize()
initView()
setupSecondaryDisplay()
setupCamera()
initData()
FoodModule.init(this)
}
private fun initData() {
viewModel.getEquipmentToken()
presentation?.step1()
}
private fun initView() {
viewModel.activeEngine()
binding.previewView.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(0, 0, view.width, view.height, 12f.dp)
}
}
binding.previewView.clipToOutline = true
binding.tvToSearch.setOnClickListener {
bottomSheetDialog = CustomBottomSheetDialog.newInstance(viewModel) {
it.photoUri = null
updateCurrentFood(it)
}
bottomSheetDialog!!.show(supportFragmentManager, "CustomBottomSheetDialog")
}
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<*>?,
view: View?,
position: Int,
id: Long
) {
presentation?.updateMealPickupMode(position)
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
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 {
SensorScaleUtils.zero()
}
}
fun updateCurrentFood(foodInfo: FoodInfo?) {
if (foodInfo != null) {
presentation?.updateFood(foodInfo)
binding.tvFoodName.text = foodInfo.foodName
binding.previewView.visibility = View.GONE
binding.ivImg.visibility = View.VISIBLE
val imgUrl = foodInfo.imgUrl
if (TextUtils.isEmpty(imgUrl)) {
takePhoto { imgUri ->
GlideUtils.loadRoundCornerImage(this, imgUri, binding.ivImg, 12)
presentation?.updateFood(foodInfo.copy(imgUrl = imgUri.toString()))
}
} else {
if (foodInfo.photoUri != null) {
GlideUtils.loadRoundCornerImage(this, foodInfo.photoUri, binding.ivImg, 12)
} else {
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()
}
}
override fun registerDataChange() {
super.registerDataChange()
lifecycleScope.launch {
viewModel.identifiedFoodInfoList2.collect { list ->
Timber.d("registerDataChange识别后查询接口数据:${GsonUtils.toJson(list)}")
adapter.updateData(list)
if (list.isNotEmpty()) {
checkedItem = list[0]
checkedItem!!.photoUri = lastPhotoUri
updateCurrentFood(checkedItem)
lastPhotoUri = null
}
}
}
lifecycleScope.launch {
viewModel.identifiedFoodInfoList.collect { list ->
adapter.updateData(list)
if (list.isNotEmpty()) {
checkedItem = list[0]
checkedItem!!.photoUri = lastPhotoUri
updateCurrentFood(checkedItem)
lastPhotoUri = null
}
}
}
SensorScaleUtils.startScale { weight ->
Timber.d("registerDataChange weight = $weight")
presentation?.updateWeight(weight)
if (weight - lastWeight > 0.1 && presentation?.mealPickupMode == 0) { // 大于100g
debouncer.debounce {
takePhoto { photoUri ->
lastPhotoUri = photoUri
Timber.d("registerDataChange photoUri = ${photoUri.path}")
// viewModel.getIdentifiedFoodList()
ImageUtil.uriToBitmap(this, photoUri)?.let { bitmap ->
Timber.d("registerDataChange photoUri 拿到bitmap")
val bmp = BitmapCropper.cropCenter(bitmap, 1300, 900)
Timber.d("registerDataChange photoUri bitmap裁剪完成")
val file = BitmapSaver.saveToAppFilesDir(bmp, this, "IMG_CROP_${System.currentTimeMillis()}.jpg")
Timber.d("registerDataChange photoUri bitmap保存文件路径:${file?.absolutePath}")
val resultList = FoodModule.queryFood(bmp)
Timber.d("registerDataChange photoUri 拿到识别数据")
val foodName = resultList.joinToString(separator = ",")
Timber.d("registerDataChange photoUri 识别数据名称:$foodName")
viewModel.getFoodInfo(foodName)
}
}
}
}
lastWeight = weight
}
}
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")
item.photoUri = null
checkedItem = item
adapter.notifyDataSetChanged()
updateCurrentFood(item)
}
}
)
}
private fun setupCamera() {
Timber.d("setupCamera")
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture!!.addListener({
val cameraProvider = cameraProviderFuture!!.get()
// 1. 创建预览用例
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(binding.previewView.surfaceProvider)
}
// 2. 创建图像分析用例(用于副屏显示)
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()
presentation?.updateImage(bitmap) // 更新副屏
}
imageProxy.close() // 必须关闭以释放资源
}
}
// 3. 创建拍照用例
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
// .setTargetResolution(Size(1920, 1080)) // 设置拍照分辨率
.build()
// 4. 选择摄像头
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// 解绑所有用例
cameraProvider.unbindAll()
// 绑定三个用例到生命周期
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageAnalysis, imageCapture
)
} catch (exc: Exception) {
Timber.e(exc.message)
}
}, 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(callback: (Uri) -> Unit) {
Timber.d("takePhoto")
// 临时停止分析以避免干扰
isAnalyzing = false
// 创建输出选项
val executor = ContextCompat.getMainExecutor(this)
val cacheDir = cacheDir
val photoFile = File.createTempFile(
"IMG_${System.currentTimeMillis()}",
".jpg",
cacheDir
)
val cacheOutputOptions =
ImageCapture.OutputFileOptions.Builder(photoFile).build()
// 执行拍照
imageCapture?.takePicture(
cacheOutputOptions,
executor,
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
runOnUiThread {
val savedUri = outputFileResults.savedUri
Timber.d("takePhoto savedUri = $savedUri")
// ToastUtils.showToast("照片保存成功: $savedUri")
if (savedUri != null) {
callback(savedUri)
}
}
// 恢复图像分析
isAnalyzing = true
}
override fun onError(exception: ImageCaptureException) {
Timber.e("拍照失败: ${exception.message}")
runOnUiThread {
ToastUtils.showToast("拍照失败: ${exception.message}")
}
// 恢复图像分析
isAnalyzing = true
}
}
)
}
private fun setupSecondaryDisplay() {
val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val displays = displayManager.displays
// 查找副屏(通常索引为1
if (displays.size > 1) {
val secondaryDisplay = displays[1]
// 创建副屏的Presentation
presentation =
SecondaryScreenPresentation(
activity = this,
this,
display = secondaryDisplay,
viewModel = viewModel,
recognizeViewModel = viewModels<RecognizeViewModel>().value
)
presentation!!.setStepChangeCallback { step ->
if (step == 1) {
updateCurrentFood(null)
bottomSheetDialog?.dismiss()
}
}
presentation!!.show()
} else {
ToastUtils.showToast("获取副屏失败")
}
}
override fun onDestroy() {
SensorScaleUtils.closeScale()
presentation?.dismiss()
super.onDestroy()
shutdownCamera()
}
}