feat(activity): 新增菜品识别功能,重构FoodRecognizeDialog并临时跳过调料配置检查

- 新增 FoodRecognizeDialog(继承 BottomSheetDialog),只初始化一次,暴露 loadData() 方法
- 新增 dialog_food_recognize.xml 布局(标题 + 2列 GridLayoutManager RecyclerView)
- PrepareCookActivity 集成相机初始化、拍照识别流程(重量稳定 ≥10g 时触发)
- PrepareCookActivity 新增 flCameraContainer(1dp 隐藏容器)用于相机预览绑定
- SelectDishActivity 临时注释 judgeDeviceConfig 逻辑,直接调用 block()
- 修正 GpioUtils/L/LightManager 包名为 com.shuwei.dish.match.utils
This commit is contained in:
2026-04-13 17:31:31 +08:00
parent 41036cdbf9
commit 96863e2078
9 changed files with 496 additions and 16 deletions
@@ -41,6 +41,7 @@ class GoodsInfoSearchAdapter(private var list: MutableList<CookFoodGoodsEntity>)
): VH { ): VH {
val binding = val binding =
ListItemSearchGoodsInfoBinding.inflate(LayoutInflater.from(context), parent, false) ListItemSearchGoodsInfoBinding.inflate(LayoutInflater.from(context), parent, false)
return VH(binding) return VH(binding)
} }
@@ -0,0 +1,60 @@
package com.shuwei.dish.match.dialog
import android.annotation.SuppressLint
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.GoodsInfoSearchAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.DialogFoodRecognizeBinding
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
/**
* 菜品识别结果弹窗,继承 BottomSheetDialog 确保只初始化一次
* @param activity 宿主 Activity
* @param onItemSelected 用户点击某一菜品时的回调
*/
class FoodRecognizeDialog(
private val activity: BaseActivity,
private val onItemSelected: (item: CookFoodGoodsEntity) -> Unit
) : BottomSheetDialog(activity, R.style.BottomSheet) {
private val binding = DialogFoodRecognizeBinding.inflate(LayoutInflater.from(activity))
private val list = mutableListOf<CookFoodGoodsEntity>()
private val adapter = GoodsInfoSearchAdapter(list).apply {
setOnItemClickListener { _, _, position ->
list[position].isClicked = true
notifyItemChanged(position)
Handler(Looper.getMainLooper()).postDelayed({
onItemSelected(list[position])
dismiss()
}, 300)
}
}
init {
setContentView(binding.root)
setCancelable(true)
behavior.skipCollapsed = false
setOnDismissListener { activity.hideStatusBar() }
binding.recyclerView.run {
layoutManager = GridLayoutManager(activity, 2, GridLayoutManager.VERTICAL, false)
adapter = this@FoodRecognizeDialog.adapter
}
}
/**
* 加载识别结果数据并显示弹窗
* @param items 识别到的菜品列表
*/
@SuppressLint("NotifyDataSetChanged")
fun loadData(items: List<CookFoodGoodsEntity>) {
list.clear()
list.addAll(items)
adapter.notifyDataSetChanged()
if (!isShowing) show()
}
}
@@ -1,10 +1,11 @@
package com.shuwei.dish.match.ui package com.shuwei.dish.match.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.graphics.Typeface import android.graphics.Typeface
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.activity.viewModels
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.shuwei.dish.match.R import com.shuwei.dish.match.R
@@ -12,23 +13,34 @@ import com.shuwei.dish.match.adapter.DishPartAdapter
import com.shuwei.dish.match.base.BaseActivity import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.ActivityPrepareCookBinding import com.shuwei.dish.match.databinding.ActivityPrepareCookBinding
import com.shuwei.dish.match.databinding.LayoutFoodRemindBinding import com.shuwei.dish.match.databinding.LayoutFoodRemindBinding
import com.shuwei.dish.match.dialog.FoodSearchDialog
import com.shuwei.dish.match.dialog.CommonDialog import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.dialog.FoodRecognizeDialog
import com.shuwei.dish.match.dialog.FoodSearchDialog
import com.shuwei.dish.match.entity.CookFoodEntity import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.CookFoodGoodsEntity import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.entity.FoodRecord import com.shuwei.dish.match.entity.FoodRecord
import com.shuwei.dish.match.net.NetViewModel import android.view.ViewGroup
import androidx.camera.view.PreviewView
import androidx.lifecycle.lifecycleScope
import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding
import com.shuwei.dish.match.objbox.FoodModule
import com.shuwei.dish.match.utils.AddressUtil import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.CameraUtils
import com.shuwei.dish.match.utils.ImageUtil
import com.shuwei.dish.match.utils.SwipeCallback import com.shuwei.dish.match.utils.SwipeCallback
import com.shuwei.dish.match.utils.WeightUtil import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.clickWithDebounce import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.gone import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.roundedDecimalPlace
import com.shuwei.dish.match.utils.ext.startActivity import com.shuwei.dish.match.utils.ext.startActivity
import com.shuwei.dish.match.utils.ext.toJsonString import com.shuwei.dish.match.utils.ext.toJsonString
import com.shuwei.dish.match.utils.ext.toast import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible import com.shuwei.dish.match.utils.ext.visible
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.Serializable import java.io.Serializable
import kotlin.getValue import java.util.concurrent.atomic.AtomicBoolean
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
class PrepareCookActivity : BaseActivity() { class PrepareCookActivity : BaseActivity() {
@@ -40,6 +52,8 @@ class PrepareCookActivity : BaseActivity() {
private lateinit var binding: ActivityPrepareCookBinding private lateinit var binding: ActivityPrepareCookBinding
private val cameraUtils: CameraUtils by lazy { CameraUtils(this) }
private var food: FoodRecord? = null private var food: FoodRecord? = null
private var goodsList: MutableList<CookFoodGoodsEntity>? = null private var goodsList: MutableList<CookFoodGoodsEntity>? = null
@@ -69,6 +83,7 @@ class PrepareCookActivity : BaseActivity() {
binding.tvDishName.text = food?.foodName ?: "" binding.tvDishName.text = food?.foodName ?: ""
addViewClickListener() addViewClickListener()
initCamera(binding.flCameraContainer)
initRecyclerView() initRecyclerView()
getDishDetail() getDishDetail()
@@ -81,6 +96,9 @@ class PrepareCookActivity : BaseActivity() {
//Log.d(TAG, "addViewClickListener: address=$address,state=$state,weight=$weight") //Log.d(TAG, "addViewClickListener: address=$address,state=$state,weight=$weight")
if (address == AddressUtil.ONE) { if (address == AddressUtil.ONE) {
binding.tvDishPartWeight.text = "${weight}" binding.tvDishPartWeight.text = "${weight}"
if (state == WeightUtil.STATE_STABLE) {
recognizeFood(weight)
}
} }
}) })
binding.ivWeightClear.setOnClickListener { binding.ivWeightClear.setOnClickListener {
@@ -134,6 +152,25 @@ class PrepareCookActivity : BaseActivity() {
} }
} }
private val isTakingPhoto = AtomicBoolean(false)
/**
* 重量稳定时触发菜品识别
* @param weight 当前稳定重量
*/
private fun recognizeFood(weight: Double) {
if (weight < 10) {
if (recognizeDialog.isShowing) recognizeDialog.dismiss()
return
}
if (isTakingPhoto.get() || recognizeDialog.isShowing) return
isTakingPhoto.set(true)
cameraUtils.takePhoto(
succCallback = cameraSuccessCallback,
failCallback = cameraFailureCallback
)
}
private fun addFood() { private fun addFood() {
FoodSearchDialog().show(this) { item -> FoodSearchDialog().show(this) { item ->
val filterResult = list.firstOrNull { it.goodsId == item.goodsId } val filterResult = list.firstOrNull { it.goodsId == item.goodsId }
@@ -251,6 +288,16 @@ class PrepareCookActivity : BaseActivity() {
private var firstReqSize = 0 private var firstReqSize = 0
private val list = mutableListOf<CookFoodGoodsEntity>() private val list = mutableListOf<CookFoodGoodsEntity>()
private val recognizeDialog by lazy {
FoodRecognizeDialog(this) { item ->
// TODO: 处理识别结果选中
}
.apply {
setOnDismissListener {
isTakingPhoto.set(false)
}
}
}
private val dishPartAdapter by lazy { private val dishPartAdapter by lazy {
DishPartAdapter(list).apply { DishPartAdapter(list).apply {
setOnItemClickListener { _, _, positon -> setOnItemClickListener { _, _, positon ->
@@ -326,6 +373,26 @@ class PrepareCookActivity : BaseActivity() {
} }
} }
override fun onResume() {
super.onResume()
cameraUtils.bind()
}
override fun onPause() {
super.onPause()
cameraUtils.unbind()
}
/**
* 初始化相机并绑定预览容器
* @param container 相机预览容器
*/
fun initCamera(container: ViewGroup) {
cameraUtils.initCamera()
val previewBinding = LayoutCameraPreviewBinding.inflate(layoutInflater, container)
cameraUtils.setPreviewController(previewBinding.previewView)
}
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@SuppressLint("GestureBackNavigation") @SuppressLint("GestureBackNavigation")
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
@@ -367,4 +434,49 @@ class PrepareCookActivity : BaseActivity() {
}.show() }.show()
} }
private val cameraSuccessCallback: (Uri) -> Unit = { uri ->
Log.d(TAG, "takePhoto success")
lifecycleScope.launch {
withContext(Dispatchers.IO) {
val bitmap = ImageUtil.uriToBitmap(this@PrepareCookActivity, uri)
if (bitmap == null) {
Log.d(TAG, "takePhoto bitmap is null")
isTakingPhoto.set(false)
return@withContext
}
val foodScoreList = FoodModule.getFoodScoreList(bitmap)
if (foodScoreList.isEmpty()) {
Log.d(TAG, "takePhoto foodScoreList is empty")
isTakingPhoto.set(false)
return@withContext
}
withContext(Dispatchers.Main) {
queryFood(foodScoreList)
}
}
}
}
private val cameraFailureCallback: (String) -> Unit = { errMsg ->
Log.d(TAG, "takePhoto failure")
isTakingPhoto.set(false)
}
private fun queryFood(list: List<FoodModule.IdNameScore>) {
Log.d(TAG, "takePhoto queryFood, list=${list.toString()}")
//调用接口成功,返回食材列表设置isTakingPhoto.set(false),暂时写死数据
val foodList = mutableListOf<CookFoodGoodsEntity>()
list.forEachIndexed { index, food ->
val foodName = food.name.split("WP").first()
val foodScore = ((1-food.score) * 100).roundedDecimalPlace(2)
foodList.add(CookFoodGoodsEntity(
goodsId = index.toString(),
goodsName = "${foodName}-${foodScore}%",
foodId = index.toString()
))
}
recognizeDialog.loadData(foodList)
isTakingPhoto.set(false)
}
} }
@@ -82,18 +82,19 @@ class SelectDishActivity : BaseActivity() {
private var configFinished = false private var configFinished = false
fun judgeDeviceConfig(block: () -> Unit) { fun judgeDeviceConfig(block: () -> Unit) {
if (configFinished) { // if (configFinished) {
block() // block()
return // return
} // }
appViewModel.loadSeasoning { // appViewModel.loadSeasoning {
if (it.isEmpty()) { // if (it.isEmpty()) {
showDeviceConfigDialog() // showDeviceConfigDialog()
return@loadSeasoning // return@loadSeasoning
} // }
configFinished = true // configFinished = true
block() // block()
} // }
block()
} }
private fun showDeviceConfigDialog() { private fun showDeviceConfigDialog() {
@@ -0,0 +1,175 @@
package com.shuwei.dish.match.utils;
import android.os.SystemClock;
import android.util.Log;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
/**
* Created by Administrator on 2018/11/9.
*/
public class GpioUtils {
private static final String TAG = "GpioUtils";
/*
给export申请权限
*/
public static void upgradeRootPermissionForExport() {
upgradeRootPermission("/sys/class/gpio/export");
}
/*
配置一个gpio路径
*/
public static boolean exportGpio(int gpio) {
return writeNode("/sys/class/gpio/export", "" + gpio);
}
/*
给获取io口的状态的路径申请权限,该方法需要在exportGpio后调用
*/
public static void upgradeRootPermissionForGpio(int gpio) {
upgradeRootPermission("/sys/class/gpio/gpio" + gpio + "/direction");
upgradeRootPermission("/sys/class/gpio/gpio" + gpio + "/value");
}
/*
设置io口为输入或输出
*/
public static boolean setGpioDirection(int gpio, int arg) {
String gpioDirection = "";
if (arg == 0) gpioDirection = "out";
else if (arg == 1) gpioDirection = "in";
else return false;
return writeNode("/sys/class/gpio/gpio" + gpio + "/direction", gpioDirection);
}
/*
获取io口的状态为输出还是输入
*/
public static String getGpioDirection(int gpio) {
String gpioDirection = "";
gpioDirection = readNode("/sys/class/gpio/gpio" + gpio + "/direction");
return gpioDirection;
}
/*
给输出io口写值,高电平或低电平
*/
public static boolean writeGpioValue(int gpio, String arg) {
return writeNode("/sys/class/gpio/gpio" + gpio + "/value", arg);
}
//获取当前gpio是高还是低
public static String getGpioValue(int gpio) {
return readNode("/sys/class/gpio/gpio" + gpio + "/value");
}
private static boolean upgradeRootPermission(String path) {
Process process = null;
DataOutputStream os = null;
try {
String cmd = "chmod 777 " + path;
process = Runtime.getRuntime().exec("su"); //切换到root帐号
os = new DataOutputStream(process.getOutputStream());
os.writeBytes(cmd + "\n");
os.writeBytes("exit\n");
os.flush();
process.waitFor();
} catch (Exception e) {
} finally {
try {
if (os != null) {
os.close();
}
process.destroy();
} catch (Exception e) {
}
}
try {
return process.waitFor() == 0;
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
private static boolean writeNode(String path, String arg) {
Log.d(TAG, "Gpio_test set node path: " + path + " arg: " + arg);
if (path == null || arg == null) {
Log.e(TAG, "set node error");
return false;
}
FileWriter fileWriter = null;
BufferedWriter bufferedWriter = null;
try {
fileWriter = new FileWriter(path);
fileWriter.write(arg);
} catch (Exception e) {
Log.e(TAG, "Gpio_test write node error!! path" + path + " arg: " + arg);
e.printStackTrace();
return false;
} finally {
try {
if (fileWriter != null) {
fileWriter.close();
}
if (bufferedWriter != null) {
bufferedWriter.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
private static long mTime = 0;
private static int mFailTimes = 0;
private static String readNode(String path) {
String result = "";
FileReader fread = null;
BufferedReader buffer = null;
try {
fread = new FileReader(path);
buffer = new BufferedReader(fread);
String str = null;
while ((str = buffer.readLine()) != null) {
result = str;
break;
}
mFailTimes = 0;
} catch (IOException e) {
Log.e(TAG, "IO Exception");
e.printStackTrace();
if (mTime == 0 || SystemClock.uptimeMillis() - mTime < 1000) {
mFailTimes++;
}
if (mFailTimes >= 3) {
Log.d(TAG, "read format node continuous failed three times, exist thread");
}
} finally {
try {
if (buffer != null) {
buffer.close();
}
if (fread != null) {
fread.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
}
@@ -0,0 +1,57 @@
package com.shuwei.dish.match.utils;
/**
* Log统一管理类
*/
public class L {
private L() {
/* cannot be instantiated */
throw new UnsupportedOperationException("cannot be instantiated");
}
public static boolean isDebug = true;// 是否需要打印bug,可以在application的onCreate函数里面初始化
private static final String TAG = "mzf";
// 下面四个是默认tag的函数
public static void i(String msg) {
if (isDebug)
android.util.Log.i(TAG, msg);
}
public static void d(String msg) {
if (isDebug)
android.util.Log.d(TAG, msg);
}
public static void e(String msg) {
if (isDebug)
android.util.Log.e(TAG, msg);
}
public static void v(String msg) {
if (isDebug)
android.util.Log.v(TAG, msg);
}
// 下面是传入自定义tag的函数
public static void i(String tag, String msg) {
if (isDebug)
android.util.Log.i(tag, msg);
}
public static void d(String tag, String msg) {
if (isDebug)
android.util.Log.d(tag, msg);
}
public static void e(String tag, String msg) {
if (isDebug)
android.util.Log.e(tag, msg);
}
public static void v(String tag, String msg) {
if (isDebug)
android.util.Log.v(tag, msg);
}
}
@@ -0,0 +1,25 @@
package com.shuwei.dish.match.utils;
public class LightManager {
private static final String TAG = "LightManager";
public static void openGreenLight() {
L.e(TAG, "openGreenLight");
GpioUtils.writeGpioValue(41, "1");
}
public static void closeGreenLight() {
L.e(TAG, "closeGreenLight");
GpioUtils.writeGpioValue(41, "0");
}
public static void openRedLight() {
L.e(TAG, "openRedLight");
GpioUtils.writeGpioValue(40, "1");
}
public static void closeRedLight() {
L.e(TAG, "closeRedLight");
GpioUtils.writeGpioValue(40, "0");
}
}
@@ -262,6 +262,11 @@
</LinearLayout> </LinearLayout>
<FrameLayout
android:id="@+id/flCameraContainer"
android:layout_width="1dp"
android:layout_height="1dp" />
<androidx.appcompat.widget.AppCompatButton <androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCook" android:id="@+id/btnCook"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -0,0 +1,44 @@
<?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:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:background="@color/white">
<View
android:id="@+id/viewLine"
android:layout_width="90dp"
android:layout_height="10dp"
android:layout_marginTop="30dp"
android:background="@drawable/shape_gray_dc_5" />
<TextView
android:id="@+id/tvSheetName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:text="菜品识别结果"
android:textColor="@color/black333"
android:textSize="36sp"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="45dp"
android:layout_marginTop="15dp"
android:layout_marginEnd="45dp"
android:layout_marginBottom="15dp"
android:minHeight="500dp"
android:nestedScrollingEnabled="true"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:itemCount="10"
tools:listitem="@layout/list_item_search_goods_info" />
</LinearLayout>