feat(scale): 实现调料槽位配置持久化及主从设备数据同步

- 新增 SeasoningSlotEntity / SeasoningSlotDao,独立存储格子调料配置
- AppDatabase 升级至版本 8,添加 MIGRATION_7_8
- ScaleEvent 新增 TYPE_SEASONING_CONFIG 和 SlotConfig,用于主从同步
- SeasoningConfigFragment 选择调料后写 Room 并通过 wsClient 推送给子设备
- ScaleWebSocketClient 新增 sendToAllSlaves / sendToDevice / onDeviceConnected
- ScaleWebSocketServer 扩展 onMessage 支持接收主设备下发的配置事件
- ScaleServiceManager 暴露 sendSeasoningConfig / onSeasoningConfig;子设备连接时自动推送全量配置
- MasterScaleActivity 启动时从 Room 加载 slotNameMap,注入 ScaleData.name 展示调料名
- SlaveActivity 新增 slotNameMap 防止重量回调覆盖调料名;监听 onSeasoningConfig 写 Room 并刷新 adapter
- Adapter 新增 tvAddress 和 showAddress 参数,仅 MasterScaleActivity 显示秤地址行;tvName 空值统一显示"-"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 15:27:28 +08:00
co-authored by Claude Sonnet 4.6
parent a81e914b63
commit b16d7d19cd
13 changed files with 390 additions and 40 deletions
@@ -21,9 +21,11 @@ import com.shuwei.dish.match.utils.ext.dp
* *
* @param showNameOnly true = 调料配置模式,仅显示 tvName,隐藏 tvWeight/tvState * @param showNameOnly true = 调料配置模式,仅显示 tvName,隐藏 tvWeight/tvState
* false = 秤总览模式,三行全部显示 * false = 秤总览模式,三行全部显示
* @param showAddress true = 显示秤地址行(tvAddress),仅 MasterScaleActivity 使用
*/ */
class Seasoning18GridAdapter( class Seasoning18GridAdapter(
private val showNameOnly: Boolean = false private val showNameOnly: Boolean = false,
private val showAddress: Boolean = false
) : BaseQuickAdapter<ScaleData, Seasoning18GridAdapter.VH>(mutableListOf()) { ) : BaseQuickAdapter<ScaleData, Seasoning18GridAdapter.VH>(mutableListOf()) {
/** 空位哨兵,address=0 表示无效 */ /** 空位哨兵,address=0 表示无效 */
@@ -53,9 +55,12 @@ class Seasoning18GridAdapter(
else it.setMargins(margin, margin, margin, margin) else it.setMargins(margin, margin, margin, margin)
} }
val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt() val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt()
b.tvAddress.textSize = (baseSp * 0.14f).coerceIn(8f, 12f)
b.tvName.textSize = (baseSp * 0.18f).coerceIn(10f, 16f) b.tvName.textSize = (baseSp * 0.18f).coerceIn(10f, 16f)
b.tvWeight.textSize = (baseSp * 0.22f).coerceIn(12f, 20f) b.tvWeight.textSize = (baseSp * 0.22f).coerceIn(12f, 20f)
b.tvState.textSize = (baseSp * 0.14f).coerceIn(8f, 12f) b.tvState.textSize = (baseSp * 0.14f).coerceIn(8f, 12f)
// 秤地址行:仅 MasterScaleActivity 显示
b.tvAddress.visibility = if (showAddress) View.VISIBLE else View.GONE
// 调料配置模式隐藏重量和状态行 // 调料配置模式隐藏重量和状态行
if (showNameOnly) { if (showNameOnly) {
b.tvWeight.visibility = View.GONE b.tvWeight.visibility = View.GONE
@@ -75,31 +80,31 @@ class Seasoning18GridAdapter(
override fun onBindViewHolder(holder: VH, position: Int, item: ScaleData?) { override fun onBindViewHolder(holder: VH, position: Int, item: ScaleData?) {
val valid = (item?.address ?: 0) != 0 val valid = (item?.address ?: 0) != 0
if (!valid) { if (!valid) {
holder.b.tvName.text = if (showNameOnly) "-" else "" holder.b.tvAddress.text = ""
holder.b.tvName.text = "-"
holder.b.tvWeight.text = "" holder.b.tvWeight.text = ""
holder.b.tvState.text = "" holder.b.tvState.text = ""
} else { } else {
holder.b.tvName.text = when { holder.b.tvAddress.text = "${item!!.address}"
!item!!.name.isNullOrBlank() -> item.name holder.b.tvName.text = if (!item.name.isNullOrBlank()) item.name else "-"
showNameOnly -> "-" holder.b.tvWeight.text = "${item.weight}g"
else -> "${item.address}"
}
holder.b.tvWeight.text = "${item!!.weight}g"
holder.b.tvState.text = stateStr(item.state) holder.b.tvState.text = stateStr(item.state)
} }
// 高亮格子:红底白字;其余恢复默认
if (position == highlightedPosition) { if (position == highlightedPosition) {
holder.b.root.setBackgroundColor(ContextCompat.getColor(holder.b.root.context, R.color.red_ff4444)) holder.b.root.setBackgroundColor(ContextCompat.getColor(holder.b.root.context, R.color.red_ff4444))
val white = ContextCompat.getColor(holder.b.root.context, R.color.white) val white = ContextCompat.getColor(holder.b.root.context, R.color.white)
holder.b.tvAddress.setTextColor(white)
holder.b.tvName.setTextColor(white) holder.b.tvName.setTextColor(white)
holder.b.tvWeight.setTextColor(white) holder.b.tvWeight.setTextColor(white)
holder.b.tvState.setTextColor(white) holder.b.tvState.setTextColor(white)
} else { } else {
holder.b.root.setBackgroundResource(R.drawable.shape_scale_cell) holder.b.root.setBackgroundResource(R.drawable.shape_scale_cell)
val normal = ContextCompat.getColor(holder.b.root.context, R.color.home_title) val normal = ContextCompat.getColor(holder.b.root.context, R.color.home_title)
holder.b.tvName.setTextColor(normal) val sub = ContextCompat.getColor(holder.b.root.context, R.color.home_sub_title)
holder.b.tvAddress.setTextColor(sub)
holder.b.tvName.setTextColor(sub)
holder.b.tvWeight.setTextColor(normal) holder.b.tvWeight.setTextColor(normal)
holder.b.tvState.setTextColor(normal) holder.b.tvState.setTextColor(sub)
} }
} }
@@ -24,11 +24,13 @@ import com.shuwei.dish.match.utils.ext.dp
* @param smallSize 其余小格子的尺寸(px) * @param smallSize 其余小格子的尺寸(px)
* @param showNameOnly true = 调料配置模式,仅显示 tvName,隐藏 tvWeight/tvState * @param showNameOnly true = 调料配置模式,仅显示 tvName,隐藏 tvWeight/tvState
* false = 秤总览模式,三行全部显示 * false = 秤总览模式,三行全部显示
* @param showAddress true = 显示秤地址行(tvAddress),仅 MasterScaleActivity 使用
*/ */
class Seasoning22GridAdapter( class Seasoning22GridAdapter(
private val largeSize: Int, private val largeSize: Int,
private val smallSize: Int, private val smallSize: Int,
private val showNameOnly: Boolean = false private val showNameOnly: Boolean = false,
private val showAddress: Boolean = false
) : BaseQuickAdapter<ScaleData, Seasoning22GridAdapter.VH>(mutableListOf()) { ) : BaseQuickAdapter<ScaleData, Seasoning22GridAdapter.VH>(mutableListOf()) {
/** 空位哨兵,address=0 表示无效 */ /** 空位哨兵,address=0 表示无效 */
@@ -55,9 +57,12 @@ class Seasoning22GridAdapter(
setMargins(2.dp, 2.dp, 2.dp, 2.dp) setMargins(2.dp, 2.dp, 2.dp, 2.dp)
} }
val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt() val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt()
b.tvAddress.textSize = (baseSp * 0.14f).coerceIn(8f, 12f)
b.tvName.textSize = (baseSp * 0.18f).coerceIn(10f, 16f) b.tvName.textSize = (baseSp * 0.18f).coerceIn(10f, 16f)
b.tvWeight.textSize = (baseSp * 0.22f).coerceIn(12f, 20f) b.tvWeight.textSize = (baseSp * 0.22f).coerceIn(12f, 20f)
b.tvState.textSize = (baseSp * 0.14f).coerceIn(8f, 12f) b.tvState.textSize = (baseSp * 0.14f).coerceIn(8f, 12f)
// 秤地址行:仅 MasterScaleActivity 显示
b.tvAddress.visibility = if (showAddress) View.VISIBLE else View.GONE
// 调料配置模式隐藏重量和状态行 // 调料配置模式隐藏重量和状态行
if (showNameOnly) { if (showNameOnly) {
b.tvWeight.visibility = View.GONE b.tvWeight.visibility = View.GONE
@@ -77,32 +82,32 @@ class Seasoning22GridAdapter(
override fun onBindViewHolder(holder: VH, position: Int, item: ScaleData?) { override fun onBindViewHolder(holder: VH, position: Int, item: ScaleData?) {
val valid = (item?.address ?: 0) != 0 val valid = (item?.address ?: 0) != 0
if (!valid) { if (!valid) {
holder.b.tvName.text = if (showNameOnly) "-" else "" holder.b.tvAddress.text = ""
holder.b.tvName.text = "-"
holder.b.tvWeight.text = "" holder.b.tvWeight.text = ""
holder.b.tvState.text = "" holder.b.tvState.text = ""
} else { } else {
// 有名称显示名称,无名称:调料配置显示"-",秤总览显示秤地址编号 holder.b.tvAddress.text = "${item!!.address}"
holder.b.tvName.text = when { holder.b.tvName.text = if (!item.name.isNullOrBlank()) item.name else "-"
!item!!.name.isNullOrBlank() -> item.name holder.b.tvWeight.text = "${item.weight}g"
showNameOnly -> "-"
else -> "${item.address}"
}
holder.b.tvWeight.text = "${item!!.weight}g"
holder.b.tvState.text = stateStr(item.state) holder.b.tvState.text = stateStr(item.state)
} }
// 高亮格子:红底白字;其余恢复默认 // 高亮格子:红底白字;其余恢复默认
if (position == highlightedPosition) { if (position == highlightedPosition) {
holder.b.root.setBackgroundColor(ContextCompat.getColor(holder.b.root.context, R.color.red_ff4444)) holder.b.root.setBackgroundColor(ContextCompat.getColor(holder.b.root.context, R.color.red_ff4444))
val white = ContextCompat.getColor(holder.b.root.context, R.color.white) val white = ContextCompat.getColor(holder.b.root.context, R.color.white)
holder.b.tvAddress.setTextColor(white)
holder.b.tvName.setTextColor(white) holder.b.tvName.setTextColor(white)
holder.b.tvWeight.setTextColor(white) holder.b.tvWeight.setTextColor(white)
holder.b.tvState.setTextColor(white) holder.b.tvState.setTextColor(white)
} else { } else {
holder.b.root.setBackgroundResource(R.drawable.shape_scale_cell) holder.b.root.setBackgroundResource(R.drawable.shape_scale_cell)
val normal = ContextCompat.getColor(holder.b.root.context, R.color.home_title) val normal = ContextCompat.getColor(holder.b.root.context, R.color.home_title)
holder.b.tvName.setTextColor(normal) val sub = ContextCompat.getColor(holder.b.root.context, R.color.home_sub_title)
holder.b.tvAddress.setTextColor(sub)
holder.b.tvName.setTextColor(sub)
holder.b.tvWeight.setTextColor(normal) holder.b.tvWeight.setTextColor(normal)
holder.b.tvState.setTextColor(normal) holder.b.tvState.setTextColor(sub)
} }
} }
@@ -7,9 +7,11 @@ import androidx.room.RoomDatabase
import androidx.room.migration.Migration import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import com.shuwei.dish.match.db.dao.AppDao import com.shuwei.dish.match.db.dao.AppDao
import com.shuwei.dish.match.db.dao.SeasoningSlotDao
import com.shuwei.dish.match.entity.CookFoodEntity import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.CookFoodGoodsEntity import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.entity.SeasoningEntity import com.shuwei.dish.match.entity.SeasoningEntity
import com.shuwei.dish.match.entity.SeasoningSlotEntity
// 步骤1:更新版本号 // 步骤1:更新版本号
@@ -19,8 +21,9 @@ import com.shuwei.dish.match.entity.SeasoningEntity
SeasoningEntity::class, SeasoningEntity::class,
CookFoodEntity::class, CookFoodEntity::class,
CookFoodGoodsEntity::class, CookFoodGoodsEntity::class,
SeasoningSlotEntity::class,
], ],
version = 7, version = 8,
exportSchema = true exportSchema = true
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@@ -30,6 +33,8 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun appDao(): AppDao abstract fun appDao(): AppDao
abstract fun seasoningSlotDao(): SeasoningSlotDao
} }
// 步骤2:创建Migration对象 // 步骤2:创建Migration对象
@@ -140,6 +145,20 @@ val MIGRATION_5_6 = MigrationImpl(5, 6) { db ->
db.endTransaction() db.endTransaction()
} }
val MIGRATION_7_8 = MigrationImpl(7, 8) { db ->
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS `dm_seasoning_slot` (
`deviceId` TEXT NOT NULL,
`address` INTEGER NOT NULL,
`goodsId` TEXT NOT NULL,
`goodsName` TEXT NOT NULL,
PRIMARY KEY(`deviceId`, `address`)
)
""".trimIndent()
)
}
val MIGRATION_6_7 = MigrationImpl(6, 7) {db -> val MIGRATION_6_7 = MigrationImpl(6, 7) {db ->
runCatching { runCatching {
@@ -210,6 +229,7 @@ class DatabaseProvider(private val context: Context) {
// .addMigrations(MIGRATION_4_5) // .addMigrations(MIGRATION_4_5)
// .addMigrations(MIGRATION_5_6) // .addMigrations(MIGRATION_5_6)
// .addMigrations(MIGRATION_6_7) // .addMigrations(MIGRATION_6_7)
.addMigrations(MIGRATION_7_8)
.build() .build()
} }
} }
@@ -0,0 +1,40 @@
package com.shuwei.dish.match.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.shuwei.dish.match.entity.SeasoningSlotEntity
/**
* 调料槽位配置 DAO
* 以 (deviceId, address) 为主键做 upsert,保证同一槽位只保留最新配置
*/
@Dao
interface SeasoningSlotDao {
/**
* 插入或更新单个槽位配置(主键冲突时覆盖)
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(slot: SeasoningSlotEntity)
/**
* 批量插入或更新槽位配置(主设备广播全量时使用)
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertAll(slots: List<SeasoningSlotEntity>)
/**
* 查询指定设备的全部槽位配置
* @param deviceId 目标设备 ID
*/
@Query("SELECT * FROM dm_seasoning_slot WHERE deviceId = :deviceId")
suspend fun queryByDeviceId(deviceId: String): List<SeasoningSlotEntity>
/**
* 查询所有设备的全部槽位配置(主设备广播时使用)
*/
@Query("SELECT * FROM dm_seasoning_slot")
suspend fun queryAll(): List<SeasoningSlotEntity>
}
@@ -0,0 +1,23 @@
package com.shuwei.dish.match.entity
import androidx.room.Entity
/**
* 调料槽位配置表,记录每个格子对应哪种调料
* 主键为 (deviceId, address) 组合,唯一标识一个物理秤槽位
*
* @param deviceId 所属设备 ID,用于区分 22格/18格 设备
* @param address 秤硬件地址,对应 ScaleDeviceConfig.SCALE_ORDER_22/18 中的值
* @param goodsId 调料 ID
* @param goodsName 调料名称,用于 adapter 展示
*/
@Entity(
tableName = "dm_seasoning_slot",
primaryKeys = ["deviceId", "address"]
)
data class SeasoningSlotEntity(
val deviceId: String,
val address: Int,
val goodsId: String,
val goodsName: String
)
@@ -1,20 +1,38 @@
package com.shuwei.dish.match.scale package com.shuwei.dish.match.scale
/** /**
* 秤事件数据类,用于设备向主设备发送非重量类通知 * 秤事件数据类,用于设备和子设备之间的非重量类通知
* @param type 事件类型,参见 companion object 中的常量 * @param type 事件类型,参见 companion object 中的常量
* @param deviceId 发送方设备 ID * @param deviceId 发送方设备 ID
* @param address 触发事件的秤地址 * @param address 触发事件的秤地址TYPE_SEASONING_CONFIG 时为 0(无意义)
* @param delta 重量变化量(克),仅 TYPE_SEASONING_ADDED 时有意义 * @param delta 重量变化量(克),仅 TYPE_SEASONING_ADDED 时有意义
* @param slots 调料槽位配置列表,仅 TYPE_SEASONING_CONFIG 时有效
*/ */
data class ScaleEvent( data class ScaleEvent(
val type: String, val type: String,
val deviceId: String, val deviceId: String,
val address: Int, val address: Int = 0,
val delta: Double = 0.0 val delta: Double = 0.0,
val slots: List<SlotConfig>? = null
) { ) {
/**
* 单个槽位的配置信息,随 TYPE_SEASONING_CONFIG 事件一起传输
* @param deviceId 所属设备 ID
* @param address 秤硬件地址
* @param goodsId 调料 ID
* @param goodsName 调料名称
*/
data class SlotConfig(
val deviceId: String,
val address: Int,
val goodsId: String,
val goodsName: String
)
companion object { companion object {
/** 调料添加事件:某秤重量增加超过阈值 */ /** 调料添加事件:某秤重量增加超过阈值 */
const val TYPE_SEASONING_ADDED = "seasoning_added" const val TYPE_SEASONING_ADDED = "seasoning_added"
/** 调料槽位配置同步事件:主设备配置变更后广播给子设备 */
const val TYPE_SEASONING_CONFIG = "seasoning_config"
} }
} }
@@ -2,11 +2,15 @@ package com.shuwei.dish.match.scale
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.base.DeviceRole import com.shuwei.dish.match.base.DeviceRole
import com.shuwei.dish.match.base.GlobalData import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.utils.NetworkUtil import com.shuwei.dish.match.utils.NetworkUtil
import com.shuwei.dish.match.utils.WeightUtil import com.shuwei.dish.match.utils.WeightUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
/** /**
* 秤服务统一门面(单例) * 秤服务统一门面(单例)
@@ -59,6 +63,36 @@ object ScaleServiceManager {
wsClient?.onScaleEvent = value wsClient?.onScaleEvent = value
} }
/**
* 收到主设备下发的调料配置同步事件时的回调,仅子设备有效
* 在子线程中调用,需自行切换到主线程更新 UI
*/
var onSeasoningConfig: ((ScaleEvent) -> Unit)?
get() = wsServer?.onSeasoningConfig
set(value) { wsServer?.onSeasoningConfig = value }
/**
* 向所有已连接子设备广播调料配置(主设备调用)
* @param slots 全量槽位配置列表
*/
fun sendSeasoningConfig(slots: List<ScaleEvent.SlotConfig>) {
wsClient?.sendToAllSlaves(buildConfigEvent(slots))
}
/**
* 向单台刚连接的子设备推送调料配置(连接时按需调用)
* @param deviceId 目标子设备 ID
*/
private fun sendSeasoningConfigTo(deviceId: String, slots: List<ScaleEvent.SlotConfig>) {
wsClient?.sendToDevice(deviceId, buildConfigEvent(slots))
}
private fun buildConfigEvent(slots: List<ScaleEvent.SlotConfig>) = ScaleEvent(
type = ScaleEvent.TYPE_SEASONING_CONFIG,
deviceId = GlobalData.deviceId,
slots = slots
)
/** /**
* 向主设备广播秤事件(子设备调用) * 向主设备广播秤事件(子设备调用)
* @param event 要广播的事件 * @param event 要广播的事件
@@ -123,6 +157,19 @@ object ScaleServiceManager {
it.onScaleData = { data -> aggregator?.onRemoteScaleData(data) } it.onScaleData = { data -> aggregator?.onRemoteScaleData(data) }
it.onScaleEvent = { event -> _onScaleEvent?.invoke(event) } it.onScaleEvent = { event -> _onScaleEvent?.invoke(event) }
it.onDeviceDisconnected = { remoteId -> aggregator?.removeDevice(remoteId) } it.onDeviceDisconnected = { remoteId -> aggregator?.removeDevice(remoteId) }
it.onDeviceConnected = { remoteId ->
// 子设备连接成功时,从 Room 读取全量配置并单独推送给该设备
CoroutineScope(Dispatchers.IO).launch {
val slots = BaseApp.instance!!.database.seasoningSlotDao()
.queryAll()
.map { slot ->
ScaleEvent.SlotConfig(
slot.deviceId, slot.address, slot.goodsId, slot.goodsName
)
}
if (slots.isNotEmpty()) sendSeasoningConfigTo(remoteId, slots)
}
}
} }
wsClient = client wsClient = client
@@ -55,6 +55,9 @@ class ScaleWebSocketClient {
/** 设备断线时的回调(连接失败或关闭),在子线程调用 */ /** 设备断线时的回调(连接失败或关闭),在子线程调用 */
var onDeviceDisconnected: ((deviceId: String) -> Unit)? = null var onDeviceDisconnected: ((deviceId: String) -> Unit)? = null
/** 子设备首次连接成功时的回调,在子线程调用;可用于主设备主动推送全量配置 */
var onDeviceConnected: ((deviceId: String) -> Unit)? = null
/** /**
* 连接到指定子设备 * 连接到指定子设备
* @param deviceId 子设备 ID * @param deviceId 子设备 ID
@@ -79,6 +82,7 @@ class ScaleWebSocketClient {
connections[deviceId] = webSocket connections[deviceId] = webSocket
reconnectDelays[deviceId] = RECONNECT_BASE_MS reconnectDelays[deviceId] = RECONNECT_BASE_MS
reconnectTasks.remove(deviceId)?.cancel(false) reconnectTasks.remove(deviceId)?.cancel(false)
onDeviceConnected?.invoke(deviceId)
} }
override fun onMessage(webSocket: WebSocket, text: String) { override fun onMessage(webSocket: WebSocket, text: String) {
@@ -148,6 +152,31 @@ class ScaleWebSocketClient {
return ws.send(gson.toJson(command)) return ws.send(gson.toJson(command))
} }
/**
* 向所有已连接的子设备广播事件(主设备调用,用于配置同步等场景)
* @param event 要广播的事件
*/
fun sendToAllSlaves(event: ScaleEvent) {
val json = gson.toJson(event)
connections.values.forEach { ws ->
try { ws.send(json) } catch (e: Exception) {
Log.w(TAG, "sendToAllSlaves 失败: ${e.message}")
}
}
}
/**
* 向指定子设备发送事件(用于连接时单独推送配置)
* @param deviceId 目标子设备 ID
* @param event 要发送的事件
*/
fun sendToDevice(deviceId: String, event: ScaleEvent) {
val ws = connections[deviceId] ?: return
try { ws.send(gson.toJson(event)) } catch (e: Exception) {
Log.w(TAG, "sendToDevice $deviceId 失败: ${e.message}")
}
}
/** /**
* 断开指定子设备连接(不再重连) * 断开指定子设备连接(不再重连)
* @param deviceId 子设备 ID * @param deviceId 子设备 ID
@@ -28,6 +28,9 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
/** 主设备连接状态变化回调:true=已连接,false=已断开;在主线程外调用,需自行切换线程 */ /** 主设备连接状态变化回调:true=已连接,false=已断开;在主线程外调用,需自行切换线程 */
var onConnectionChanged: ((connected: Boolean) -> Unit)? = null var onConnectionChanged: ((connected: Boolean) -> Unit)? = null
/** 收到主设备下发的调料配置同步事件时的回调,在子线程调用 */
var onSeasoningConfig: ((event: ScaleEvent) -> Unit)? = null
/** 当前是否有主设备连接 */ /** 当前是否有主设备连接 */
val isConnected: Boolean val isConnected: Boolean
get() = server?.connections?.isNotEmpty() == true get() = server?.connections?.isNotEmpty() == true
@@ -72,6 +75,8 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
*/ */
fun stop() { fun stop() {
WeightUtil.removeWeightListener(TAG) WeightUtil.removeWeightListener(TAG)
onConnectionChanged = null
onSeasoningConfig = null
try { try {
server?.stop(1000) server?.stop(1000)
} catch (e: Exception) { } catch (e: Exception) {
@@ -135,6 +140,15 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
override fun onMessage(conn: WebSocket, message: String) { override fun onMessage(conn: WebSocket, message: String) {
try { try {
val raw = gson.fromJson(message, Map::class.java)
if (raw.containsKey("type")) {
// 主设备下发的事件(如调料配置同步)
val event = gson.fromJson(message, ScaleEvent::class.java)
when (event.type) {
ScaleEvent.TYPE_SEASONING_CONFIG -> onSeasoningConfig?.invoke(event)
}
return
}
val cmd = gson.fromJson(message, ScaleCommand::class.java) val cmd = gson.fromJson(message, ScaleCommand::class.java)
// 校验指令目标设备是否为本机 // 校验指令目标设备是否为本机
if (cmd.deviceId != deviceId) { if (cmd.deviceId != deviceId) {
@@ -148,7 +162,7 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "收到清零指令, 解析指令失败: ${e.message}") Log.w(TAG, "收到消息, 解析失败: ${e.message}")
} }
} }
@@ -27,9 +27,12 @@ import com.shuwei.dish.match.databinding.ListTypeScale1Binding
import com.shuwei.dish.match.databinding.ListTypeScale18Binding import com.shuwei.dish.match.databinding.ListTypeScale18Binding
import com.shuwei.dish.match.databinding.ListTypeScale2Binding import com.shuwei.dish.match.databinding.ListTypeScale2Binding
import com.shuwei.dish.match.databinding.ListTypeScale22Binding import com.shuwei.dish.match.databinding.ListTypeScale22Binding
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.scale.ScaleData import com.shuwei.dish.match.scale.ScaleData
import com.shuwei.dish.match.scale.ScaleDeviceConfig import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.scale.ScaleServiceManager import com.shuwei.dish.match.scale.ScaleServiceManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import com.shuwei.dish.match.utils.NetworkUtil import com.shuwei.dish.match.utils.NetworkUtil
import com.shuwei.dish.match.utils.WeightUtil import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp import com.shuwei.dish.match.utils.ext.dp
@@ -59,6 +62,9 @@ class MasterScaleActivity : BaseActivity() {
private val groupList = mutableListOf<DeviceGroup>() private val groupList = mutableListOf<DeviceGroup>()
private val adapter = DeviceGroupAdapter(groupList) private val adapter = DeviceGroupAdapter(groupList)
/** key = "$deviceId#$address"value = 调料名称,用于注入 ScaleData.name */
private val slotNameMap = mutableMapOf<String, String>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityMasterScaleBinding.inflate(layoutInflater) binding = ActivityMasterScaleBinding.inflate(layoutInflater)
@@ -70,7 +76,26 @@ class MasterScaleActivity : BaseActivity() {
binding.rvScaleList.itemAnimator = null binding.rvScaleList.itemAnimator = null
binding.rvScaleList.adapter = adapter binding.rvScaleList.adapter = adapter
observeScaleData() loadSlotNames()
}
/** 从 Room 加载槽位配置到 slotNameMap,加载完成后启动数据观测 */
private fun loadSlotNames() {
lifecycleScope.launch(Dispatchers.IO) {
val slots = BaseApp.instance!!.database.seasoningSlotDao().queryAll()
withContext(Dispatchers.Main) {
slots.forEach { slot ->
slotNameMap["${slot.deviceId}#${slot.address}"] = slot.goodsName
}
observeScaleData()
}
}
}
/** 用 slotNameMap 为 ScaleData 注入名称 */
private fun ScaleData.withSlotName(): ScaleData {
val name = slotNameMap["$deviceId#$address"]
return if (name != null) copy(name = name) else this
} }
private fun observeScaleData() { private fun observeScaleData() {
@@ -90,7 +115,7 @@ class MasterScaleActivity : BaseActivity() {
deviceId = deviceId, deviceId = deviceId,
ip = if (deviceId == localId) localIp ip = if (deviceId == localId) localIp
else scales.firstOrNull { it.ip.isNotEmpty() }?.ip ?: "", else scales.firstOrNull { it.ip.isNotEmpty() }?.ip ?: "",
scales = scales.sortedBy { it.address } scales = scales.map { it.withSlotName() }.sortedBy { it.address }
) )
} }
@@ -269,7 +294,7 @@ class MasterScaleActivity : BaseActivity() {
alignItems = AlignItems.FLEX_START alignItems = AlignItems.FLEX_START
} }
b.rvScales.itemAnimator = null b.rvScales.itemAnimator = null
innerAdapter = Seasoning22GridAdapter(largeSize, smallSize, showNameOnly = false).apply { innerAdapter = Seasoning22GridAdapter(largeSize, smallSize, showNameOnly = false, showAddress = true).apply {
onItemClick = { scale, _ -> onItemClick = { scale, _ ->
showTareDialog(deviceId = scale.deviceId, ip = scale.ip, address = scale.address) showTareDialog(deviceId = scale.deviceId, ip = scale.ip, address = scale.address)
} }
@@ -303,7 +328,7 @@ class MasterScaleActivity : BaseActivity() {
} }
b.rvScales.layoutManager = gridLayoutManager b.rvScales.layoutManager = gridLayoutManager
b.rvScales.itemAnimator = null b.rvScales.itemAnimator = null
innerAdapter = Seasoning18GridAdapter(showNameOnly = false).apply { innerAdapter = Seasoning18GridAdapter(showNameOnly = false, showAddress = true).apply {
onItemClick = { scale, _ -> onItemClick = { scale, _ ->
showTareDialog(deviceId = scale.deviceId, ip = scale.ip, address = scale.address) showTareDialog(deviceId = scale.deviceId, ip = scale.ip, address = scale.address)
} }
@@ -369,6 +394,8 @@ class MasterScaleActivity : BaseActivity() {
setBackgroundResource(R.drawable.shape_scale_cell) setBackgroundResource(R.drawable.shape_scale_cell)
val baseSize = ((cellSize - margin * 2) / resources.displayMetrics.density).toInt() val baseSize = ((cellSize - margin * 2) / resources.displayMetrics.density).toInt()
addView(makeCellText("${scale.address}", (baseSize * 0.16f).coerceIn(9f, 14f).toInt())) addView(makeCellText("${scale.address}", (baseSize * 0.16f).coerceIn(9f, 14f).toInt()))
val foodType = if (scale.address == 1) "净材" else "熟食"
addView(makeCellText(foodType, (baseSize * 0.16f).coerceIn(9f, 14f).toInt()))
addView(makeCellText("${scale.weight}g", (baseSize * 0.22f).coerceIn(12f, 20f).toInt(), bold = true)) 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())) addView(makeCellText(stateStr(scale.state), (baseSize * 0.14f).coerceIn(8f, 12f).toInt()))
tag = scale tag = scale
@@ -379,8 +406,9 @@ class MasterScaleActivity : BaseActivity() {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
private fun updateScaleCell(cell: LinearLayout, scale: ScaleData) { private fun updateScaleCell(cell: LinearLayout, scale: ScaleData) {
(cell.getChildAt(0) as? TextView)?.text = "${scale.address}" (cell.getChildAt(0) as? TextView)?.text = "${scale.address}"
(cell.getChildAt(1) as? TextView)?.text = "${scale.weight}g" (cell.getChildAt(1) as? TextView)?.text = if (scale.address == 1) "净材" else "熟食"
(cell.getChildAt(2) as? TextView)?.text = stateStr(scale.state) (cell.getChildAt(2) as? TextView)?.text = "${scale.weight}g"
(cell.getChildAt(3) as? TextView)?.text = stateStr(scale.state)
} }
private fun makeCellText(text: String, spSize: Int, bold: Boolean = false): TextView = private fun makeCellText(text: String, spSize: Int, bold: Boolean = false): TextView =
@@ -9,6 +9,7 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.lifecycle.lifecycleScope
import com.google.android.flexbox.AlignItems import com.google.android.flexbox.AlignItems
import com.google.android.flexbox.FlexDirection import com.google.android.flexbox.FlexDirection
import com.google.android.flexbox.FlexWrap import com.google.android.flexbox.FlexWrap
@@ -17,15 +18,20 @@ import com.shuwei.dish.match.adapter.Seasoning18GridAdapter
import com.shuwei.dish.match.adapter.Seasoning22GridAdapter import com.shuwei.dish.match.adapter.Seasoning22GridAdapter
import com.shuwei.dish.match.adapter.ScaleRowAdapter import com.shuwei.dish.match.adapter.ScaleRowAdapter
import com.shuwei.dish.match.base.BaseActivity import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.base.GlobalData import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.databinding.ActivitySlaveBinding import com.shuwei.dish.match.databinding.ActivitySlaveBinding
import com.shuwei.dish.match.dialog.CommonDialog import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.entity.SeasoningSlotEntity
import com.shuwei.dish.match.scale.ScaleData import com.shuwei.dish.match.scale.ScaleData
import com.shuwei.dish.match.scale.ScaleDeviceConfig import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.scale.ScaleServiceManager import com.shuwei.dish.match.scale.ScaleServiceManager
import com.shuwei.dish.match.utils.NetworkUtil import com.shuwei.dish.match.utils.NetworkUtil
import com.shuwei.dish.match.utils.WeightUtil import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp import com.shuwei.dish.match.utils.ext.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/** /**
* 子设备专属页面(小屏幕设备使用) * 子设备专属页面(小屏幕设备使用)
@@ -49,6 +55,8 @@ class SlaveActivity : BaseActivity() {
/** 各秤地址的上一次重量快照,用于计算 delta */ /** 各秤地址的上一次重量快照,用于计算 delta */
private val weightSnapshot = mutableMapOf<Int, Double>() private val weightSnapshot = mutableMapOf<Int, Double>()
/** key = addressvalue = 调料名称,用于注入 ScaleData.name,避免重量更新时覆盖名称 */
private val slotNameMap = mutableMapOf<Int, String>()
/** 主线程 Handler,用于延迟清除高亮 */ /** 主线程 Handler,用于延迟清除高亮 */
private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())
/** 当前高亮清除任务,新高亮触发时取消旧任务 */ /** 当前高亮清除任务,新高亮触发时取消旧任务 */
@@ -65,8 +73,10 @@ class SlaveActivity : BaseActivity() {
setupScaleList() setupScaleList()
updateMasterStatus(ScaleServiceManager.isMasterConnected) updateMasterStatus(ScaleServiceManager.isMasterConnected)
loadSlotsFromDb()
listenLocalScales() listenLocalScales()
listenMasterConnection() listenMasterConnection()
listenSeasoningConfig()
addBackKeyListener() addBackKeyListener()
} }
@@ -140,6 +150,55 @@ class SlaveActivity : BaseActivity() {
} }
} }
/**
* 从本地 Room 加载本机设备的槽位配置并更新 adapter
* 主设备未开机时也能正常展示上次配置
*/
private fun loadSlotsFromDb() {
lifecycleScope.launch(Dispatchers.IO) {
val dao = BaseApp.instance!!.database.seasoningSlotDao()
val slots = dao.queryByDeviceId(GlobalData.deviceId)
withContext(Dispatchers.Main) { applySlots(slots) }
}
}
/**
* 监听主设备下发的调料配置同步事件
* 收到后更新 adapter 并写入本地 Room
*/
private fun listenSeasoningConfig() {
ScaleServiceManager.onSeasoningConfig = onSeasoningConfig@{ event ->
val mySlots = event.slots
?.filter { it.deviceId == GlobalData.deviceId }
?.map { SeasoningSlotEntity(it.deviceId, it.address, it.goodsId, it.goodsName) }
?: return@onSeasoningConfig
lifecycleScope.launch(Dispatchers.IO) {
BaseApp.instance!!.database.seasoningSlotDao().upsertAll(mySlots)
withContext(Dispatchers.Main) { applySlots(mySlots) }
}
}
}
/**
* 将槽位配置应用到对应 adapter(按地址匹配格子位置)
* 同时更新 slotNameMap,确保后续重量回调构造 ScaleData 时携带正确名称
*/
private fun applySlots(slots: List<SeasoningSlotEntity>) {
slots.forEach { slot ->
slotNameMap[slot.address] = slot.goodsName
when {
scale22Adapter != null -> {
val pos = ScaleDeviceConfig.SCALE_ORDER_22.indexOf(slot.address)
if (pos >= 0) scale22Adapter!!.updateItemName(pos, slot.goodsName)
}
scale18Adapter != null -> {
val pos = ScaleDeviceConfig.SCALE_ORDER_18.indexOf(slot.address)
if (pos >= 0) scale18Adapter!!.updateItemName(pos, slot.goodsName)
}
}
}
}
/** /**
* 监听本机 WeightUtil 回调,实时刷新秤列表 * 监听本机 WeightUtil 回调,实时刷新秤列表
* 22/18个秤走网格 adapter,其余走线性列表 * 22/18个秤走网格 adapter,其余走线性列表
@@ -164,14 +223,14 @@ class SlaveActivity : BaseActivity() {
// 用最新数据更新缓存后整体刷新网格 // 用最新数据更新缓存后整体刷新网格
val existing = scale22Adapter!!.items.toMutableList() val existing = scale22Adapter!!.items.toMutableList()
val idx = existing.indexOfFirst { it.address == address } val idx = existing.indexOfFirst { it.address == address }
val newData = ScaleData(GlobalData.deviceId, address, weight, state, System.currentTimeMillis()) val newData = ScaleData(GlobalData.deviceId, address, weight, state, System.currentTimeMillis(), name = slotNameMap[address])
if (idx >= 0) existing[idx] = newData else existing.add(newData) if (idx >= 0) existing[idx] = newData else existing.add(newData)
scale22Adapter!!.updateByAddress(existing) scale22Adapter!!.updateByAddress(existing)
} }
scale18Adapter != null -> { scale18Adapter != null -> {
val existing = scale18Adapter!!.items.toMutableList() val existing = scale18Adapter!!.items.toMutableList()
val idx = existing.indexOfFirst { it.address == address } val idx = existing.indexOfFirst { it.address == address }
val newData = ScaleData(GlobalData.deviceId, address, weight, state, System.currentTimeMillis()) val newData = ScaleData(GlobalData.deviceId, address, weight, state, System.currentTimeMillis(), name = slotNameMap[address])
if (idx >= 0) existing[idx] = newData else existing.add(newData) if (idx >= 0) existing[idx] = newData else existing.add(newData)
scale18Adapter!!.updateByAddress(existing) scale18Adapter!!.updateByAddress(existing)
} }
@@ -219,6 +278,7 @@ class SlaveActivity : BaseActivity() {
override fun onDestroy() { override fun onDestroy() {
ScaleServiceManager.onMasterConnectionChanged = null ScaleServiceManager.onMasterConnectionChanged = null
ScaleServiceManager.onSeasoningConfig = null
WeightUtil.removeWeightListener(TAG) WeightUtil.removeWeightListener(TAG)
clearHighlightRunnable?.let { mainHandler.removeCallbacks(it) } clearHighlightRunnable?.let { mainHandler.removeCallbacks(it) }
super.onDestroy() super.onDestroy()
@@ -13,18 +13,23 @@ import com.google.android.flexbox.FlexWrap
import com.google.android.flexbox.FlexboxLayoutManager import com.google.android.flexbox.FlexboxLayoutManager
import com.shuwei.dish.match.adapter.Seasoning18GridAdapter import com.shuwei.dish.match.adapter.Seasoning18GridAdapter
import com.shuwei.dish.match.adapter.Seasoning22GridAdapter import com.shuwei.dish.match.adapter.Seasoning22GridAdapter
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.base.BaseFragment import com.shuwei.dish.match.base.BaseFragment
import com.shuwei.dish.match.base.DeviceRole import com.shuwei.dish.match.base.DeviceRole
import com.shuwei.dish.match.base.GlobalData import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.databinding.FragmentSeasoningConfigBinding import com.shuwei.dish.match.databinding.FragmentSeasoningConfigBinding
import com.shuwei.dish.match.dialog.SeasoningSelectDialog import com.shuwei.dish.match.dialog.SeasoningSelectDialog
import com.shuwei.dish.match.entity.SeasoningSlotEntity
import com.shuwei.dish.match.scale.ScaleDeviceConfig import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.scale.ScaleEvent import com.shuwei.dish.match.scale.ScaleEvent
import com.shuwei.dish.match.scale.ScaleServiceManager import com.shuwei.dish.match.scale.ScaleServiceManager
import com.shuwei.dish.match.scale.ScaleEvent.SlotConfig
import com.shuwei.dish.match.ui.SettingActivity import com.shuwei.dish.match.ui.SettingActivity
import com.shuwei.dish.match.utils.WeightUtil import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp import com.shuwei.dish.match.utils.ext.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() { class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
@@ -65,26 +70,58 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
/** 绑定两个 Adapter 的格子点击,弹出调料选择弹窗 */ /** 绑定两个 Adapter 的格子点击,弹出调料选择弹窗 */
private fun bindAdapterClicks() { private fun bindAdapterClicks() {
adapter22.onItemClick = { _, position -> adapter22.onItemClick = { scaleData, position ->
SeasoningSelectDialog( SeasoningSelectDialog(
activity = settingActivity, activity = settingActivity,
clickIndex = position, clickIndex = position,
onItemSelected = { item -> onItemSelected = { item ->
adapter22.updateItemName(position, item.goodsName ?: "") adapter22.updateItemName(position, item.goodsName ?: "")
saveSlotAndBroadcast(
SeasoningSlotEntity(
deviceId = ScaleDeviceConfig.DEVICE_ID_22,
address = scaleData.address,
goodsId = item.goodsId,
goodsName = item.goodsName ?: ""
)
)
} }
).show() ).show()
} }
adapter18.onItemClick = { _, position -> adapter18.onItemClick = { scaleData, position ->
SeasoningSelectDialog( SeasoningSelectDialog(
activity = settingActivity, activity = settingActivity,
clickIndex = position, clickIndex = position,
onItemSelected = { item -> onItemSelected = { item ->
adapter18.updateItemName(position, item.goodsName ?: "") adapter18.updateItemName(position, item.goodsName ?: "")
saveSlotAndBroadcast(
SeasoningSlotEntity(
deviceId = ScaleDeviceConfig.DEVICE_ID_18,
address = scaleData.address,
goodsId = item.goodsId,
goodsName = item.goodsName ?: ""
)
)
} }
).show() ).show()
} }
} }
/**
* 将单个槽位写入 Room,再读取全量配置广播给子设备
* 仅主设备执行广播,子设备不调用此方法
*/
private fun saveSlotAndBroadcast(slot: SeasoningSlotEntity) {
lifecycleScope.launch(Dispatchers.IO) {
val dao = BaseApp.instance!!.database.seasoningSlotDao()
dao.upsert(slot)
// 读取全量配置,通过 wsClient 推送给所有已连接子设备
val allSlots = dao.queryAll().map {
SlotConfig(it.deviceId, it.address, it.goodsId, it.goodsName)
}
ScaleServiceManager.sendSeasoningConfig(allSlots)
}
}
// /** 初始化 ViewModel */ // /** 初始化 ViewModel */
// private fun initViewModel() { // private fun initViewModel() {
// val factory = AppFactory(AppRepository(BaseApp.instance!!.database.appDao())) // val factory = AppFactory(AppRepository(BaseApp.instance!!.database.appDao()))
@@ -126,10 +163,25 @@ class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
} }
} }
/** 加载默认空数据,使两个列表完整展示所有格子 */ /** 从 Room 加载已配置的槽位名称,填充到对应格子 */
private fun loadData() { private fun loadData() {
adapter22.update(emptyList()) adapter22.update(emptyList())
adapter18.update(emptyList()) adapter18.update(emptyList())
lifecycleScope.launch(Dispatchers.IO) {
val dao = BaseApp.instance!!.database.seasoningSlotDao()
val slots22 = dao.queryByDeviceId(ScaleDeviceConfig.DEVICE_ID_22)
val slots18 = dao.queryByDeviceId(ScaleDeviceConfig.DEVICE_ID_18)
withContext(Dispatchers.Main) {
slots22.forEach { slot ->
val pos = ScaleDeviceConfig.SCALE_ORDER_22.indexOf(slot.address)
if (pos >= 0) adapter22.updateItemName(pos, slot.goodsName)
}
slots18.forEach { slot ->
val pos = ScaleDeviceConfig.SCALE_ORDER_18.indexOf(slot.address)
if (pos >= 0) adapter18.updateItemName(pos, slot.goodsName)
}
}
}
// appViewModel.loadSeasoning { list -> // appViewModel.loadSeasoning { list ->
// adapter22.update(list ?: emptyList()) // adapter22.update(list ?: emptyList())
// adapter18.update(list ?: emptyList()) // adapter18.update(list ?: emptyList())
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- 单个秤格子:地址 + 重量 + 状态 --> <!-- 单个秤格子:地址 + 调料名 + 重量 + 状态 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -8,6 +8,15 @@
android:padding="4dp" android:padding="4dp"
android:background="@drawable/shape_scale_cell"> android:background="@drawable/shape_scale_cell">
<TextView
android:id="@+id/tvAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:includeFontPadding="false"
android:textColor="@color/home_sub_title"
android:visibility="gone" />
<TextView <TextView
android:id="@+id/tvName" android:id="@+id/tvName"
android:layout_width="match_parent" android:layout_width="match_parent"