feat(activity): 为 MasterScaleActivity 各秤格子新增清零点击事件,修复高频数据更新导致点击失效的问题

This commit is contained in:
2026-04-16 13:45:39 +08:00
parent b775b20ff1
commit 7fd2f02b87
2 changed files with 194 additions and 36 deletions
@@ -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
}
}
@@ -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<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 +149,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 +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<DeviceGroup, QuickViewHolder> {
@@ -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<DeviceGroup, QuickViewHolder> {
@@ -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<DeviceGroup, QuickViewHolder> {
@@ -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<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 +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<ScaleData>) {
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<ScaleData>) {
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<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)
@@ -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<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)
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<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)
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()
}
}