feat(activity): 重构 SubmitFoodActivity 调料展示,改用子设备实时秤数据

This commit is contained in:
2026-04-21 17:08:34 +08:00
parent 07eb626e12
commit 0a90a8b368
4 changed files with 239 additions and 268 deletions
@@ -0,0 +1,72 @@
package com.shuwei.dish.match.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.shuwei.dish.match.databinding.ItemSeasoningWeightBinding
/**
* 调料使用重量列表 Adapter
* 用于 SubmitFoodActivity 展示各调料槽位的实时用量
*
* @param items 调料数据列表,外部直接操作后调用 notifyDataSetChanged
*/
class SeasoningWeightAdapter : RecyclerView.Adapter<SeasoningWeightAdapter.VH>() {
/** 调料展示数据项 */
data class Item(
/** 唯一标识:deviceId#address */
val key: String,
val goodsId: String,
val goodsName: String,
/** 使用重量(克),= 初始重量 - 当前重量,≥0 */
val useWeight: Double
)
val items = mutableListOf<Item>()
/**
* 更新单个 item 的重量,若 key 不存在则追加
*/
fun updateItem(item: Item) {
val idx = items.indexOfFirst { it.key == item.key }
if (idx >= 0) {
items[idx] = item
notifyItemChanged(idx)
} else {
items.add(item)
notifyItemInserted(items.lastIndex)
}
}
/**
* 移除指定 key 的 item(用量不足阈值时调用)
*/
fun removeItem(key: String) {
val idx = items.indexOfFirst { it.key == key }
if (idx >= 0) {
items.removeAt(idx)
notifyItemRemoved(idx)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val binding = ItemSeasoningWeightBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.size
class VH(private val b: ItemSeasoningWeightBinding) : RecyclerView.ViewHolder(b.root) {
fun bind(item: Item) {
b.tvSeasoningName.text = item.goodsName
b.tvSeasoningWeight.text = "${item.useWeight}g"
}
}
}
@@ -3,25 +3,22 @@ package com.shuwei.dish.match.ui
import android.annotation.SuppressLint
import android.os.Bundle
import android.util.Log
import android.util.SparseArray
import android.util.SparseIntArray
import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.core.util.forEach
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.google.gson.reflect.TypeToken
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.TextCellAdapter
import com.shuwei.dish.match.adapter.SeasoningWeightAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.ActivitySubmitFoodBinding
import com.shuwei.dish.match.db.AppRepository
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.entity.SeasoningEntity
import com.shuwei.dish.match.net.NetViewModel
import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.scale.ScaleServiceManager
import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.clickWithDebounce
@@ -34,7 +31,10 @@ import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
import com.shuwei.dish.match.viewmodel.AppViewModel
import com.shuwei.dish.match.viewmodel.factory.AppFactory
import kotlin.getValue
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@SuppressLint("UseSparseArrays")
class SubmitFoodActivity : BaseActivity() {
@@ -50,8 +50,6 @@ class SubmitFoodActivity : BaseActivity() {
private var food: FoodRecord? = null
private var goodsList: MutableList<CookFoodGoodsEntity>? = null
private var isCooking = false
private val cookFoodEntity by lazy {
CookFoodEntity().apply {
food?.let {
@@ -64,6 +62,21 @@ class SubmitFoodActivity : BaseActivity() {
}
}
/** 调料列表 adapter */
private val seasoningAdapter = SeasoningWeightAdapter()
/**
* 各秤的初始重量快照,key = "deviceId#address"
* 首次收到稳定数据时记录,之后用于计算用量差值
*/
private val baselineMap = mutableMapOf<String, Double>()
/**
* 槽位配置:key = "deviceId#address"value = goodsId to goodsName
* 从 Room dm_seasoning_slot 加载
*/
private val slotMap = mutableMapOf<String, Pair<String, String>>()
@Suppress("unchecked_cast", "DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -74,14 +87,8 @@ class SubmitFoodActivity : BaseActivity() {
intent.extras?.apply {
food = getSerializable(FOOD_ITEM) as FoodRecord?
Log.d(TAG, "onCreate: cookMode=${food?.cookMode}")
isCooking = food?.isCooking ?: false
if (isCooking) {
//烹饪中,从本地查询数据
loadSeasoningFromLocal()
} else {
//非烹饪中,从菜品主辅材配置页面带入数据
goodsList = getSerializable(GOODS_LIST) as MutableList<CookFoodGoodsEntity>?
}
// 非烹饪中,从菜品主辅材配置页面带入数据
goodsList = getSerializable(GOODS_LIST) as MutableList<CookFoodGoodsEntity>?
}
setTitleBar(titleBarAction = {
it.visible()
@@ -90,80 +97,91 @@ class SubmitFoodActivity : BaseActivity() {
}, rightIconActon = {
it.gone()
it.setImageResource(R.drawable.ic_setting)
it.setOnClickListener {
//startActivity<DeviceSettingActivity>()
}
})
setupRecyclerView()
addViewListener()
loadSlotsThenObserveScales()
}
private fun loadSeasoningFromLocal() {
appViewModel.getCookFoodGoodsList(
foodId = food!!.foodId!!,
cookMode = food!!.cookMode
) { it ->
goodsList = mutableListOf()
val goodsIdList = mutableListOf<String>()
//设置主辅材数据
val tempList = it.filter { goods -> goods.materialType != 3 }
tempList.forEach {
if (!goodsIdList.contains(it.goodsId)) {
goodsList?.add(it)
goodsIdList.add(it.goodsId)
}
}
/** 初始化 RecyclerView2列 GridLayoutManager */
private fun setupRecyclerView() {
binding.rvSeasoning.itemAnimator = null
binding.rvSeasoning.layoutManager = GridLayoutManager(this, 2)
binding.rvSeasoning.adapter = seasoningAdapter
}
//获取调料数据并展示
val filterList: List<SeasoningEntity>? = it.filter { goods -> goods.materialType == 3 }
.map { entity ->
SeasoningEntity().also { se ->
se.goodsId = entity.goodsId
se.goodsName = entity.goodsName
se.goodsOrRelationCode = entity.goodsOrRelationCode
se.relateionType = entity.relateionType
se.materialType = entity.materialType
se.allEdible = entity.allEdible
se.useWeight = entity.useWeight
se.popularName = entity.popularName
se.canteenId = entity.canteenId
se.relateionType_dictText = entity.relateionType_dictText
se.foodId = entity.foodId
se.sort = entity.sort
se.pageType = 0
}
/**
* 从 Room 加载槽位配置后,开始订阅子设备秤数据
* 确保 slotMap 就绪后再处理秤数据,避免名称为空
*/
private fun loadSlotsThenObserveScales() {
lifecycleScope.launch(Dispatchers.IO) {
val slots = BaseApp.instance!!.database.seasoningSlotDao().queryAll()
withContext(Dispatchers.Main) {
slots.forEach { slot ->
slotMap["${slot.deviceId}#${slot.address}"] = slot.goodsId to slot.goodsName
}
filterList?.toMutableList()?.let { cellData ->
loadTextCell(cellData)
observeScaleData()
}
}
}
/**
* 订阅 ScaleServiceManager.allScales,过滤子设备数据并更新 adapter
* 排除主设备(DEVICE_ID_2),只处理 22格、18格、1格子设备
*/
private fun observeScaleData() {
val flow = ScaleServiceManager.allScales ?: return
lifecycleScope.launch {
flow.collectLatest { scaleMap ->
scaleMap.forEach { (key, data) ->
// 排除主设备 2格秤
if (data.deviceId == ScaleDeviceConfig.DEVICE_ID_2) return@forEach
// private lateinit var seasoningList: List<SeasoningEntity>
// private var isFirstSave = true
// fun updateSeasoningList(list: MutableList<SeasoningEntity>) {
// if (isFirstSave) {
// val json = list.toJsonString()
// val typeToken = object : TypeToken<List<SeasoningEntity>>() {}
// seasoningList = json.toType(typeToken = typeToken)
// isFirstSave = false
// }
// }
// 1格设备调料名固定为"油",其余从槽位配置读取
val (goodsId, goodsName) = if (data.deviceId == ScaleDeviceConfig.DEVICE_ID_1) {
"" to ""
} else {
slotMap[key] ?: return@forEach
}
private val seasoningArray by lazy {
SparseArray<Double>()
// 首次收到稳定数据时记录基准重量
val baseline = baselineMap[key]
if (baseline == null) {
if (data.state == WeightUtil.STATE_STABLE) {
baselineMap[key] = data.weight
}
return@forEach
}
// 用量 = 初始重量 - 当前重量,负值归零
val useWeight = (baseline - data.weight)
.coerceAtLeast(0.0)
.roundedOneDecimalPlace()
// 用量不足 0.5g 时从列表移除(或不添加)
if (useWeight < 0.5) {
seasoningAdapter.removeItem(key)
return@forEach
}
seasoningAdapter.updateItem(
SeasoningWeightAdapter.Item(
key = key,
goodsId = goodsId,
goodsName = goodsName,
useWeight = useWeight
)
)
}
}
}
}
private val firstGoodsArray = SparseArray<Double>()
private fun addViewListener() {
addWeightListener()
binding.btnWeightClear.setOnClickListener { WeightUtil.tareTwo(AddressUtil.TWO) }
binding.btnCook.clickWithDebounce {
cook()
}
binding.btnCook.clickWithDebounce { cook() }
binding.btnSubmit.clickWithDebounce {
if (cookFoodEntity.foodWeight <= 0.toDouble()) {
@@ -172,114 +190,40 @@ class SubmitFoodActivity : BaseActivity() {
}
submit()
}
}
private fun addWeightListener() {
WeightUtil.addWeightListener(
weightKey = TAG,
getWeight = { address, state, weight ->
if (address == AddressUtil.TWO) {
cookFoodEntity.foodWeight = weight.toDouble()
binding.tvTotalWeight.text = "${weight / 1000f}"
}
val firstWeight = firstGoodsArray.get(address, null)
if (firstWeight == null) {
//未设置数据
if (state != WeightUtil.STATE_STABLE) {
//首次记录数据需要稳定数据
return@addWeightListener
}
firstGoodsArray.put(address, weight)
return@addWeightListener
}
var realUseWeight = firstWeight - weight
realUseWeight = if (realUseWeight > 0) realUseWeight else 0.0
seasoningArray.put(address, realUseWeight.toDouble())
// refreshSeasoningWeight()
val item = seasoningItems.firstOrNull { address == addressArray[it.sort] }
item?.let {
val lastWeight = seasoningArray.get(address) ?: 0.toDouble()
it.useWeight =
(lastWeight + getCookingSeasoning(address)).roundedOneDecimalPlace()
updateGridData(it)
}
})
}
// private fun refreshSeasoningWeight() {
// if (seasoningItems.isEmpty()) {
// return
// }
// seasoningItems.let { items ->
// addressArray.forEach { key, value ->
// items[key].useWeight =
// (seasoningArray.get(value) ?: 0.toDouble()) + getCookingSeasoning(value)
// }
// }
// setGridData(seasoningItems)
// }
private fun updateGridData(entity: SeasoningEntity) {
val gridLayout = binding.include.root
val tag = entity.sort.toString()
val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag)
TextCellAdapter.loadLayout(frameLayout, entity)
}
private fun getCookingSeasoning(address: Int): Double {
if (isCooking.not()) return 0.toDouble()
if (seasoningItems.isEmpty()) return 0.toDouble()
val weight =
seasoningCookingItems?.get(weightRelateArray2.get(address))?.useWeight ?: 0.toDouble()
return weight
}
private val weightRelateArray2 by lazy {
SparseIntArray().apply {
addressArray.forEach { key, value ->
put(value, key)
// 监听主设备 2格秤的熟重
WeightUtil.addWeightListener(TAG) { address, _, weight ->
if (address == AddressUtil.TWO) {
cookFoodEntity.foodWeight = weight.toDouble()
binding.tvTotalWeight.text = "${weight / 1000f}"
}
}
}
private val addressArray by lazy {
AddressUtil.getWeighAddressArray()
}
// private fun prepareData(acton: () -> Unit) {
//
// //只有制作和采样列表页面才能进入设置页面,提交页面无法进行设置,不存在调料数据被修改的问题
// val seasoningJson = realSeasoningData.toJsonString()
// val typeToken = object : TypeToken<List<CookFoodGoodsEntity>>() {}
// val tempSeasoningList = seasoningJson.toType(typeToken = typeToken)
// goodsList?.addAll(tempSeasoningList)
// acton()
//
// }
private fun cook() {
if (food == null || goodsList.isNullOrEmpty()) {
toast("未获取到菜品或构成信息")
return
}
val realSeasoningData = seasoningItems
//(binding.rvSeasoning.adapter as TextCellAdapter).list
if (realSeasoningData.isEmpty() && !isCooking) {
val seasoningData = seasoningAdapter.items
if (seasoningData.isEmpty()) {
toast("未获取到调料信息")
return
}
if (realSeasoningData.isNotEmpty() && realSeasoningData[0].goodsId.isNullOrBlank()) {
toast("还未设置调料信息,请去设置页面操作")
return
}
showLoading()
//只有制作和采样列表页面才能进入设置页面,提交页面无法进行设置,不存在调料数据被修改的问题
val seasoningJson = realSeasoningData.toJsonString()
// 将调料数据转为 CookFoodGoodsEntity 追加到 goodsList
val typeToken = object : TypeToken<List<CookFoodGoodsEntity>>() {}
val tempSeasoningList = seasoningJson.toType(typeToken = typeToken)
val tempSeasoningList = seasoningData.map { item ->
CookFoodGoodsEntity().also { entity ->
entity.goodsId = item.goodsId
entity.goodsName = item.goodsName
entity.useWeight = item.useWeight
entity.materialType = 3
}
}
goodsList?.addAll(tempSeasoningList)
cookFoodEntity.let {
it.dinnerType = when (it.dinnerType) {
"早餐" -> "1"
@@ -288,17 +232,14 @@ class SubmitFoodActivity : BaseActivity() {
else -> it.dinnerType
}
}
val isSamplingData = food!!.cookMode == 1
if (isSamplingData) {
//生成临时foodId,解决采样模式没有foodId,无法关联数据的问题
val tempFoodId = cookFoodEntity.foodId.ifBlank { "${System.currentTimeMillis()}" }
cookFoodEntity.foodId = tempFoodId
}
//全部设置同一foodId
goodsList?.forEach {
it.foodId = cookFoodEntity.foodId
}
//保存菜品-保存主辅料调料
goodsList?.forEach { it.foodId = cookFoodEntity.foodId }
appViewModel.saveCookFoodAndGoods(
cookMode = food!!.cookMode,
entity = cookFoodEntity,
@@ -316,31 +257,22 @@ class SubmitFoodActivity : BaseActivity() {
toast("未获取到菜品或构成信息")
return
}
val realSeasoningData = seasoningItems
if (realSeasoningData.isEmpty() && !isCooking) {
toast("未获取到调料信息")
return
}
if (realSeasoningData.isNotEmpty() && realSeasoningData[0].goodsId.isNullOrBlank()) {
toast("还未设置调料信息,请去设置页面操作")
return
}
val seasoningData = seasoningAdapter.items
showLoading()
//只有制作和采样列表页面才能进入设置页面,提交页面无法进行设置,不存在调料数据被修改的问题
val seasoningJson = realSeasoningData.toJsonString()
val typeToken = object : TypeToken<List<CookFoodGoodsEntity>>() {}
val tempSeasoningList = seasoningJson.toType(typeToken = typeToken)
tempSeasoningList.forEach { entity ->
val realWeight = entity.useWeight ?: 0.toDouble()
val filterData = goodsList?.firstOrNull { it.goodsId == entity.goodsId }
//确保重量大于0,且无重复数据
if (realWeight > 0.toDouble() && filterData == null) {
goodsList?.add(entity)
// 将重量大于0且不重复的调料追加到 goodsList
seasoningData.forEach { item ->
if (item.useWeight > 0.0 && goodsList?.none { it.goodsId == item.goodsId } == true) {
goodsList?.add(CookFoodGoodsEntity().also { entity ->
entity.goodsId = item.goodsId
entity.goodsName = item.goodsName
entity.useWeight = item.useWeight
entity.materialType = 3
})
}
}
cookFoodEntity.let {
// it.stFoodInfoConstituteList = this@SubmitFoodActivity.goodsList
it.matchingConstituteInfoList = this@SubmitFoodActivity.goodsList
it.dinnerType = when (it.dinnerType) {
"1" -> "早餐"
@@ -352,30 +284,15 @@ class SubmitFoodActivity : BaseActivity() {
val isSamplingData = food!!.cookMode == 1
if (isSamplingData) {
//采样模式没有foodId
cookFoodEntity.foodId = ""
goodsList?.forEach { it.foodId = "" }
}
val json = cookFoodEntity.toJsonString()
Log.d(TAG, "submit: json=$json")
// HttpUtil.postJson(
// url = UrlConfig.SUBMIT_DISH,
// json = json,
// doSuccess = {
// Log.d(TAG, "submit: postJson=$it")
// submitSuccess(isSamplingData)
// }, doFailure = { code, msg ->
// Log.d(TAG, "submit: postJson:code=$code,msg=$msg")
// toast(msg)
// dismissLoading()
// })
Log.d(TAG, "submit: json=${cookFoodEntity.toJsonString()}")
netViewModel.submitCookFood(
entity = cookFoodEntity,
onSuccess = {
submitSuccess(isSamplingData)
},
onFailure = { code, msg ->
onSuccess = { submitSuccess(isSamplingData) },
onFailure = { _, msg ->
toast(msg)
dismissLoading()
})
@@ -383,12 +300,10 @@ class SubmitFoodActivity : BaseActivity() {
private fun submitSuccess(isSamplingData: Boolean) {
if (food?.foodId.isNullOrBlank()) {
//本地无删除数据直接跳转页面
dismissLoading()
jumpPage(isSamplingData, false)
return
}
//删除本地数据
appViewModel.deleteCookFoodAndGoods(
cookMode = food!!.cookMode,
foodId = food!!.foodId!!
@@ -415,57 +330,12 @@ class SubmitFoodActivity : BaseActivity() {
private fun initViewModel() {
val db = BaseApp.instance!!.database
val factory =
AppFactory(AppRepository(db.appDao()))
appViewModel =
ViewModelProvider(this, factory)[AppViewModel::class.java]
val factory = AppFactory(AppRepository(db.appDao()))
appViewModel = ViewModelProvider(this, factory)[AppViewModel::class.java]
}
override fun onResume() {
super.onResume()
if (isCooking.not()) {
//烹饪中使用本地数据,未烹饪则使用默认的调料配置数据
appViewModel.loadSeasoning {
Log.d(TAG, "onResume: loadSeasoning:${it.toJsonString()}")
loadTextCell(it)
}
}
}
private var seasoningItems = mutableListOf<SeasoningEntity>()
private var seasoningCookingItems: List<SeasoningEntity>? = null
private fun loadTextCell(list: MutableList<SeasoningEntity>) {
seasoningItems.clear()
seasoningItems.addAll(list)
initConfigData()
// setGridData(list)
list.forEach { entity ->
//烹饪中重量数据从本地获取,否则显示0
if (isCooking.not()) {
entity.useWeight = 0.0
}
updateGridData(entity)
}
seasoningCookingItems = seasoningItems.map { it.copy() }.toList()
}
private fun initConfigData() {
repeat(12) { num ->
val firstOne = seasoningItems.firstOrNull { it.sort == num }
if (firstOne == null) {
seasoningItems.add(SeasoningEntity().also { it.sort = num })
}
}
}
private fun setGridData(list: MutableList<SeasoningEntity>) {
val gridLayout = binding.include.root
list.forEach { entity ->
val tag = entity.sort.toString()
val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag)
TextCellAdapter.loadLayout(frameLayout, entity)
}
override fun onDestroy() {
WeightUtil.removeWeightListener(TAG)
super.onDestroy()
}
}
@@ -27,11 +27,12 @@
android:textColor="@color/black666"
android:textSize="28sp" />
<include
android:id="@+id/include"
layout="@layout/layout_grid"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvSeasoning"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
android:layout_height="660dp"
android:layout_marginTop="10dp"
android:overScrollMode="never" />
</LinearLayout>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="60dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<TextView
android:id="@+id/tvSeasoningName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/black666"
android:textSize="24sp" />
<TextView
android:id="@+id/tvSeasoningWeight"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:textColor="@color/black"
android:textSize="24sp" />
</LinearLayout>