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 {
val binding =
ListItemSearchGoodsInfoBinding.inflate(LayoutInflater.from(context), parent, false)
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
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.graphics.Typeface
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.activity.viewModels
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
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.databinding.ActivityPrepareCookBinding
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.FoodRecognizeDialog
import com.shuwei.dish.match.dialog.FoodSearchDialog
import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
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.CameraUtils
import com.shuwei.dish.match.utils.ImageUtil
import com.shuwei.dish.match.utils.SwipeCallback
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.clickWithDebounce
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.toJsonString
import com.shuwei.dish.match.utils.ext.toast
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 kotlin.getValue
import java.util.concurrent.atomic.AtomicBoolean
@SuppressLint("NotifyDataSetChanged")
class PrepareCookActivity : BaseActivity() {
@@ -40,6 +52,8 @@ class PrepareCookActivity : BaseActivity() {
private lateinit var binding: ActivityPrepareCookBinding
private val cameraUtils: CameraUtils by lazy { CameraUtils(this) }
private var food: FoodRecord? = null
private var goodsList: MutableList<CookFoodGoodsEntity>? = null
@@ -69,6 +83,7 @@ class PrepareCookActivity : BaseActivity() {
binding.tvDishName.text = food?.foodName ?: ""
addViewClickListener()
initCamera(binding.flCameraContainer)
initRecyclerView()
getDishDetail()
@@ -81,6 +96,9 @@ class PrepareCookActivity : BaseActivity() {
//Log.d(TAG, "addViewClickListener: address=$address,state=$state,weight=$weight")
if (address == AddressUtil.ONE) {
binding.tvDishPartWeight.text = "${weight}"
if (state == WeightUtil.STATE_STABLE) {
recognizeFood(weight)
}
}
})
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() {
FoodSearchDialog().show(this) { item ->
val filterResult = list.firstOrNull { it.goodsId == item.goodsId }
@@ -251,6 +288,16 @@ class PrepareCookActivity : BaseActivity() {
private var firstReqSize = 0
private val list = mutableListOf<CookFoodGoodsEntity>()
private val recognizeDialog by lazy {
FoodRecognizeDialog(this) { item ->
// TODO: 处理识别结果选中
}
.apply {
setOnDismissListener {
isTakingPhoto.set(false)
}
}
}
private val dishPartAdapter by lazy {
DishPartAdapter(list).apply {
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")
@SuppressLint("GestureBackNavigation")
@Deprecated("Deprecated in Java")
@@ -367,4 +434,49 @@ class PrepareCookActivity : BaseActivity() {
}.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
fun judgeDeviceConfig(block: () -> Unit) {
if (configFinished) {
block()
return
}
appViewModel.loadSeasoning {
if (it.isEmpty()) {
showDeviceConfigDialog()
return@loadSeasoning
}
configFinished = true
block()
}
// if (configFinished) {
// block()
// return
// }
// appViewModel.loadSeasoning {
// if (it.isEmpty()) {
// showDeviceConfigDialog()
// return@loadSeasoning
// }
// configFinished = true
// block()
// }
block()
}
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>
<FrameLayout
android:id="@+id/flCameraContainer"
android:layout_width="1dp"
android:layout_height="1dp" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCook"
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>