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() { private val viewModel by viewModels() private var imageCapture: ImageCapture? = null private var isAnalyzing = true // 控制是否进行图像分析 private lateinit var presentation: SecondaryScreenPresentation private val executor = Executors.newSingleThreadExecutor() private lateinit var adapter: GenericItemAdapter private var checkedItem: FoodInfo? = null private var imageAnalysis: ImageAnalysis? = null private var cameraProviderFuture: ListenableFuture? = 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( 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 { 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().value ) presentation.setStepChangeCallback { step -> if (step == 1) { updateCurrentFood(null) bottomSheetDialog?.dismiss() } } presentation.show() } } override fun onDestroy() { SensorScaleUtils.closeScale() presentation.dismiss() super.onDestroy() } }