5 Commits
Author SHA1 Message Date
lvmeng 7373ccacbb feat(activity): 新增食材识别页面,重构识别流程并优化多设备布局适配 2026-04-16 18:05:46 +08:00
lvmeng ad63639f66 refactor(dialog): 重构 CommonDialog 为链式调用 API,内置 title/content,全量替换旧调用方 2026-04-16 15:02:33 +08:00
lvmeng c3f3b1215b refactor(scale): 提取 Scale22/18GridAdapter 到 adapter 包,SlaveActivity 复用网格布局 2026-04-16 14:26:34 +08:00
lvmeng cba5bda757 fix(scale): 子设备断线时从聚合器移除数据,修复列表不消失的问题
- ScaleWebSocketClient 新增 onDeviceDisconnected 回调,在 onFailure/onClosed 时触发
- ScaleServiceManager 绑定回调,断线时调用 aggregator.removeDevice() 清除缓存
- 重连成功后数据重新推送,UI 列表自动恢复
2026-04-16 13:52:11 +08:00
lvmeng 7fd2f02b87 feat(activity): 为 MasterScaleActivity 各秤格子新增清零点击事件,修复高频数据更新导致点击失效的问题 2026-04-16 13:45:39 +08:00
26 changed files with 1078 additions and 404 deletions
+5
View File
@@ -111,6 +111,11 @@
android:theme="@style/Theme.DishMatch.NoSplash"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity
android:name="com.shuwei.dish.match.ui.FoodRecognizeActivity"
android:theme="@style/Theme.DishMatch.NoSplash"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity
android:name="com.shuwei.dish.match.ui.SlaveActivity"
android:theme="@style/Theme.DishMatch.NoSplash"
@@ -0,0 +1,102 @@
package com.shuwei.dish.match.adapter
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.databinding.ListItemScale22RowBinding
import com.shuwei.dish.match.scale.ScaleData
import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp
/**
* 18个秤的网格 Adapter
* 使用 GridLayoutManager6列)排列,第1行格子 margin 较大(视觉更小),第2-3行 margin 较小
* 点击格子触发清零回调
*/
class Scale18GridAdapter : BaseQuickAdapter<ScaleData, Scale18GridAdapter.VH>(mutableListOf()) {
inner class VH(val b: ListItemScale22RowBinding) : QuickViewHolder(b.root)
/** 用 address=0 的哨兵表示空位 */
private val empty = ScaleData("", 0, 0.0, -1, 0)
/** 点击回调,在 onCreateViewHolder 中绑定,不受 notifyItemChanged 影响 */
var onItemClick: ((ScaleData) -> Unit)? = null
/**
* 按 SCALE_ORDER_18 顺序更新数据,空位用哨兵填充
* 只更新显示字段有变化的格子,避免全量刷新
*/
fun update(newList: List<ScaleData?>) {
val filled = newList.map { it ?: empty }
if (items.isEmpty()) {
submitList(filled)
} else {
filled.forEachIndexed { i, scale ->
val cur = items.getOrNull(i)
if (cur == null || cur.address != scale.address || cur.weight != scale.weight || cur.state != scale.state) {
set(i, scale)
}
}
}
}
/**
* 将 ScaleData 列表按 SCALE_ORDER_18 重排后调用 update
*/
fun updateByAddress(scales: List<ScaleData>) {
val byAddress = scales.associateBy { it.address }
update(ScaleDeviceConfig.SCALE_ORDER_18.map { byAddress[it] })
}
/** 前6个位置为第1行(viewType=0margin大),其余为第2-3行(viewType=1margin小) */
override fun getItemViewType(position: Int, list: List<ScaleData>) = if (position < 6) 0 else 1
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val b = ListItemScale22RowBinding.inflate(LayoutInflater.from(context), parent, false)
// 第1行 margin=12dp(格子视觉更小),第2-3行 margin=2dp
val margin = if (viewType == 0) 12.dp else 2.dp
val screenWidth = parent.resources.displayMetrics.widthPixels
val cellSize = (screenWidth - 6 * margin * 2 - 10.dp) / 6
b.root.layoutParams = (b.root.layoutParams as ViewGroup.MarginLayoutParams).also {
it.width = cellSize
it.height = cellSize
// 第1行:顶部间距为0,底部间距2dp,左右保持原有 margin
if (viewType == 0) it.setMargins(margin, 0, margin, 2.dp)
else it.setMargins(margin, margin, margin, margin)
}
val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt()
b.tvAddr.textSize = (baseSp * 0.16f).coerceIn(9f, 14f)
b.tvWeight.textSize = (baseSp * 0.22f).coerceIn(12f, 20f)
b.tvState.textSize = (baseSp * 0.14f).coerceIn(8f, 12f)
val vh = VH(b)
// 在 ViewHolder 创建时绑定点击,只执行一次,不受 notifyItemChanged 重绑定影响
b.root.setOnClickListener {
val pos = vh.bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION) {
items.getOrNull(pos)?.takeIf { it.address != 0 }?.let { onItemClick?.invoke(it) }
}
}
return vh
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: VH, position: Int, item: ScaleData?) {
val valid = (item?.address ?: 0) != 0
holder.b.tvAddr.text = if (valid) "${item!!.address}" else ""
holder.b.tvWeight.text = if (valid) "${item!!.weight}g" else ""
holder.b.tvState.text = if (valid) stateStr(item!!.state) else ""
}
private fun stateStr(state: Int) = when (state) {
WeightUtil.STATE_STABLE -> "稳定"
WeightUtil.STATE_UNSTABLE -> "不稳定"
WeightUtil.STATE_OVER_WEIGHT -> "超量"
else -> "$state"
}
}
@@ -0,0 +1,104 @@
package com.shuwei.dish.match.adapter
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.google.android.flexbox.FlexboxLayoutManager
import com.shuwei.dish.match.databinding.ListItemScale22RowBinding
import com.shuwei.dish.match.scale.ScaleData
import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp
/**
* 22个秤的网格 Adapter
* 使用 FlexboxLayoutManager 按列排列,前4个格子较大(对应物理上的大秤),其余较小
* 点击格子触发清零回调
*
* @param largeSize 前4个大格子的尺寸(px
* @param smallSize 其余小格子的尺寸(px
*/
class Scale22GridAdapter(
private val largeSize: Int,
private val smallSize: Int
) : BaseQuickAdapter<ScaleData, Scale22GridAdapter.VH>(mutableListOf()) {
inner class VH(val b: ListItemScale22RowBinding) : QuickViewHolder(b.root)
/** 用 address=0 的哨兵表示空位 */
private val empty = ScaleData("", 0, 0.0, -1, 0)
/** 点击回调,在 onCreateViewHolder 中绑定,不受 notifyItemChanged 影响 */
var onItemClick: ((ScaleData) -> Unit)? = null
/**
* 按 SCALE_ORDER_22 顺序更新数据,空位用哨兵填充
* 只更新显示字段有变化的格子,避免全量刷新
*/
fun update(newList: List<ScaleData?>) {
val filled = newList.map { it ?: empty }
if (items.isEmpty()) {
submitList(filled)
} else {
filled.forEachIndexed { i, scale ->
val cur = items.getOrNull(i)
if (cur == null || cur.address != scale.address || cur.weight != scale.weight || cur.state != scale.state) {
set(i, scale)
}
}
}
}
/**
* 将 ScaleData 列表按 SCALE_ORDER_22 重排后调用 update
*/
fun updateByAddress(scales: List<ScaleData>) {
val byAddress = scales.associateBy { it.address }
update(ScaleDeviceConfig.SCALE_ORDER_22.map { byAddress[it] })
}
/** 前4个位置为大格子(viewType=1),其余为小格子(viewType=0 */
override fun getItemViewType(position: Int, list: List<ScaleData>) = if (position < 4) 1 else 0
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val b = ListItemScale22RowBinding.inflate(LayoutInflater.from(context), parent, false)
val cellSize = if (viewType == 1) largeSize else smallSize
(b.root.layoutParams as? FlexboxLayoutManager.LayoutParams)?.apply {
width = cellSize
height = cellSize
setMargins(2.dp, 2.dp, 2.dp, 2.dp)
}
val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt()
b.tvAddr.textSize = (baseSp * 0.16f).coerceIn(9f, 14f)
b.tvWeight.textSize = (baseSp * 0.22f).coerceIn(12f, 20f)
b.tvState.textSize = (baseSp * 0.14f).coerceIn(8f, 12f)
val vh = VH(b)
// 在 ViewHolder 创建时绑定点击,只执行一次,不受 notifyItemChanged 重绑定影响
b.root.setOnClickListener {
val pos = vh.bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION) {
items.getOrNull(pos)?.takeIf { it.address != 0 }?.let { onItemClick?.invoke(it) }
}
}
return vh
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: VH, position: Int, item: ScaleData?) {
val valid = (item?.address ?: 0) != 0
holder.b.tvAddr.text = if (valid) "${item!!.address}" else ""
holder.b.tvWeight.text = if (valid) "${item!!.weight}g" else ""
holder.b.tvState.text = if (valid) stateStr(item!!.state) else ""
}
private fun stateStr(state: Int) = when (state) {
WeightUtil.STATE_STABLE -> "稳定"
WeightUtil.STATE_UNSTABLE -> "不稳定"
WeightUtil.STATE_OVER_WEIGHT -> "超量"
else -> "$state"
}
}
@@ -0,0 +1,42 @@
package com.shuwei.dish.match.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.databinding.ListItemScaleRowBinding
import com.shuwei.dish.match.utils.WeightUtil
/**
* 线性列表秤数据 Adapter
* 用于 SlaveActivity 非22/18格子设备的秤列表展示
*/
class ScaleRowAdapter : BaseQuickAdapter<ScaleRowAdapter.ScaleItem, ScaleRowAdapter.VH>() {
/**
* 秤数据条目
* @param address 秤地址
* @param weight 重量(克)
* @param state 状态码
*/
data class ScaleItem(val address: Int, val weight: Double, val state: Int)
inner class VH(val b: ListItemScaleRowBinding) : QuickViewHolder(b.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
return VH(ListItemScaleRowBinding.inflate(LayoutInflater.from(context), parent, false))
}
override fun onBindViewHolder(holder: VH, position: Int, item: ScaleItem?) {
item ?: return
holder.b.tvScaleLabel.text = "${item.address}"
holder.b.tvWeight.text = "${item.weight} g"
holder.b.tvState.text = when (item.state) {
WeightUtil.STATE_STABLE -> "稳定"
WeightUtil.STATE_UNSTABLE -> "不稳定"
WeightUtil.STATE_OVER_WEIGHT -> "超量"
else -> "${item.state}"
}
}
}
@@ -26,6 +26,7 @@ import com.shuwei.dish.match.entity.SeasoningEntity
import com.shuwei.dish.match.net.NetViewModel
import com.shuwei.dish.match.ui.InitActivity
import com.shuwei.dish.match.utils.ActivityManager
import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.invisible
import com.shuwei.dish.match.utils.ext.visible
@@ -65,6 +66,10 @@ open class BaseActivity : AppCompatActivity() {
// systemBarsBehavior =
// WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE // 滑动时临时显示
// }
// 主设备垂直 padding 30dp,子设备 15dp
val verticalPadding = if (GlobalData.deviceRole == DeviceRole.MASTER) 30.dp else 10.dp
binding.tvLeftTime.setPadding(0, verticalPadding, 0, verticalPadding)
binding.tvRightTime.setPadding(0, verticalPadding, 0, verticalPadding)
binding.llTitleBar.gone()
binding.ivBack.setOnClickListener { finish() }
}
@@ -7,9 +7,11 @@ import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.View
import android.view.Window
import android.view.WindowManager
import android.widget.LinearLayout
import androidx.annotation.StyleRes
import androidx.fragment.app.FragmentActivity
import androidx.core.view.updateLayoutParams
import com.shuwei.dish.match.base.DeviceRole
import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.databinding.DialogCommonBinding
import com.shuwei.dish.match.utils.ext.dp
import java.lang.ref.WeakReference
@@ -20,53 +22,74 @@ open class CommonDialog(
private lateinit var binding: DialogCommonBinding
private var titleText: String? = null
private var contentText: String? = null
private var negativeText = "取消"
private var positiveText = "确认"
private var negativeClick: (() -> Unit)? = null
private var positiveClick: (() -> Unit)? = null
private var dismissCallback: (() -> Unit)? = null
/** 设置标题,为空时隐藏 */
fun setTitle(text: String): CommonDialog = apply { titleText = text }
/** 设置内容,为空时隐藏 */
fun setContent(text: String): CommonDialog = apply { contentText = text }
/** 设置左侧取消按钮文字及点击回调 */
fun setNegativeButton(text: String, onClick: (() -> Unit)? = null): CommonDialog = apply {
negativeText = text
negativeClick = onClick
}
/** 设置右侧确认按钮文字及点击回调 */
fun setPositiveButton(text: String, onClick: (() -> Unit)? = null): CommonDialog = apply {
positiveText = text
positiveClick = onClick
}
/** 设置弹窗消失回调 */
fun setOnDismissCallback(callback: () -> Unit): CommonDialog = apply {
dismissCallback = callback
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
binding = DialogCommonBinding.inflate(layoutInflater)
setContentView(binding.root)
window?.run {
// decorView.setPadding(0.dp, 0, 0.dp, 0)
val screenWidth = context.resources.displayMetrics.widthPixels
val isMaster = GlobalData.deviceRole == DeviceRole.MASTER
attributes = attributes.apply {
width = 708.dp
width = if (isMaster) (screenWidth * 0.72f).toInt() else (screenWidth * 0.60f).toInt()
}
setBackgroundDrawable(ColorDrawable())
setCancelable(false)
// setFlags(
// WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
// WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
// )
}
binding.llContent.run {
removeAllViews()
addView(messageView)
val verticalMargin = if (GlobalData.deviceRole == DeviceRole.MASTER) 35.dp else 20.dp
binding.tvTitle.updateLayoutParams<LinearLayout.LayoutParams> { topMargin = verticalMargin }
binding.tvContent.updateLayoutParams<LinearLayout.LayoutParams> { bottomMargin = verticalMargin }
binding.btnLeft.setOnClickListener {
negativeClick?.invoke()
dismiss()
}
addListener()
binding.btnRight.setOnClickListener {
positiveClick?.invoke()
dismiss()
}
setOnDismissListener { dismissCallback?.invoke() }
}
private fun addListener() {
binding.btnLeft.setOnClickListener { onLeftClick?.invoke() }
binding.btnRight.setOnClickListener { onRightClick?.invoke() }
setOnDismissListener { onDismiss?.invoke() }
}
override fun show() {
super.show()
binding.btnLeft.text = leftText
binding.btnRight.text = rightText
binding.tvTitle.text = titleText
binding.tvContent.text = contentText
binding.tvTitle.visibility = if (titleText.isNullOrEmpty()) View.GONE else View.VISIBLE
binding.tvContent.visibility = if (contentText.isNullOrEmpty()) View.GONE else View.VISIBLE
binding.btnLeft.text = negativeText
binding.btnRight.text = positiveText
}
var messageView: View? = null
var onLeftClick: (() -> Unit)? = null
var onRightClick: (() -> Unit)? = null
var leftText = ""
var rightText = ""
var onDismiss: (() -> Unit)? = null
}
open class SafeDialog(activity: Activity, @StyleRes themeResId: Int) :
@@ -1,38 +0,0 @@
package com.shuwei.dish.match.dialog
import android.content.Context
import android.view.LayoutInflater
import com.shuwei.dish.match.databinding.LayoutFoodRemindBinding
object DialogTool {
fun load(
context: Context,
title:String,
content:String,
leftBtnText: String,
rightBtnText: String,
leftBtnClick: () -> Unit = {},
rightBtnClick: () -> Unit = {}
) {
val layoutInflater = LayoutInflater.from(context)
val remindBinding = LayoutFoodRemindBinding.inflate(layoutInflater)
remindBinding.tvDialogTitle.text = title
remindBinding.tvDialogContent.text = content
CommonDialog(context).apply {
messageView = remindBinding.root
leftText = leftBtnText
rightText = rightBtnText
onLeftClick = {
leftBtnClick()
dismiss()
}
onRightClick = {
rightBtnClick()
dismiss()
}
onDismiss = { }
}.show()
}
}
@@ -66,10 +66,10 @@ class LoadingDialog(
setCancelable(true)
window?.run {
setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
setFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
);
// setFlags(
// WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
// WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
// );
}
setOnDismissListener {
onDismiss()
@@ -0,0 +1,22 @@
package com.shuwei.dish.match.scale
/**
* 各子设备的固定 ID 与秤地址排列顺序配置
* MasterScaleActivity 和 SlaveActivity 共用此配置,避免重复定义
*/
object ScaleDeviceConfig {
const val DEVICE_ID_2 = "8fc2ab34-2137-3112-acca-f884ea8736d4"
const val DEVICE_ID_22 = "a46fa55c-113c-3511-bb1f-41e5eff77c4b"
const val DEVICE_ID_18 = "1038da9f-c6eb-326e-a1d9-d6d3af978b22"
const val DEVICE_ID_1 = "7cc0f6ea-f13d-3013-a867-fc998eb554ac"
/** 22个秤的显示顺序(按物理位置排列) */
val SCALE_ORDER_22 = listOf(20, 22, 19, 21, 6, 12, 18, 5, 11, 17, 4, 10, 16, 3, 9, 15, 2, 8, 14, 1, 7, 13)
/** 18个秤的显示顺序(按物理位置排列) */
val SCALE_ORDER_18 = listOf(6, 5, 4, 3, 2, 1, 12, 11, 10, 9, 8, 7, 18, 17, 16, 15, 14, 13)
/** 设备在列表中的显示顺序 */
val DEVICE_ORDER = listOf(DEVICE_ID_2, DEVICE_ID_22, DEVICE_ID_18, DEVICE_ID_1)
}
@@ -100,6 +100,7 @@ object ScaleServiceManager {
// WebSocket 客户端
val client = ScaleWebSocketClient().also {
it.onScaleData = { data -> aggregator?.onRemoteScaleData(data) }
it.onDeviceDisconnected = { remoteId -> aggregator?.removeDevice(remoteId) }
}
wsClient = client
@@ -141,12 +142,13 @@ object ScaleServiceManager {
* @param deviceId 目标设备 ID
* @param address 目标秤地址
*/
fun sendTare(deviceId: String, address: Int) {
if (deviceId == GlobalData.deviceId) {
fun sendTare(deviceId: String, address: Int): Boolean {
return if (deviceId == GlobalData.deviceId) {
com.shuwei.dish.match.utils.WeightUtil.tareTwo(address)
true
} else {
val cmd = ScaleCommand(ScaleCommand.CMD_TARE, deviceId, address)
wsClient?.sendCommand(deviceId, cmd)
wsClient?.sendCommand(deviceId, cmd) ?: false
}
}
@@ -49,6 +49,9 @@ class ScaleWebSocketClient {
/** 收到秤数据时的回调,在子线程调用 */
var onScaleData: ((data: ScaleData) -> Unit)? = null
/** 设备断线时的回调(连接失败或关闭),在子线程调用 */
var onDeviceDisconnected: ((deviceId: String) -> Unit)? = null
/**
* 连接到指定子设备
* @param deviceId 子设备 ID
@@ -85,12 +88,14 @@ class ScaleWebSocketClient {
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Log.w(TAG, "连接子设备失败: $deviceId, ${t.message}")
connections.remove(deviceId)
onDeviceDisconnected?.invoke(deviceId)
scheduleReconnect(deviceId, host, port)
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Log.d(TAG, "子设备连接已关闭: $deviceId, reason=$reason")
connections.remove(deviceId)
onDeviceDisconnected?.invoke(deviceId)
scheduleReconnect(deviceId, host, port)
}
})
@@ -15,7 +15,6 @@ import androidx.core.view.forEach
import androidx.lifecycle.ViewModelProvider
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.LayoutFoodRemindBinding
import com.shuwei.dish.match.db.AppRepository
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.entity.SeasoningEntity
@@ -169,21 +168,13 @@ class DeviceConfigActivity : BaseActivity() {
val detector = MultiClickDetector(targetCount = 10, intervalMs = 800)
private fun defaultDataSettingDialog() {
val remindBinding = LayoutFoodRemindBinding.inflate(layoutInflater)
remindBinding.tvDialogTitle.text = "温馨提示"
remindBinding.tvDialogContent.text =
"您好,使用默认配置作为您的调料数据,则已有配置将被清除,确认吗?"
CommonDialog(this).apply {
messageView = remindBinding.root
leftText = "取消"
rightText = "确认"
onLeftClick = { dismiss() }
onRightClick = {
dismiss()
resetList()
}
onDismiss = { hideStatusBar() }
}.show()
CommonDialog(this)
.setTitle("温馨提示")
.setContent("您好,使用默认配置作为您的调料数据,则已有配置将被清除,确认吗?")
.setNegativeButton("取消")
.setPositiveButton("确认") { resetList() }
.setOnDismissCallback { hideStatusBar() }
.show()
}
private fun resetList() {
@@ -15,7 +15,6 @@ import com.shuwei.dish.match.adapter.DishPartAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.ActivityDishSamplingBinding
import com.shuwei.dish.match.databinding.LayoutFoodRemindBinding
import com.shuwei.dish.match.db.AppRepository
import com.shuwei.dish.match.dialog.FoodSearchDialog
import com.shuwei.dish.match.dialog.CommonDialog
@@ -130,23 +129,18 @@ class DishSamplingActivity : BaseActivity() {
}
private fun saveDataRemindDialog() {
val remindBinding = LayoutFoodRemindBinding.inflate(layoutInflater)
remindBinding.tvDialogTitle.text = "温馨提示"
remindBinding.tvDialogContent.text = "您好,当前页面存在未保存的数据,确认返回吗?"
CommonDialog(this).apply {
messageView = remindBinding.root
leftText = "取消"
rightText = "确认"
onLeftClick = { dismiss() }
onRightClick = {
dismiss()
CommonDialog(this)
.setTitle("温馨提示")
.setContent("您好,当前页面存在未保存的数据,确认返回吗?")
.setNegativeButton("取消")
.setPositiveButton("确认") {
if (pageFrom == HOME) {
startActivity<SamplingListActivity>()
}
finish()
}
onDismiss = { hideStatusBar() }
}.show()
.setOnDismissCallback { hideStatusBar() }
.show()
}
@@ -0,0 +1,213 @@
package com.shuwei.dish.match.ui
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.recyclerview.widget.GridLayoutManager
import com.shuwei.dish.match.adapter.GoodsInfoSearchAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.ActivityFoodRecognizeBinding
import com.shuwei.dish.match.dialog.FoodSearchDialog
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
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
import com.shuwei.dish.match.utils.ext.visible
/**
* 食材识别结果页面
* 展示识别到的食材照片、食材列表、选中食材名称及秤1重量
*/
class FoodRecognizeActivity : BaseActivity() {
companion object {
private const val TAG = "FoodRecognizeActivity"
private const val EXTRA_IMAGE_URI = "extra_image_uri"
const val EXTRA_SELECTED_ITEM = "extra_selected_item"
/** 内存缓存食材列表,避免通过 Intent 序列化传递大数据 */
private var pendingGoodsList: ArrayList<CookFoodGoodsEntity>? = null
/**
* 启动本页面,通过 onResult 回调返回用户选中的食材
* @param activity 宿主 Activity
* @param imageUri 食材照片路径(可为空)
* @param goodsList 识别到的食材列表
* @param onResult 页面关闭后回调,携带用户选中的食材(未选中时为 null)
*/
fun start(
activity: BaseActivity,
imageUri: String? = null,
goodsList: ArrayList<CookFoodGoodsEntity> = arrayListOf(),
onResult: (CookFoodGoodsEntity?) -> Unit = {}
) {
pendingGoodsList = goodsList
val intent = Intent(activity, FoodRecognizeActivity::class.java).apply {
putExtra(EXTRA_IMAGE_URI, imageUri)
}
activity.startActivity(intent) { resultIntent ->
@Suppress("DEPRECATION")
val item = resultIntent?.getSerializableExtra(EXTRA_SELECTED_ITEM) as? CookFoodGoodsEntity
onResult(item)
}
}
/**
* 关闭当前识别页面,并将选中食材通过 Activity Result 回传
* 由外部(如重量归零时)调用
*/
fun close() {
ActivityManager.getActivityStack()
.filterIsInstance<FoodRecognizeActivity>()
.lastOrNull()
?.finishWithResult()
}
}
private lateinit var binding: ActivityFoodRecognizeBinding
private val list = mutableListOf<CookFoodGoodsEntity>()
private val adapter = GoodsInfoSearchAdapter(list).apply {
setOnItemClickListener { _, _, position ->
// 更新选中状态
list.forEachIndexed { i, item -> item.isClicked = (i == position) }
notifyDataSetChanged()
// 同步更新选中食材名称
binding.tvSelectedFood.text = list[position].goodsName ?: "-"
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityFoodRecognizeBinding.inflate(layoutInflater)
setContentView(binding.root)
setHeaderBackground()
// 显示标题栏
setTitleBar(titleBarAction = { it.visible() }, titleAction = { it.text = "食材识别" })
initData()
initRecyclerView()
initClickListeners()
registerWeightListener()
}
/**
* 从 Intent 读取数据并初始化页面
*/
private fun initData() {
// 加载食材照片
val imageUri = intent.getStringExtra(EXTRA_IMAGE_URI)
if (!imageUri.isNullOrBlank()) {
binding.ivFoodPhoto.setImageURI(Uri.parse(imageUri))
}
// 取出内存缓存的食材列表,取用后立即清空
val goodsList = pendingGoodsList?.also { pendingGoodsList = null }
if (!goodsList.isNullOrEmpty()) {
list.addAll(goodsList)
// 默认选中第一项
list[0].isClicked = true
binding.tvSelectedFood.text = list[0].goodsName ?: "-"
}
}
/**
* 初始化 RecyclerView
*/
private fun initRecyclerView() {
binding.recyclerView.run {
layoutManager = GridLayoutManager(this@FoodRecognizeActivity, 2)
adapter = this@FoodRecognizeActivity.adapter
}
updateListVisibility()
}
/**
* 根据列表是否有数据控制 RecyclerView 显隐
*/
private fun updateListVisibility() {
if (list.isEmpty()) {
binding.recyclerView.gone()
} else {
binding.recyclerView.visible()
}
}
/**
* 初始化按钮点击事件
*/
private fun initClickListeners() {
// 更换食材:打开食材搜索弹窗
binding.btnChangeFood.clickWithDebounce {
val currentName = if (list.isNotEmpty()) list.firstOrNull { it.isClicked }?.goodsName else null
FoodSearchDialog(
activity = this,
defGoodsName = currentName,
onItemSelected = { item ->
// 用搜索结果替换当前选中食材
updateSelectedFood(item)
}
).show()
}
// 取消:关闭页面
binding.btnCancel.setOnClickListener { finish() }
}
/**
* 用新食材替换当前选中项(或追加到列表首位)
*/
@SuppressLint("NotifyDataSetChanged")
private fun updateSelectedFood(item: CookFoodGoodsEntity) {
item.isClicked = true
val clickedIndex = list.indexOfFirst { it.isClicked && it.goodsId != item.goodsId }
if (clickedIndex >= 0) {
list[clickedIndex].isClicked = false
}
// 若列表中已存在则直接选中,否则插入到首位
val existIndex = list.indexOfFirst { it.goodsId == item.goodsId }
if (existIndex >= 0) {
list.forEachIndexed { i, e -> e.isClicked = (i == existIndex) }
} else {
list.add(0, item)
}
adapter.notifyDataSetChanged()
binding.tvSelectedFood.text = item.goodsName ?: "-"
updateListVisibility()
}
/**
* 注册秤1重量监听
*/
private fun registerWeightListener() {
WeightUtil.addWeightListener(TAG) { address, _, weight ->
if (address == AddressUtil.ONE) {
runOnUiThread {
binding.tvWeight.text = "$weight g"
}
}
}
}
/**
* 携带当前选中食材关闭页面
*/
private fun finishWithResult() {
val selected = list.firstOrNull { it.isClicked }
val intent = Intent().apply {
selected?.let { putExtra(EXTRA_SELECTED_ITEM, it) }
}
setResult(RESULT_OK, intent)
finish()
}
override fun onDestroy() {
super.onDestroy()
WeightUtil.removeWeightListener(TAG)
}
}
@@ -12,37 +12,31 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.chad.library.adapter4.BaseMultiItemAdapter
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.google.android.flexbox.AlignItems
import com.google.android.flexbox.FlexDirection
import com.google.android.flexbox.FlexWrap
import com.google.android.flexbox.FlexboxLayoutManager
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.Scale18GridAdapter
import com.shuwei.dish.match.adapter.Scale22GridAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.databinding.ActivityMasterScaleBinding
import com.shuwei.dish.match.databinding.ListItemScale22RowBinding
import com.shuwei.dish.match.databinding.ListTypeScale1Binding
import com.shuwei.dish.match.databinding.ListTypeScale18Binding
import com.shuwei.dish.match.databinding.ListTypeScale2Binding
import com.shuwei.dish.match.databinding.ListTypeScale22Binding
import com.shuwei.dish.match.scale.ScaleData
import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.scale.ScaleServiceManager
import com.shuwei.dish.match.utils.NetworkUtil
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.toast
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
private const val DEVICE_ID_2 = "8fc2ab34-2137-3112-acca-f884ea8736d4"
private const val DEVICE_ID_22 = "a46fa55c-113c-3511-bb1f-41e5eff77c4b"
private const val DEVICE_ID_18 = "1038da9f-c6eb-326e-a1d9-d6d3af978b22"
private const val DEVICE_ID_1 = "7cc0f6ea-f13d-3013-a867-fc998eb554ac"
private val SCALE_ORDER_22 = listOf(20, 22, 19, 21, 6, 12, 18, 5, 11, 17, 4, 10, 16, 3, 9, 15, 2, 8, 14, 1, 7, 13)
private val SCALE_ORDER_18 = listOf(6, 5, 4, 3, 2, 1, 12, 11, 10, 9, 8, 7, 18, 17, 16, 15, 14, 13)
private val deviceOrder = listOf(DEVICE_ID_2, DEVICE_ID_22, DEVICE_ID_18, DEVICE_ID_1)
private const val VIEW_TYPE_SCALE2 = 0
private const val VIEW_TYPE_SCALE22 = 1
private const val VIEW_TYPE_SCALE18 = 2
@@ -89,7 +83,7 @@ class MasterScaleActivity : BaseActivity() {
.groupBy { it.deviceId }
.entries
.sortedWith(compareBy { entry ->
deviceOrder.indexOf(entry.key).let { if (it == -1) Int.MAX_VALUE else it }
ScaleDeviceConfig.DEVICE_ORDER.indexOf(entry.key).let { if (it == -1) Int.MAX_VALUE else it }
})
.map { (deviceId, scales) ->
DeviceGroup(
@@ -99,17 +93,43 @@ class MasterScaleActivity : BaseActivity() {
scales = scales.sortedBy { it.address }
)
}
updateList(groups)
val structureChanged = groups.size != groupList.size ||
groups.indices.any { groups[it].deviceId != groupList[it].deviceId }
if (structureChanged) {
updateList(groups)
} else {
groups.forEach { group -> adapter.updateScalesForDevice(group.deviceId, group.scales) }
}
updateStatusBar(scaleMap)
}
}
}
@SuppressLint("NotifyDataSetChanged")
private fun updateList(groups: List<DeviceGroup>) {
groupList.clear()
groupList.addAll(groups)
adapter.notifyDataSetChanged()
if (groupList.isEmpty()) {
groupList.addAll(groups)
adapter.notifyDataSetChanged()
return
}
// 精确 diff,只 notify 真正变化的 item,避免全量刷新打断触摸事件
groups.forEachIndexed { i, group ->
if (i < groupList.size) {
if (groupList[i] != group) {
groupList[i] = group
adapter.notifyItemChanged(i)
}
} else {
groupList.add(group)
adapter.notifyItemInserted(i)
}
}
while (groupList.size > groups.size) {
groupList.removeAt(groupList.lastIndex)
adapter.notifyItemRemoved(groupList.size)
}
}
@SuppressLint("SetTextI18n")
@@ -122,6 +142,18 @@ class MasterScaleActivity : BaseActivity() {
private inner class DeviceGroupAdapter(data: MutableList<DeviceGroup>) :
BaseMultiItemAdapter<DeviceGroup>(data) {
private val scale1VHMap = mutableMapOf<String, Scale1VH>()
private val scale2VHMap = mutableMapOf<String, Scale2VH>()
private val scale22VHMap = mutableMapOf<String, Scale22VH>()
private val scale18VHMap = mutableMapOf<String, Scale18VH>()
fun updateScalesForDevice(deviceId: String, scales: List<ScaleData>) {
scale1VHMap[deviceId]?.updateScales(scales)
scale2VHMap[deviceId]?.updateScales(scales)
scale22VHMap[deviceId]?.updateScales(scales)
scale18VHMap[deviceId]?.updateScales(scales)
}
init {
addItemType(VIEW_TYPE_SCALE2, object : BaseMultiItemAdapter.OnMultiItemAdapterListener<DeviceGroup, QuickViewHolder> {
override fun onCreate(context: Context, parent: ViewGroup, viewType: Int) =
@@ -129,7 +161,9 @@ class MasterScaleActivity : BaseActivity() {
override fun onBind(holder: QuickViewHolder, position: Int, item: DeviceGroup?) {
item ?: return
val b = ListTypeScale2Binding.bind(holder.itemView)
Scale2VH(b).bind(item)
val vh = Scale2VH(b)
scale2VHMap[item.deviceId] = vh
vh.bind(item)
}
})
addItemType(VIEW_TYPE_SCALE22, object : BaseMultiItemAdapter.OnMultiItemAdapterListener<DeviceGroup, QuickViewHolder> {
@@ -138,7 +172,10 @@ class MasterScaleActivity : BaseActivity() {
override fun onBind(holder: QuickViewHolder, position: Int, item: DeviceGroup?) {
item ?: return
val b = ListTypeScale22Binding.bind(holder.itemView)
Scale22VH(b).bind(item)
// 每次 onBind 时注册/更新 VH 缓存,后续数据更新直接走 VH,不再触发 notify
val vh = Scale22VH(b)
scale22VHMap[item.deviceId] = vh
vh.bind(item)
}
})
addItemType(VIEW_TYPE_SCALE18, object : BaseMultiItemAdapter.OnMultiItemAdapterListener<DeviceGroup, QuickViewHolder> {
@@ -147,7 +184,9 @@ class MasterScaleActivity : BaseActivity() {
override fun onBind(holder: QuickViewHolder, position: Int, item: DeviceGroup?) {
item ?: return
val b = ListTypeScale18Binding.bind(holder.itemView)
Scale18VH(b).bind(item)
val vh = Scale18VH(b)
scale18VHMap[item.deviceId] = vh
vh.bind(item)
}
})
addItemType(VIEW_TYPE_SCALE1, object : BaseMultiItemAdapter.OnMultiItemAdapterListener<DeviceGroup, QuickViewHolder> {
@@ -156,13 +195,15 @@ class MasterScaleActivity : BaseActivity() {
override fun onBind(holder: QuickViewHolder, position: Int, item: DeviceGroup?) {
item ?: return
val b = ListTypeScale1Binding.bind(holder.itemView)
Scale1VH(b).bind(item)
val vh = Scale1VH(b)
scale1VHMap[item.deviceId] = vh
vh.bind(item)
}
})
onItemViewType { position, list -> when (list.getOrNull(position)?.deviceId) {
DEVICE_ID_2 -> VIEW_TYPE_SCALE2
DEVICE_ID_22 -> VIEW_TYPE_SCALE22
DEVICE_ID_18 -> VIEW_TYPE_SCALE18
ScaleDeviceConfig.DEVICE_ID_2 -> VIEW_TYPE_SCALE2
ScaleDeviceConfig.DEVICE_ID_22 -> VIEW_TYPE_SCALE22
ScaleDeviceConfig.DEVICE_ID_18 -> VIEW_TYPE_SCALE18
else -> VIEW_TYPE_SCALE1
}}
}
@@ -171,14 +212,36 @@ class MasterScaleActivity : BaseActivity() {
// ── ViewHolder: 2个秤 ──────────────────────────────────────────────────
private inner class Scale2VH(val b: ListTypeScale2Binding) : RecyclerView.ViewHolder(b.root) {
private lateinit var cells: List<LinearLayout>
@SuppressLint("SetTextI18n")
fun bind(group: DeviceGroup) {
b.tvDeviceIp.text = "IP${group.ip.ifEmpty { "未知" }}"
b.tvDeviceLabel.text = "设备:${group.deviceId}"
b.llScaleContainer.removeAllViews()
val cellSize = (resources.displayMetrics.widthPixels - 4 * 4.dp - 10.dp) / 2
group.scales.take(2).forEach { scale ->
b.llScaleContainer.addView(makeScaleCell(scale, cellSize))
if (!::cells.isInitialized) {
val cellSize = (resources.displayMetrics.widthPixels - 4 * 4.dp - 10.dp) / 2
cells = group.scales.take(2).map { scale ->
makeScaleCell(scale, cellSize).also { cell ->
cell.setOnClickListener {
val s = cell.tag as? ScaleData ?: return@setOnClickListener
showTareDialog(deviceId = s.deviceId, ip = s.ip, address = s.address)
}
}
}
b.llScaleContainer.removeAllViews()
cells.forEach { b.llScaleContainer.addView(it) }
}
updateScales(group.scales)
}
fun updateScales(scales: List<ScaleData>) {
if (!::cells.isInitialized) return
scales.take(2).forEachIndexed { i, scale ->
cells.getOrNull(i)?.let { cell ->
cell.tag = scale
updateScaleCell(cell, scale)
}
}
}
}
@@ -186,17 +249,16 @@ class MasterScaleActivity : BaseActivity() {
// ── ViewHolder: 22个秤 ─────────────────────────────────────────────────
private inner class Scale22VH(val b: ListTypeScale22Binding) : RecyclerView.ViewHolder(b.root) {
private var bound = false
private var innerAdapter: Scale22GridAdapter? = null
@SuppressLint("SetTextI18n")
fun bind(group: DeviceGroup) {
b.tvDeviceIp.text = "IP${group.ip.ifEmpty { "未知" }}"
b.tvDeviceLabel.text = "设备:${group.deviceId}"
if (!bound) {
// 计算格子尺寸(与 Scale22GridActivity 一致)
if (innerAdapter == null) {
val screenWidth = resources.displayMetrics.widthPixels
val usable = screenWidth - 8 * 4.dp - 10.dp // 减去8列格子margin + 外层RV左右margin各5dp
val usable = screenWidth - 8 * 4.dp - 10.dp
val smallSize = usable / 9
val largeSize = (usable - smallSize * 6) / 2
val rvHeight = smallSize * 3 + 3 * 4.dp
@@ -207,154 +269,83 @@ class MasterScaleActivity : BaseActivity() {
alignItems = AlignItems.FLEX_START
}
b.rvScales.itemAnimator = null
b.rvScales.adapter = Scale22InnerAdapter(largeSize, smallSize)
bound = true
innerAdapter = Scale22GridAdapter(largeSize, smallSize).apply {
onItemClick = { scale ->
showTareDialog(deviceId = scale.deviceId, ip = scale.ip, address = scale.address)
}
}
b.rvScales.adapter = innerAdapter
}
val scaleByAddress = group.scales.associateBy { it.address }
val ordered = SCALE_ORDER_22.map { scaleByAddress[it] }
(b.rvScales.adapter as Scale22InnerAdapter).update(ordered)
updateScales(group.scales)
}
/** 仅更新秤数据,不触发外层任何 notify */
fun updateScales(scales: List<ScaleData>) {
innerAdapter?.updateByAddress(scales)
}
}
// ── ViewHolder: 18个秤 ─────────────────────────────────────────────────
private inner class Scale18VH(val b: ListTypeScale18Binding) : RecyclerView.ViewHolder(b.root) {
private var bound = false
private var innerAdapter: Scale18GridAdapter? = null
@SuppressLint("SetTextI18n")
fun bind(group: DeviceGroup) {
b.tvDeviceIp.text = "IP${group.ip.ifEmpty { "未知" }}"
b.tvDeviceLabel.text = "设备:${group.deviceId}"
if (!bound) {
if (innerAdapter == null) {
val gridLayoutManager = GridLayoutManager(b.root.context, 6).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
// 每个格子占1 span,共6列
override fun getSpanSize(position: Int) = 1
}
}
b.rvScales.layoutManager = gridLayoutManager
b.rvScales.itemAnimator = null
b.rvScales.adapter = Scale18InnerAdapter()
bound = true
innerAdapter = Scale18GridAdapter().apply {
onItemClick = { scale ->
showTareDialog(deviceId = scale.deviceId, ip = scale.ip, address = scale.address)
}
}
b.rvScales.adapter = innerAdapter
}
val scaleByAddress = group.scales.associateBy { it.address }
val ordered = SCALE_ORDER_18.map { scaleByAddress[it] }
(b.rvScales.adapter as Scale18InnerAdapter).update(ordered)
updateScales(group.scales)
}
/** 仅更新秤数据,不触发外层任何 notify */
fun updateScales(scales: List<ScaleData>) {
innerAdapter?.updateByAddress(scales)
}
}
// ── ViewHolder: 1个秤 ──────────────────────────────────────────────────
private inner class Scale1VH(val b: ListTypeScale1Binding) : RecyclerView.ViewHolder(b.root) {
init {
b.llScaleContainer.setOnClickListener {
val scale = b.llScaleContainer.tag as? ScaleData ?: return@setOnClickListener
showTareDialog(deviceId = scale.deviceId, ip = scale.ip, address = scale.address)
}
}
@SuppressLint("SetTextI18n")
fun bind(group: DeviceGroup) {
b.tvDeviceIp.text = "IP${group.ip.ifEmpty { "未知" }}"
b.tvDeviceLabel.text = "设备:${group.deviceId}"
val scale = group.scales.firstOrNull()
updateScales(group.scales)
}
fun updateScales(scales: List<ScaleData>) {
val scale = scales.firstOrNull()
b.llScaleContainer.tag = scale
b.tvAddr.text = scale?.let { "${it.address}" } ?: ""
b.tvWeight.text = scale?.let { "${it.weight} g" } ?: ""
b.tvState.text = stateStr(scale?.state)
}
}
// ── 22格内嵌 Adapter ───────────────────────────────────────────────────
private inner class Scale22InnerAdapter(
private val largeSize: Int,
private val smallSize: Int
) : BaseQuickAdapter<ScaleData, Scale22InnerAdapter.VH>(mutableListOf()) {
inner class VH(val b: ListItemScale22RowBinding) : QuickViewHolder(b.root)
// 用 address=0 的哨兵表示空位
private val empty = ScaleData("", 0, 0.0, -1, 0)
fun update(newList: List<ScaleData?>) {
val filled = newList.map { it ?: empty }
if (items.isEmpty()) {
submitList(filled)
} else {
filled.forEachIndexed { i, scale ->
if (items.getOrNull(i) != scale) set(i, scale)
}
}
}
override fun getItemViewType(position: Int, list: List<ScaleData>) = if (position < 4) 1 else 0
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val b = ListItemScale22RowBinding.inflate(LayoutInflater.from(context), parent, false)
val cellSize = if (viewType == 1) largeSize else smallSize
(b.root.layoutParams as? FlexboxLayoutManager.LayoutParams)?.apply {
width = cellSize
height = cellSize
setMargins(2.dp, 2.dp, 2.dp, 2.dp)
}
val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt()
b.tvAddr.textSize = (baseSp * 0.16f).coerceIn(9f, 14f)
b.tvWeight.textSize = (baseSp * 0.22f).coerceIn(12f, 20f)
b.tvState.textSize = (baseSp * 0.14f).coerceIn(8f, 12f)
return VH(b)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: VH, position: Int, item: ScaleData?) {
val valid = (item?.address ?: 0) != 0
holder.b.tvAddr.text = if (valid) "${item!!.address}" else ""
holder.b.tvWeight.text = if (valid) "${item!!.weight}g" else ""
holder.b.tvState.text = if (valid) stateStr(item!!.state) else ""
}
}
// ── 18格内嵌 Adapter ───────────────────────────────────────────────────
private inner class Scale18InnerAdapter : BaseQuickAdapter<ScaleData, Scale18InnerAdapter.VH>(mutableListOf()) {
inner class VH(val b: ListItemScale22RowBinding) : QuickViewHolder(b.root)
private val empty = ScaleData("", 0, 0.0, -1, 0)
fun update(newList: List<ScaleData?>) {
val filled = newList.map { it ?: empty }
if (items.isEmpty()) {
submitList(filled)
} else {
filled.forEachIndexed { i, scale ->
if (items.getOrNull(i) != scale) set(i, scale)
}
}
}
override fun getItemViewType(position: Int, list: List<ScaleData>) = if (position < 6) 0 else 1
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val b = ListItemScale22RowBinding.inflate(LayoutInflater.from(context), parent, false)
// 第1行 margin=12dp(格子视觉更小),第2-3行 margin=2dp
val margin = if (viewType == 0) 12.dp else 2.dp
val screenWidth = parent.resources.displayMetrics.widthPixels
val cellSize = (screenWidth - 6 * margin * 2 - 10.dp) / 6
b.root.layoutParams = (b.root.layoutParams as ViewGroup.MarginLayoutParams).also {
it.width = cellSize
it.height = cellSize
it.setMargins(margin, margin, margin, margin)
}
val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt()
b.tvAddr.textSize = (baseSp * 0.16f).coerceIn(9f, 14f)
b.tvWeight.textSize = (baseSp * 0.22f).coerceIn(12f, 20f)
b.tvState.textSize = (baseSp * 0.14f).coerceIn(8f, 12f)
return VH(b)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: VH, position: Int, item: ScaleData?) {
val valid = (item?.address ?: 0) != 0
holder.b.tvAddr.text = if (valid) "${item!!.address}" else ""
holder.b.tvWeight.text = if (valid) "${item!!.weight}g" else ""
holder.b.tvState.text = if (valid) stateStr(item!!.state) else ""
}
}
// ── 工具方法 ───────────────────────────────────────────────────────────
private fun stateStr(state: Int?) = when (state) {
@@ -365,7 +356,7 @@ class MasterScaleActivity : BaseActivity() {
else -> "$state"
}
/** 创建正方形秤格子(用于 Scale2VH) */
/** 创建正方形秤格子(用于 Scale2VH 初始化 */
@SuppressLint("SetTextI18n")
private fun makeScaleCell(scale: ScaleData, cellSize: Int): LinearLayout {
val margin = 2.dp
@@ -380,9 +371,18 @@ class MasterScaleActivity : BaseActivity() {
addView(makeCellText("${scale.address}", (baseSize * 0.16f).coerceIn(9f, 14f).toInt()))
addView(makeCellText("${scale.weight}g", (baseSize * 0.22f).coerceIn(12f, 20f).toInt(), bold = true))
addView(makeCellText(stateStr(scale.state), (baseSize * 0.14f).coerceIn(8f, 12f).toInt()))
tag = scale
}
}
/** 仅更新格子内文字,不重建 view(用于 Scale2VH 数据刷新) */
@SuppressLint("SetTextI18n")
private fun updateScaleCell(cell: LinearLayout, scale: ScaleData) {
(cell.getChildAt(0) as? TextView)?.text = "${scale.address}"
(cell.getChildAt(1) as? TextView)?.text = "${scale.weight}g"
(cell.getChildAt(2) as? TextView)?.text = stateStr(scale.state)
}
private fun makeCellText(text: String, spSize: Int, bold: Boolean = false): TextView =
TextView(this).apply {
this.text = text
@@ -395,4 +395,23 @@ class MasterScaleActivity : BaseActivity() {
gravity = Gravity.CENTER
includeFontPadding = false
}
/**
* 弹出清零确认对话框
* @param deviceId 目标设备 ID
* @param ip 设备 IP
* @param address 目标秤地址
*/
private fun showTareDialog(deviceId: String, ip: String, address: Int) {
CommonDialog(this)
.setTitle("清零确认")
.setContent("设备 IP${ip.ifEmpty { "未知" }}\n确认对秤 $address 执行清零操作?")
.setNegativeButton("取消")
.setPositiveButton("确认") {
val sent = ScaleServiceManager.sendTare(deviceId, address)
if (!sent) toast("设备未连接,清零失败")
}
.show()
}
}
@@ -11,9 +11,7 @@ import com.shuwei.dish.match.R
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.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
@@ -159,6 +157,10 @@ class PrepareCookActivity : BaseActivity() {
}
private val isTakingPhoto = AtomicBoolean(false)
private var isRecognizeOpen = false
// 页面可见时允许识别,onPause 后禁止,onResume 后恢复
private var isRecognizeEnabled = true
private var lastPhotoUri: Uri? = null
private var lastWeight = 0.0
private var currentWeight = 0.0
@@ -169,10 +171,11 @@ class PrepareCookActivity : BaseActivity() {
private fun recognizeFood(weight: Double) {
this.currentWeight = weight
if (weight < WEIGHT_RECOGNIZE_VALUE) {
if (recognizeDialog.isShowing) recognizeDialog.dismiss()
isRecognizeOpen = false
FoodRecognizeActivity.close()
return
}
if (isTakingPhoto.get() || recognizeDialog.isShowing || abs(lastWeight - weight) <= WEIGHT_CHANGE_VALUE) return
if (!isRecognizeEnabled || isTakingPhoto.get() || isRecognizeOpen || abs(lastWeight - weight) <= WEIGHT_CHANGE_VALUE) return
isTakingPhoto.set(true)
cameraUtils.takePhoto(
succCallback = cameraSuccessCallback,
@@ -195,9 +198,13 @@ class PrepareCookActivity : BaseActivity() {
// materialType =
isNewDishType = true
isOriginalData = false
useWeight = lastWeight
isSetFinished = true
})
onFoodItemClick(list.size - 1)
binding.rvDishPartList.smoothScrollToPosition(list.size - 1)
val positon = list.size - 1
dishPartAdapter.notifyItemInserted(positon)
onFoodItemClick(positon)
binding.rvDishPartList.smoothScrollToPosition(positon)
}
/**
@@ -239,18 +246,12 @@ class PrepareCookActivity : BaseActivity() {
}
private fun showRemindDialog() {
val remindBinding = LayoutFoodRemindBinding.inflate(layoutInflater)
CommonDialog(this).apply {
messageView = remindBinding.root
leftText = "返回调整"
rightText = "确认无误"
onLeftClick = { dismiss() }
onRightClick = {
openSubmitPage()
dismiss()
}
onDismiss = { hideStatusBar() }
}.show()
CommonDialog(this)
.setTitle("温馨提示")
.setNegativeButton("返回调整")
.setPositiveButton("确认无误") { openSubmitPage() }
.setOnDismissCallback { hideStatusBar() }
.show()
}
@@ -305,18 +306,24 @@ class PrepareCookActivity : BaseActivity() {
private var firstReqSize = 0
private val list = mutableListOf<CookFoodGoodsEntity>()
private val recognizeDialog by lazy {
FoodRecognizeDialog(
activity = this,
onDismiss = { isTakingPhoto.set(false) },
onItemSelected = foodSelectCallback
)
}
private val dishPartAdapter by lazy {
DishPartAdapter(list).apply {
setOnItemClickListener { _, _, positon ->
onFoodItemClick(positon)
}
addOnItemChildClickListener(R.id.ivClearIcon) { _, _, positon ->
if (list[positon].isOriginalData) {
list[positon].run {
isNewDishType = false
isOriginalData = false
useWeight = 0.0
isSetFinished = false
}
notifyItemChanged(positon)
return@addOnItemChildClickListener
}
removeAt(positon)
}
}
}
@@ -395,11 +402,14 @@ class PrepareCookActivity : BaseActivity() {
override fun onResume() {
super.onResume()
isRecognizeEnabled = true
isRecognizeOpen = false
cameraUtils.bind()
}
override fun onPause() {
super.onPause()
isRecognizeEnabled = false
cameraUtils.unbind()
}
@@ -431,20 +441,13 @@ class PrepareCookActivity : BaseActivity() {
* 未保存数据提醒
*/
private fun saveDataRemindDialog() {
val remindBinding = LayoutFoodRemindBinding.inflate(layoutInflater)
remindBinding.tvDialogTitle.text = "温馨提示"
remindBinding.tvDialogContent.text = "您好,当前页面存在尚未保存的数据,确认返回上页吗?"
CommonDialog(this).apply {
messageView = remindBinding.root
leftText = "取消"
rightText = "确认"
onLeftClick = { dismiss() }
onRightClick = {
dismiss()
finish()
}
onDismiss = { hideStatusBar() }
}.show()
CommonDialog(this)
.setTitle("温馨提示")
.setContent("您好,当前页面存在尚未保存的数据,确认返回吗?")
.setNegativeButton("取消")
.setPositiveButton("确认") { finish() }
.setOnDismissCallback { hideStatusBar() }
.show()
}
/**
@@ -452,6 +455,7 @@ class PrepareCookActivity : BaseActivity() {
*/
private val cameraSuccessCallback: (Uri) -> Unit = { uri ->
Log.d(TAG, "takePhoto success")
lastPhotoUri = uri
lifecycleScope.launch {
withContext(Dispatchers.IO) {
val bitmap = ImageUtil.uriToBitmap(this@PrepareCookActivity, uri)
@@ -499,7 +503,13 @@ class PrepareCookActivity : BaseActivity() {
foodId = index.toString()
))
}
recognizeDialog.loadData(foodList)
FoodRecognizeActivity.start(
activity = this,
imageUri = lastPhotoUri?.toString(),
goodsList = ArrayList(foodList),
onResult = { item -> item?.let { foodSelectCallback(it) } }
)
isRecognizeOpen = true
isTakingPhoto.set(false)
lastWeight = currentWeight
}
@@ -15,7 +15,7 @@ import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.ActivitySamplingBinding
import com.shuwei.dish.match.databinding.LayoutEmptyViewBinding
import com.shuwei.dish.match.db.AppRepository
import com.shuwei.dish.match.dialog.DialogTool
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.FoodRecord
import com.shuwei.dish.match.utils.DateTimeUtil
@@ -378,16 +378,12 @@ class SamplingListActivity : BaseActivity() {
}
private fun showDeviceConfigDialog() {
DialogTool.load(
context = this,
title = "温馨提示",
content = "请先在设备配置页面进行调料设置",
leftBtnText = "取消",
rightBtnText = "去设置",
leftBtnClick = {},
rightBtnClick = {
startActivity<SettingActivity>()
})
CommonDialog(this)
.setTitle("温馨提示")
.setContent("请先在设备配置页面进行调料设置")
.setNegativeButton("取消")
.setPositiveButton("去设置") { startActivity<SettingActivity>() }
.show()
}
private var isVisible = false
@@ -9,7 +9,7 @@ import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.ActivitySelectDishBinding
import com.shuwei.dish.match.db.AppRepository
import com.shuwei.dish.match.dialog.DialogTool
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.FoodRecord
import com.shuwei.dish.match.ui.fragment.DishListFragment
@@ -95,17 +95,12 @@ class SelectDishActivity : BaseActivity() {
}
private fun showDeviceConfigDialog() {
DialogTool.load(
context = this,
title = "温馨提示",
content = "请先在设备配置页面进行调料设置",
leftBtnText = "取消",
rightBtnText = "去设置",
leftBtnClick = {},
rightBtnClick = {
startActivity<SettingActivity>()
}
)
CommonDialog(this)
.setTitle("温馨提示")
.setContent("请先在设备配置页面进行调料设置")
.setNegativeButton("取消")
.setPositiveButton("去设置") { startActivity<SettingActivity>() }
.show()
}
fun addViewListener() {
@@ -2,33 +2,45 @@ package com.shuwei.dish.match.ui
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import android.widget.LinearLayout
import androidx.activity.addCallback
import com.google.android.flexbox.AlignItems
import com.google.android.flexbox.FlexDirection
import com.google.android.flexbox.FlexWrap
import com.google.android.flexbox.FlexboxLayoutManager
import com.shuwei.dish.match.adapter.Scale18GridAdapter
import com.shuwei.dish.match.adapter.Scale22GridAdapter
import com.shuwei.dish.match.adapter.ScaleRowAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.databinding.ActivitySlaveBinding
import com.shuwei.dish.match.databinding.ListItemScaleRowBinding
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.scale.ScaleData
import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.scale.ScaleServiceManager
import com.shuwei.dish.match.utils.NetworkUtil
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp
/**
* 子设备专属页面(小屏幕设备使用)
* 仅展示本机秤的实时数据和主设备连接状态
* 布局简洁,适配小屏幕
* 22个秤和18个秤使用与 MasterScaleActivity 相同的网格布局,其余使用线性列表
*/
class SlaveActivity : BaseActivity() {
private lateinit var binding: ActivitySlaveBinding
/** 本机秤数据列表:address → (weight, state) */
private data class ScaleItem(val address: Int, val weight: Double, val state: Int)
/** 本机秤数据列表:线性列表模式使用 */
private val linearAdapter = ScaleRowAdapter()
private val scaleList = mutableListOf<ScaleItem>()
private val adapter = ScaleAdapter(scaleList)
/** 22个秤网格 adapter */
private var scale22Adapter: Scale22GridAdapter? = null
/** 18个秤网格 adapter */
private var scale18Adapter: Scale18GridAdapter? = null
/** 已连接的主设备数量(有连接即代表主设备在线) */
private var masterConnected = false
@@ -41,23 +53,73 @@ class SlaveActivity : BaseActivity() {
binding.tvDeviceId.text = "设备:${GlobalData.deviceId}"
binding.tvDeviceIp.text = "IP${getLocalIpAddress()}"
binding.rvScaleList.layoutManager = LinearLayoutManager(this)
binding.rvScaleList.itemAnimator = null // 关闭默认动画,避免数据频繁更新时出现闪烁错乱
binding.rvScaleList.adapter = adapter
setupScaleList()
updateMasterStatus(ScaleServiceManager.isMasterConnected)
listenLocalScales()
listenMasterConnection()
addBackKeyListener()
}
/**
* 根据 deviceId 选择对应的布局方式:
* - 22个秤:FlexboxLayoutManager + Scale22GridAdapter
* - 18个秤:GridLayoutManager(6列) + Scale18GridAdapter
* - 其他:LinearLayoutManager + 线性列表
*/
private fun setupScaleList() {
binding.rvScaleList.itemAnimator = null
when (GlobalData.deviceId) {
ScaleDeviceConfig.DEVICE_ID_22 -> {
val screenWidth = resources.displayMetrics.widthPixels
val usable = screenWidth - 8 * 4.dp - 10.dp
val smallSize = usable / 9
val largeSize = (usable - smallSize * 6) / 2
val rvHeight = smallSize * 3 + 3 * 4.dp
binding.rvScaleList.updateLayoutParams<LinearLayout.LayoutParams> {
height = rvHeight
weight = 0f
topMargin = 20.dp
}
binding.rvScaleList.layoutManager = FlexboxLayoutManager(this).apply {
flexDirection = FlexDirection.COLUMN
flexWrap = FlexWrap.WRAP
alignItems = AlignItems.FLEX_START
}
scale22Adapter = Scale22GridAdapter(largeSize, smallSize).also {
it.onItemClick = { scale -> showTareDialog(scale.address) }
}
binding.rvScaleList.adapter = scale22Adapter
}
ScaleDeviceConfig.DEVICE_ID_18 -> {
binding.rvScaleList.updateLayoutParams<LinearLayout.LayoutParams> { topMargin = 0 }
binding.rvScaleList.layoutManager = GridLayoutManager(this, 6).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int) = 1
}
}
scale18Adapter = Scale18GridAdapter().also {
it.onItemClick = { scale -> showTareDialog(scale.address) }
}
binding.rvScaleList.adapter = scale18Adapter
}
else -> {
binding.rvScaleList.updateLayoutParams<LinearLayout.LayoutParams> { topMargin = 20.dp }
binding.rvScaleList.layoutManager = LinearLayoutManager(this)
linearAdapter.setOnItemClickListener { _, _, position ->
val item = linearAdapter.items.getOrNull(position) ?: return@setOnItemClickListener
showTareDialog(item.address)
}
binding.rvScaleList.adapter = linearAdapter
}
}
}
/**
* 禁用返回键,防止退回 InitActivity 后重复打开本页
*/
private fun addBackKeyListener() {
onBackPressedDispatcher.addCallback(this) {
// 禁用返回键,不执行任何操作
}
onBackPressedDispatcher.addCallback(this) { }
}
/**
@@ -71,19 +133,39 @@ class SlaveActivity : BaseActivity() {
/**
* 监听本机 WeightUtil 回调,实时刷新秤列表
* 22/18个秤走网格 adapter,其余走线性列表
*/
private fun listenLocalScales() {
WeightUtil.addWeightListener(TAG) { address, state, weight ->
runOnUiThread {
val idx = scaleList.indexOfFirst { it.address == address }
val item = ScaleItem(address, weight, state)
if (idx >= 0) {
scaleList[idx] = item
adapter.notifyItemChanged(idx)
} else {
scaleList.add(item)
scaleList.sortBy { it.address }
adapter.notifyDataSetChanged()
when {
scale22Adapter != null -> {
// 用最新数据更新缓存后整体刷新网格
val existing = scale22Adapter!!.items.toMutableList()
val idx = existing.indexOfFirst { it.address == address }
val newData = ScaleData(GlobalData.deviceId, address, weight, state, System.currentTimeMillis())
if (idx >= 0) existing[idx] = newData else existing.add(newData)
scale22Adapter!!.updateByAddress(existing)
}
scale18Adapter != null -> {
val existing = scale18Adapter!!.items.toMutableList()
val idx = existing.indexOfFirst { it.address == address }
val newData = ScaleData(GlobalData.deviceId, address, weight, state, System.currentTimeMillis())
if (idx >= 0) existing[idx] = newData else existing.add(newData)
scale18Adapter!!.updateByAddress(existing)
}
else -> {
val item = ScaleRowAdapter.ScaleItem(address, weight, state)
val list = linearAdapter.items.toMutableList()
val idx = list.indexOfFirst { it.address == address }
if (idx >= 0) {
list[idx] = item
} else {
list.add(item)
list.sortBy { it.address }
}
linearAdapter.submitList(list)
}
}
}
}
@@ -91,7 +173,6 @@ class SlaveActivity : BaseActivity() {
/**
* 更新主设备连接状态文字
* 由 ScaleWebSocketServer 的 onOpen/onClose 回调驱动(可在后续扩展中接入)
*/
@SuppressLint("SetTextI18n")
fun updateMasterStatus(connected: Boolean) {
@@ -112,37 +193,27 @@ class SlaveActivity : BaseActivity() {
const val TAG = "SlaveActivity"
}
/**
* 弹出清零确认对话框
* @param address 目标秤地址
*/
private fun showTareDialog(address: Int) {
CommonDialog(this)
.setTitle("清零确认")
.setContent("确认对秤 $address 执行清零操作?")
.setNegativeButton("取消")
.setPositiveButton("确认") {
WeightUtil.tareTwo(address)
}
.show()
}
/**
* 获取本机局域网 IP,委托给 NetworkUtil 统一处理版本兼容
* 获取失败时返回"未知"
*/
private fun getLocalIpAddress(): String {
return NetworkUtil.getLocalIpAddress(this).ifEmpty { "未知" }
}
private class ScaleAdapter(private val data: List<ScaleItem>) :
RecyclerView.Adapter<ScaleAdapter.VH>() {
inner class VH(val binding: ListItemScaleRowBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = VH(
ListItemScaleRowBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
override fun getItemCount() = data.size
override fun onBindViewHolder(holder: VH, position: Int) {
val item = data[position]
val stateStr = when (item.state) {
WeightUtil.STATE_STABLE -> "稳定"
WeightUtil.STATE_UNSTABLE -> "不稳定"
WeightUtil.STATE_OVER_WEIGHT -> "超量"
else -> "${item.state}"
}
holder.binding.tvScaleLabel.text = "${item.address}"
holder.binding.tvWeight.text = "${item.weight} g"
holder.binding.tvState.text = stateStr
}
}
}
@@ -20,7 +20,6 @@ import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.base.BaseFragment
import com.shuwei.dish.match.databinding.FragmentDeviceConfigBinding
import com.shuwei.dish.match.db.AppRepository
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.dialog.SeasoningSearchDialog
import com.shuwei.dish.match.entity.SeasoningEntity
import com.shuwei.dish.match.entity.ResetReasoningRecord
@@ -39,7 +38,6 @@ 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 com.shuwei.dish.match.databinding.LayoutFoodRemindBinding
import com.shuwei.dish.match.ui.SamplingListActivity
import com.shuwei.dish.match.ui.SelectDishActivity
import com.shuwei.dish.match.utils.SpTool
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

@@ -21,8 +21,6 @@
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:gravity="center_vertical"
android:paddingTop="30dp"
android:paddingBottom="30dp"
android:textColor="@color/black"
android:textSize="28sp"
app:layout_constraintStart_toStartOf="parent"
@@ -0,0 +1,86 @@
<?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="match_parent"
android:orientation="vertical"
android:padding="16dp"
tools:background="@color/white">
<!-- 食材照片,权重占满剩余空间 -->
<ImageView
android:id="@+id/ivFoodPhoto"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scaleType="centerCrop"
tools:ignore="ContentDescription"
tools:src="@color/bg_color" />
<!-- 识别到的食材列表,无数据时隐藏 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:maxHeight="200dp"
android:nestedScrollingEnabled="false"
android:overScrollMode="never"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:itemCount="4"
tools:listitem="@layout/list_item_search_goods_info"
tools:visibility="visible" />
<!-- 选中的食材名称 -->
<TextView
android:id="@+id/tvSelectedFood"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginTop="12dp"
android:gravity="center"
android:text="-"
android:textColor="@color/black333"
android:textSize="32sp" />
<!-- 秤1重量 -->
<TextView
android:id="@+id/tvWeight"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginTop="8dp"
android:background="@drawable/shape_white_12_corners"
android:gravity="center"
android:text="0.0 g"
android:textColor="@color/black333"
android:textSize="32sp" />
<!-- 更换食材 -->
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnChangeFood"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginTop="30dp"
android:background="@drawable/shape_green_bg"
android:text="更换食材"
android:textColor="@color/white"
android:textSize="32sp"
android:textStyle="bold" />
<!-- 取消 -->
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCancel"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginTop="30dp"
android:layout_marginBottom="30dp"
android:background="@drawable/shape_white_12_corners"
android:text="取消"
android:textColor="@color/black333"
android:textSize="32sp"
android:textStyle="bold" />
</LinearLayout>
+28 -24
View File
@@ -4,20 +4,36 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="12dp"
android:paddingHorizontal="5dp"
tools:background="@color/bg_color">
<!-- 本机IP地址 -->
<TextView
android:id="@+id/tvDeviceIp"
android:layout_width="wrap_content"
<!-- IP 与主设备状态同行显示 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="0dp"
android:textSize="22sp"
android:textStyle="bold"
android:textColor="@color/home_title"
tools:text="IP192.168.1.100" />
android:orientation="horizontal"
android:gravity="center">
<!-- 本机IP地址 -->
<TextView
android:id="@+id/tvDeviceIp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/home_title"
tools:text="IP192.168.1.100" />
<!-- 主设备连接状态 -->
<TextView
android:id="@+id/tvMasterStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:textSize="16sp"
tools:text="主设备:未连接" />
</LinearLayout>
<!-- 设备信息 -->
<TextView
@@ -25,22 +41,10 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="0dp"
android:textSize="18sp"
android:textSize="16sp"
android:textColor="@color/home_sub_title"
tools:text="设备:device_02" />
<!-- 主设备连接状态 -->
<TextView
android:id="@+id/tvMasterStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="0dp"
android:layout_marginBottom="12dp"
android:textSize="16sp"
tools:text="主设备:未连接" />
<!-- 本机秤数据列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvScaleList"
+26 -10
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
@@ -8,10 +7,29 @@
android:background="@drawable/shape_white_30_corners"
android:orientation="vertical">
<FrameLayout
android:id="@+id/llContent"
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
android:layout_marginStart="40dp"
android:layout_marginTop="55dp"
android:layout_marginEnd="40dp"
android:gravity="center"
android:textColor="@color/black"
android:textSize="36sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="40dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="40dp"
android:layout_marginBottom="55dp"
android:gravity="center"
android:textColor="@color/black999"
android:textSize="26sp" />
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
@@ -20,7 +38,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_height="100dp"
android:gravity="center_vertical"
android:orientation="horizontal">
@@ -32,8 +50,7 @@
android:gravity="center"
android:background="@drawable/ripple_effect_light2"
android:textColor="@color/dish_green"
android:textSize="30sp"
tools:text="取消" />
android:textSize="30sp" />
<com.google.android.material.divider.MaterialDivider
android:layout_width="1dp"
@@ -49,8 +66,7 @@
android:textColor="@color/dish_green"
android:background="@drawable/ripple_effect_light2"
android:textSize="30sp"
android:textStyle="bold"
tools:text="确定" />
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
@@ -46,11 +46,22 @@
<ImageView
android:id="@+id/ivOperateIcon"
android:layout_width="60dp"
android:layout_width="80dp"
android:layout_height="60dp"
android:layout_marginEnd="30dp"
tools:src="@drawable/ic_dish_selected"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/ivClearIcon"
android:layout_marginEnd="10dp"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/ivClearIcon"
android:layout_width="80dp"
android:layout_height="60dp"
tools:src="@drawable/ic_dish_clear"
android:layout_marginEnd="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />