diff --git a/app/src/main/java/com/shuwei/dish/match/scale/ScaleServiceManager.kt b/app/src/main/java/com/shuwei/dish/match/scale/ScaleServiceManager.kt index 44490bc..43879fb 100644 --- a/app/src/main/java/com/shuwei/dish/match/scale/ScaleServiceManager.kt +++ b/app/src/main/java/com/shuwei/dish/match/scale/ScaleServiceManager.kt @@ -141,12 +141,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 } } diff --git a/app/src/main/java/com/shuwei/dish/match/ui/MasterScaleActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/MasterScaleActivity.kt index 57470ae..acf8f17 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/MasterScaleActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/MasterScaleActivity.kt @@ -31,6 +31,7 @@ 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 @@ -99,17 +100,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) { - 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 +149,18 @@ class MasterScaleActivity : BaseActivity() { private inner class DeviceGroupAdapter(data: MutableList) : BaseMultiItemAdapter(data) { + private val scale1VHMap = mutableMapOf() + private val scale2VHMap = mutableMapOf() + private val scale22VHMap = mutableMapOf() + private val scale18VHMap = mutableMapOf() + + fun updateScalesForDevice(deviceId: String, scales: List) { + scale1VHMap[deviceId]?.updateScales(scales) + scale2VHMap[deviceId]?.updateScales(scales) + scale22VHMap[deviceId]?.updateScales(scales) + scale18VHMap[deviceId]?.updateScales(scales) + } + init { addItemType(VIEW_TYPE_SCALE2, object : BaseMultiItemAdapter.OnMultiItemAdapterListener { override fun onCreate(context: Context, parent: ViewGroup, viewType: Int) = @@ -129,7 +168,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 { @@ -138,7 +179,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 { @@ -147,7 +191,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 { @@ -156,7 +202,9 @@ 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) { @@ -171,14 +219,36 @@ class MasterScaleActivity : BaseActivity() { // ── ViewHolder: 2个秤 ────────────────────────────────────────────────── private inner class Scale2VH(val b: ListTypeScale2Binding) : RecyclerView.ViewHolder(b.root) { + private lateinit var cells: List + @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) { + if (!::cells.isInitialized) return + scales.take(2).forEachIndexed { i, scale -> + cells.getOrNull(i)?.let { cell -> + cell.tag = scale + updateScaleCell(cell, scale) + } } } } @@ -186,17 +256,16 @@ class MasterScaleActivity : BaseActivity() { // ── ViewHolder: 22个秤 ───────────────────────────────────────────────── private inner class Scale22VH(val b: ListTypeScale22Binding) : RecyclerView.ViewHolder(b.root) { - private var bound = false + private var innerAdapter: Scale22InnerAdapter? = 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,26 +276,35 @@ class MasterScaleActivity : BaseActivity() { alignItems = AlignItems.FLEX_START } b.rvScales.itemAnimator = null - b.rvScales.adapter = Scale22InnerAdapter(largeSize, smallSize) - bound = true + innerAdapter = Scale22InnerAdapter(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 } + updateScales(group.scales) + } + + /** 仅更新秤数据,不触发外层任何 notify */ + fun updateScales(scales: List) { + val scaleByAddress = scales.associateBy { it.address } val ordered = SCALE_ORDER_22.map { scaleByAddress[it] } - (b.rvScales.adapter as Scale22InnerAdapter).update(ordered) + innerAdapter?.update(ordered) } } // ── ViewHolder: 18个秤 ───────────────────────────────────────────────── private inner class Scale18VH(val b: ListTypeScale18Binding) : RecyclerView.ViewHolder(b.root) { - private var bound = false + private var innerAdapter: Scale18InnerAdapter? = 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列 @@ -235,23 +313,45 @@ class MasterScaleActivity : BaseActivity() { } b.rvScales.layoutManager = gridLayoutManager b.rvScales.itemAnimator = null - b.rvScales.adapter = Scale18InnerAdapter() - bound = true + innerAdapter = Scale18InnerAdapter().apply { + onItemClick = { scale -> + showTareDialog(deviceId = scale.deviceId, ip = scale.ip, address = scale.address) + } + } + b.rvScales.adapter = innerAdapter } - val scaleByAddress = group.scales.associateBy { it.address } + updateScales(group.scales) + } + + /** 仅更新秤数据,不触发外层任何 notify */ + fun updateScales(scales: List) { + val scaleByAddress = scales.associateBy { it.address } val ordered = SCALE_ORDER_18.map { scaleByAddress[it] } - (b.rvScales.adapter as Scale18InnerAdapter).update(ordered) + innerAdapter?.update(ordered) } } // ── 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) { + 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) @@ -270,13 +370,20 @@ class MasterScaleActivity : BaseActivity() { // 用 address=0 的哨兵表示空位 private val empty = ScaleData("", 0, 0.0, -1, 0) + /** 点击回调,在 onCreateViewHolder 中绑定,不受 notifyItemChanged 影响 */ + var onItemClick: ((ScaleData) -> Unit)? = null + fun update(newList: List) { val filled = newList.map { it ?: empty } if (items.isEmpty()) { submitList(filled) } else { filled.forEachIndexed { i, scale -> - if (items.getOrNull(i) != scale) set(i, scale) + val cur = items.getOrNull(i) + // 排除 ts 时间戳比较,只有显示字段变化才触发更新 + if (cur == null || cur.address != scale.address || cur.weight != scale.weight || cur.state != scale.state) { + set(i, scale) + } } } } @@ -295,7 +402,15 @@ class MasterScaleActivity : BaseActivity() { 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) + 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") @@ -315,13 +430,19 @@ class MasterScaleActivity : BaseActivity() { private val empty = ScaleData("", 0, 0.0, -1, 0) + /** 点击回调,在 onCreateViewHolder 中绑定,不受 notifyItemChanged 影响 */ + var onItemClick: ((ScaleData) -> Unit)? = null + fun update(newList: List) { val filled = newList.map { it ?: empty } if (items.isEmpty()) { submitList(filled) } else { filled.forEachIndexed { i, scale -> - if (items.getOrNull(i) != scale) set(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) + } } } } @@ -343,7 +464,15 @@ class MasterScaleActivity : BaseActivity() { 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) + 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") @@ -365,7 +494,7 @@ class MasterScaleActivity : BaseActivity() { else -> "$state" } - /** 创建正方形秤格子(用于 Scale2VH) */ + /** 创建正方形秤格子(用于 Scale2VH 初始化) */ @SuppressLint("SetTextI18n") private fun makeScaleCell(scale: ScaleData, cellSize: Int): LinearLayout { val margin = 2.dp @@ -380,9 +509,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 +533,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) { + android.app.AlertDialog.Builder(this) + .setTitle("清零确认") + .setMessage("设备 IP:${ip.ifEmpty { "未知" }}\n确认对秤 $address 执行清零操作?") + .setPositiveButton("确认") { _, _ -> + val sent = ScaleServiceManager.sendTare(deviceId, address) + if (!sent) toast("设备未连接,清零失败") + } + .setNegativeButton("取消", null) + .show() + } + }