374 lines
14 KiB
Kotlin
374 lines
14 KiB
Kotlin
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 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.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.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 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
|
||
private var canIdentify: Boolean = false
|
||
private var bottomSheetDialog: CustomBottomSheetDialog? = null
|
||
private var lastWeight: Double = 0.0
|
||
|
||
override fun getViewModel(): BaseViewModel {
|
||
return viewModel
|
||
}
|
||
|
||
override fun inflateViewBinding(): ActivityMainBinding {
|
||
return ActivityMainBinding.inflate(layoutInflater)
|
||
}
|
||
|
||
override fun initialize() {
|
||
super.initialize()
|
||
initView()
|
||
setupSecondaryDisplay()
|
||
setupCamera()
|
||
initData()
|
||
}
|
||
|
||
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) {
|
||
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 {
|
||
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.identifiedFoodInfoList.collect { list ->
|
||
binding.recyclerview
|
||
adapter.updateData(list)
|
||
// presentation.step2()
|
||
}
|
||
}
|
||
SensorScaleUtils.startScale { weight ->
|
||
Timber.d("registerDataChange weight = $weight")
|
||
presentation.updateWeight(weight)
|
||
if (weight > 0.0) {
|
||
if (canIdentify) {
|
||
canIdentify = false
|
||
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() {
|
||
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()
|
||
}
|
||
}
|
||
|
||
override fun onDestroy() {
|
||
SensorScaleUtils.closeScale()
|
||
presentation.dismiss()
|
||
super.onDestroy()
|
||
}
|
||
} |