feat(activity): 优化食材识别重量逻辑,修复主辅材重复保存问题

- PrepareFoodActivity:改为以首次秤读数为基准计算净重,注释清零按钮和测试按钮,修复 foodSelectCallback 中 isClicked 赋值对象错误导致选中状态异常的 bug,修复 getGoodsList 重复调用时主辅材累加的问题,向 FoodRecognizeActivity 传递 initialWeight
- FoodRecognizeActivity:启用秤重量监听,改为净重显示;用滑动窗口(最近10帧)替换延迟更新逻辑,关闭页面时从窗口中提取连续稳定值作为 useWeight,数据不稳定时弹窗提示而非直接关闭
- DbViewModel:saveCookFoodAndGoods 首次插入后回填 entity.id,防止重复进入时主辅材重复插入
- InitActivity:注释启动时自动清零秤的逻辑
- SubmitFoodActivity:将魔法数字 0.5 提取为常量 WEIGHT_THRESHOLD
This commit is contained in:
2026-05-13 16:01:28 +08:00
parent 65958bf637
commit ce7d2da6d8
5 changed files with 125 additions and 73 deletions
@@ -73,6 +73,8 @@ class DbViewModel : ViewModel() {
// 首次保存:主表 + 主辅材 + 调料 全量 insert
rep.insertCookFood(entity)
val newData = rep.getCookFoodById(entity.foodId, cookMode, entity.dinnerType ?: "0")
// 回填 id,防止下次调用时 getCookFoodById 查不到而重复插入主辅材
newData?.let { entity.id = it.id }
list?.let {
it.forEach { goods ->
goods.id = 0
@@ -10,10 +10,12 @@ import androidx.recyclerview.widget.RecyclerView
import com.shuwei.dish.match.adapter.FoodAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.ActivityFoodRecognizeBinding
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.dialog.FoodSearchDialog
import com.shuwei.dish.match.entity.GoodsItem
import com.shuwei.dish.match.entity.GoodsNameQueryDTO
import com.shuwei.dish.match.utils.ActivityManager
import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.gone
@@ -38,6 +40,7 @@ class FoodRecognizeActivity : BaseActivity() {
private const val TAG = "FoodRecognizeActivity"
private const val EXTRA_IMAGE_URI = "extra_image_uri"
private const val EXTRA_CURRENT_WEIGHT = "extra_current_weight"
private const val EXTRA_INITIAL_WEIGHT = "extra_initial_weight"
const val EXTRA_SELECTED_ITEM = "extra_selected_item"
const val IS_MANUAL_CANCEL = "isManualCancel"
@@ -54,6 +57,7 @@ class FoodRecognizeActivity : BaseActivity() {
activity: BaseActivity,
imageUri: String? = null,
currentWeight: Double = 0.0,
initialWeight: Double = 0.0,
nameScoreList: ArrayList<FoodModule.IdNameScore> = arrayListOf(),
finishCallback: (Boolean, GoodsItem?) -> Unit = { _, _ -> }
) {
@@ -61,6 +65,7 @@ class FoodRecognizeActivity : BaseActivity() {
val launchIntent = Intent(activity, FoodRecognizeActivity::class.java).apply {
putExtra(EXTRA_IMAGE_URI, imageUri)
putExtra(EXTRA_CURRENT_WEIGHT, currentWeight)
putExtra(EXTRA_INITIAL_WEIGHT, initialWeight)
}
activity.startActivity(launchIntent) { resultIntent ->
val isManualCancel = resultIntent?.getBooleanExtra(IS_MANUAL_CANCEL, false) ?: false
@@ -94,14 +99,17 @@ class FoodRecognizeActivity : BaseActivity() {
/** 当前选中的列表位置,-1 表示无选中 */
private var selectedPosition = -1
/** 秤1当前实时重量 */
/** 秤1当前实时净重(已减去初始读数) */
private var currentWeight = 0.0
/** 重量减少时延迟更新的任务,页面关闭前取消可防止用错误重量 */
private var pendingWeightRunnable: Runnable? = null
/** 初始秤读数,净重 = 秤读数 - initialWeight */
private var initialWeight = 0.0
/** 当前待延迟更新的目标重量,避免相同重量重复重置计时器 */
private var pendingWeight = 0.0
/**
* 滑动窗口:保存最近 10 条稳定帧净重,用于关闭页面时判断数据是否稳定
* 超过 10 条时移除队头,保持最新数据
*/
private val weightWindow = ArrayDeque<Double>(10)
/** 右侧自定义滚动指示条最小高度 */
private val scrollIndicatorMinHeightPx by lazy { (24 * resources.displayMetrics.density).toInt() }
@@ -199,6 +207,7 @@ class FoodRecognizeActivity : BaseActivity() {
binding.ivFoodPhoto.setImageURI(imageUri.toUri())
}
currentWeight = intent.getDoubleExtra(EXTRA_CURRENT_WEIGHT, 0.0)
initialWeight = intent.getDoubleExtra(EXTRA_INITIAL_WEIGHT, 0.0)
binding.tvWeight.text = "${currentWeight}g"
// val tempList = arrayListOf<GoodsItem>()
// Process recognized name list if available
@@ -436,58 +445,82 @@ class FoodRecognizeActivity : BaseActivity() {
* 低于阈值说明食材已被取走,保留上次有效值等待页面关闭
*/
private fun registerWeightListener() {
// WeightUtil.addWeightListener(TAG) { address, state, weight ->
// if (address == AddressUtil.ONE && state == WeightUtil.STATE_STABLE) {
// runOnUiThread {
// currentWeight = weight
// binding.tvWeight.run {
// if (tag != weight) {
// text = "$weight g"
// tag = weight
// }
// }
// if (weight >= PrepareCookActivity.WEIGHT_RECOGNIZE_VALUE) {
// adapter.items.firstOrNull { it.isClicked }?.let { item ->
// if (pendingWeight != weight) {
// // 无论增减,均延迟更新,避免手拿物品时的瞬时增重干扰
// cancelPendingWeightUpdate()
// pendingWeight = weight
// pendingWeightRunnable = Runnable {
// item.useWeight = weight
// pendingWeightRunnable = null
// }.also { handler.postDelayed(it, 3000) }
// }
// }
// }
// }
// }
// }
WeightUtil.addWeightListener(TAG) { address, state, weight ->
if (address == AddressUtil.ONE && state == WeightUtil.STATE_STABLE) {
runOnUiThread {
// 净重 = 秤读数 - 初始读数
val netWeight = weight - initialWeight
currentWeight = netWeight
binding.tvWeight.run {
if (tag != netWeight) {
text = "${netWeight}g"
tag = netWeight
}
}
// 入队,超过 10 条移除队头
if (weightWindow.size >= 10) weightWindow.removeFirst()
weightWindow.addLast(netWeight)
// 实时更新选中食材重量
list.getOrNull(selectedPosition)?.useWeight = netWeight
}
}
}
}
/**
* 取消待执行的延迟重量更新任务
* 从滑动窗口尾部向前查找连续稳定值:
* 跳过尾部不稳定的过渡帧,找到第一段连续相同且大于阈值的序列
* 连续条数 >= 3 时返回该值,否则返回 null(数据不稳定)
*/
private fun cancelPendingWeightUpdate() {
// pendingWeightRunnable?.let { handler.removeCallbacks(it) }
// pendingWeightRunnable = null
private fun getStableWeightFromWindow(): Double? {
val threshold = PrepareFoodActivity.WEIGHT_RECOGNIZE_VALUE
var i = weightWindow.indices.last
while (i >= 0) {
val candidate = weightWindow[i]
// 跳过低于阈值的过渡帧
if (candidate <= threshold) {
i--
continue
}
// 以 candidate 为基准,向前统计连续相同条数
var count = 0
var j = i
while (j >= 0 && weightWindow[j] == candidate) {
count++
j--
}
if (count >= 3) return candidate
// 不足 3 条,继续向前找
i = j
}
return null
}
/**
* 携带当前选中食材关闭页面
* @param isManualCancel true=用户手动返回;false=被动关闭(重量低于阈值)
*/
private fun finishWithResult(isManualCancel: Boolean) {
cancelPendingWeightUpdate()
val item = list.getOrNull(selectedPosition)
if (!isManualCancel && item != null) {
// 被动关闭:从窗口取稳定值
val stableWeight = getStableWeightFromWindow()
if (stableWeight == null) {
// 数据不稳定,不关闭页面,弹窗提示
CommonDialog(this)
.setTitle("提示")
.setContent("数据不稳定,请重新放置食材后再取走")
.setPositiveButton("确认")
.show()
return
}
item.useWeight = stableWeight
}
val intent = Intent().apply {
putExtra(IS_MANUAL_CANCEL, isManualCancel)
if (isManualCancel.not()) {
list.getOrNull(selectedPosition)?.let {
// 兜底:若 useWeight 仍为空或0,使用当前实时重量
if ((it.useWeight ?: 0.0) <= 0.0) {
it.useWeight = currentWeight
}
putExtra(EXTRA_SELECTED_ITEM, it)
}
if (!isManualCancel) {
item?.let { putExtra(EXTRA_SELECTED_ITEM, it) }
}
}
setResult(RESULT_OK, intent)
@@ -496,7 +529,6 @@ class FoodRecognizeActivity : BaseActivity() {
override fun onDestroy() {
super.onDestroy()
cancelPendingWeightUpdate()
WeightUtil.removeWeightListener(TAG)
}
}
@@ -86,8 +86,8 @@ class InitActivity : BaseActivity() {
WeightUtil.getWeight()
WeightUtil.startContinuousRead()
//对净材秤1和熟食秤2称进行清零
WeightUtil.tareTwo(AddressUtil.ONE)
WeightUtil.tareTwo(AddressUtil.TWO)
//WeightUtil.tareTwo(AddressUtil.ONE)
//WeightUtil.tareTwo(AddressUtil.TWO)
initViews()
}
@@ -124,34 +124,41 @@ class PrepareFoodActivity : BaseActivity() {
}
private fun addViewClickListener() {
binding.tvShowWeight.setOnClickListener {
showLoading("正在清零……")
WeightUtil.tareTwo(AddressUtil.ONE)
delayDismissLoading()
}
// TODO: 测试功能-------------------------------
binding.btnTestConfirm.clickWithDebounce {
if (currentWeight <= 0.0) return@clickWithDebounce
val (index, item) = list.withIndex().find { (_, it) -> it.isOriginalData && it.isClicked && (it.useWeight?:0.0) <= 0.0 } ?: return@clickWithDebounce
item.useWeight = currentWeight
item.isSetFinished = true
list.forEachIndexed { i, it -> it.isClicked = i == index }
materialAdapter.notifyDataSetChanged()
}
// TODO: 测试功能-------------------------------
// binding.tvShowWeight.setOnClickListener {
// showLoading("正在清零……")
// WeightUtil.tareTwo(AddressUtil.ONE)
// delayDismissLoading()
// }
// // TODO: 测试功能-------------------------------
// binding.btnTestConfirm.clickWithDebounce {
// if (currentWeight <= 0.0) return@clickWithDebounce
// val (index, item) = list.withIndex().find { (_, it) -> it.isOriginalData && it.isClicked && (it.useWeight?:0.0) <= 0.0 } ?: return@clickWithDebounce
// item.useWeight = currentWeight
// item.isSetFinished = true
// list.forEachIndexed { i, it -> it.isClicked = i == index }
// materialAdapter.notifyDataSetChanged()
// }
// // TODO: 测试功能-------------------------------
//binding.root.setOnClickListener { hideKeyboard() }
WeightUtil.addWeightListener(
weightKey = TAG,
getWeight = { address, state, weight ->
if (address == AddressUtil.ONE && state == WeightUtil.STATE_STABLE) {
runOnUiThread {
// 首次收到稳定读数时,将其作为初始值(去皮基准)
if (!initialWeightReady) {
initialWeight = weight
initialWeightReady = true
}
// 净重 = 秤读数 - 初始读数
val netWeight = weight - initialWeight
binding.tvShowWeight.run {
if (tag != weight) {
text = "称重:${weight}g"
tag = weight
if (tag != netWeight) {
text = "称重:${netWeight}g"
tag = netWeight
}
}
recognizeFood(weight)
recognizeFood(netWeight)
}
}
})
@@ -216,6 +223,12 @@ class PrepareFoodActivity : BaseActivity() {
private var lastWeight = 0.0
private var currentWeight = 0.0
/** 初始秤读数,净重 = 秤读数 - initialWeight */
private var initialWeight = 0.0
/** 初始读数是否已从秤获取 */
private var initialWeightReady = false
/**
* 重量稳定时触发菜品识别
* @param weight 当前稳定重量
@@ -257,11 +270,11 @@ class PrepareFoodActivity : BaseActivity() {
val filterValue = list.withIndex().find { (_, it) -> it.goodsId == entity.goodsId }
if (filterValue != null) {
val index = filterValue.index
val item = filterValue.value
item.useWeight = (item.useWeight ?: 0.0) + (entity.useWeight ?: 0.0)
item.isSetFinished = true
val goods = filterValue.value
goods.useWeight = (goods.useWeight ?: 0.0) + (entity.useWeight ?: 0.0)
goods.isSetFinished = true
list.forEachIndexed { i, it ->
item.isClicked = i == index
it.isClicked = i == index
}
} else {
list.forEach { it.isClicked = false }
@@ -325,6 +338,8 @@ class PrepareFoodActivity : BaseActivity() {
private fun getGoodsList() {
if (goodsList == null) {
goodsList = mutableListOf()
} else {
goodsList!!.clear()
}
// 将 GoodsItem 转换为 CookFoodGoodsEntity 并补充菜品相关字段
list.filter { it.isSetFinished }.forEach {
@@ -629,6 +644,7 @@ class PrepareFoodActivity : BaseActivity() {
activity = this,
imageUri = lastPhotoUri?.toString(),
currentWeight = currentWeight,
initialWeight = initialWeight,
nameScoreList = ArrayList(nameScoreList),
finishCallback = { isManualCancel, entity ->
Log.d(
@@ -49,6 +49,8 @@ class SubmitFoodActivity : BaseActivity() {
/** 本地临时 foodId 前缀;以此开头说明菜品尚未在服务器登记,提交时需清空让服务器分配真实 ID */
const val LOCAL_ID_PREFIX = "localId_"
const val WEIGHT_THRESHOLD = 0.01
}
private lateinit var binding: ActivitySubmitFoodBinding
@@ -247,7 +249,7 @@ class SubmitFoodActivity : BaseActivity() {
.coerceAtLeast(0.0)
.roundedOneDecimalPlace()
if (useWeight < 0.5) {
if (useWeight < WEIGHT_THRESHOLD) {
// 放回场景:必须稳定后才移除,避免拿取过程中误删
if (data.state != WeightUtil.STATE_STABLE) return@forEach
if (rawWeightMap.remove(key) != null) {
@@ -279,8 +281,8 @@ class SubmitFoodActivity : BaseActivity() {
val (baseGoodsId, baseWeight) = baseSeasoningMap[goodsName] ?: ("" to 0.0)
val totalWeight = (newWeight + baseWeight).roundedOneDecimalPlace()
Log.d(TAG, "refreshAdapterByName, goodsName=$goodsName, newWeight=$newWeight, baseWeight=$baseWeight, totalWeight=$totalWeight, isCooking=${food?.isCooking}")
if (totalWeight < 0.5) {
Log.d(TAG, "refreshAdapterByName,重量小于0.5g, isCooking=${food?.isCooking}, goodsName=$goodsName, totalWeight=$totalWeight")
if (totalWeight < WEIGHT_THRESHOLD) {
Log.d(TAG, "refreshAdapterByName,重量小于${WEIGHT_THRESHOLD}g, isCooking=${food?.isCooking}, goodsName=$goodsName, totalWeight=$totalWeight")
// 仅保护 baseSeasoningMap 中的历史调料;本次会话新加的调料可以移除
if (!baseSeasoningMap.containsKey(goodsName)) {
// 只有 item 确实存在时才调用 remove,避免无效遍历