refactor(scale): 提取 Scale22/18GridAdapter 到 adapter 包,SlaveActivity 复用网格布局

This commit is contained in:
2026-04-16 14:26:34 +08:00
parent cba5bda757
commit c3f3b1215b
5 changed files with 329 additions and 172 deletions
@@ -0,0 +1,100 @@
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
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,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)
}
@@ -12,21 +12,22 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.chad.library.adapter4.BaseMultiItemAdapter import com.chad.library.adapter4.BaseMultiItemAdapter
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder import com.chad.library.adapter4.viewholder.QuickViewHolder
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
import com.google.android.flexbox.FlexboxLayoutManager import com.google.android.flexbox.FlexboxLayoutManager
import com.shuwei.dish.match.R 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.base.BaseActivity
import com.shuwei.dish.match.databinding.ActivityMasterScaleBinding 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.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.scale.ScaleData 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.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
@@ -35,15 +36,6 @@ import com.shuwei.dish.match.utils.ext.toast
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch 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_SCALE2 = 0
private const val VIEW_TYPE_SCALE22 = 1 private const val VIEW_TYPE_SCALE22 = 1
private const val VIEW_TYPE_SCALE18 = 2 private const val VIEW_TYPE_SCALE18 = 2
@@ -90,7 +82,7 @@ class MasterScaleActivity : BaseActivity() {
.groupBy { it.deviceId } .groupBy { it.deviceId }
.entries .entries
.sortedWith(compareBy { entry -> .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) -> .map { (deviceId, scales) ->
DeviceGroup( DeviceGroup(
@@ -208,9 +200,9 @@ class MasterScaleActivity : BaseActivity() {
} }
}) })
onItemViewType { position, list -> when (list.getOrNull(position)?.deviceId) { onItemViewType { position, list -> when (list.getOrNull(position)?.deviceId) {
DEVICE_ID_2 -> VIEW_TYPE_SCALE2 ScaleDeviceConfig.DEVICE_ID_2 -> VIEW_TYPE_SCALE2
DEVICE_ID_22 -> VIEW_TYPE_SCALE22 ScaleDeviceConfig.DEVICE_ID_22 -> VIEW_TYPE_SCALE22
DEVICE_ID_18 -> VIEW_TYPE_SCALE18 ScaleDeviceConfig.DEVICE_ID_18 -> VIEW_TYPE_SCALE18
else -> VIEW_TYPE_SCALE1 else -> VIEW_TYPE_SCALE1
}} }}
} }
@@ -256,7 +248,7 @@ class MasterScaleActivity : BaseActivity() {
// ── ViewHolder: 22个秤 ───────────────────────────────────────────────── // ── ViewHolder: 22个秤 ─────────────────────────────────────────────────
private inner class Scale22VH(val b: ListTypeScale22Binding) : RecyclerView.ViewHolder(b.root) { private inner class Scale22VH(val b: ListTypeScale22Binding) : RecyclerView.ViewHolder(b.root) {
private var innerAdapter: Scale22InnerAdapter? = null private var innerAdapter: Scale22GridAdapter? = null
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun bind(group: DeviceGroup) { fun bind(group: DeviceGroup) {
@@ -276,7 +268,7 @@ class MasterScaleActivity : BaseActivity() {
alignItems = AlignItems.FLEX_START alignItems = AlignItems.FLEX_START
} }
b.rvScales.itemAnimator = null b.rvScales.itemAnimator = null
innerAdapter = Scale22InnerAdapter(largeSize, smallSize).apply { innerAdapter = Scale22GridAdapter(largeSize, smallSize).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)
} }
@@ -288,16 +280,14 @@ class MasterScaleActivity : BaseActivity() {
/** 仅更新秤数据,不触发外层任何 notify */ /** 仅更新秤数据,不触发外层任何 notify */
fun updateScales(scales: List<ScaleData>) { fun updateScales(scales: List<ScaleData>) {
val scaleByAddress = scales.associateBy { it.address } innerAdapter?.updateByAddress(scales)
val ordered = SCALE_ORDER_22.map { scaleByAddress[it] }
innerAdapter?.update(ordered)
} }
} }
// ── ViewHolder: 18个秤 ───────────────────────────────────────────────── // ── ViewHolder: 18个秤 ─────────────────────────────────────────────────
private inner class Scale18VH(val b: ListTypeScale18Binding) : RecyclerView.ViewHolder(b.root) { private inner class Scale18VH(val b: ListTypeScale18Binding) : RecyclerView.ViewHolder(b.root) {
private var innerAdapter: Scale18InnerAdapter? = null private var innerAdapter: Scale18GridAdapter? = null
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun bind(group: DeviceGroup) { fun bind(group: DeviceGroup) {
@@ -307,13 +297,12 @@ class MasterScaleActivity : BaseActivity() {
if (innerAdapter == null) { if (innerAdapter == null) {
val gridLayoutManager = GridLayoutManager(b.root.context, 6).apply { val gridLayoutManager = GridLayoutManager(b.root.context, 6).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
// 每个格子占1 span,共6列
override fun getSpanSize(position: Int) = 1 override fun getSpanSize(position: Int) = 1
} }
} }
b.rvScales.layoutManager = gridLayoutManager b.rvScales.layoutManager = gridLayoutManager
b.rvScales.itemAnimator = null b.rvScales.itemAnimator = null
innerAdapter = Scale18InnerAdapter().apply { innerAdapter = Scale18GridAdapter().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)
} }
@@ -325,9 +314,7 @@ class MasterScaleActivity : BaseActivity() {
/** 仅更新秤数据,不触发外层任何 notify */ /** 仅更新秤数据,不触发外层任何 notify */
fun updateScales(scales: List<ScaleData>) { fun updateScales(scales: List<ScaleData>) {
val scaleByAddress = scales.associateBy { it.address } innerAdapter?.updateByAddress(scales)
val ordered = SCALE_ORDER_18.map { scaleByAddress[it] }
innerAdapter?.update(ordered)
} }
} }
@@ -358,132 +345,6 @@ class MasterScaleActivity : BaseActivity() {
} }
} }
// ── 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)
/** 点击回调,在 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 ->
val cur = items.getOrNull(i)
// 排除 ts 时间戳比较,只有显示字段变化才触发更新
if (cur == null || cur.address != scale.address || cur.weight != scale.weight || cur.state != scale.state) {
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)
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 ""
}
}
// ── 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)
/** 点击回调,在 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 ->
val cur = items.getOrNull(i)
if (cur == null || cur.address != scale.address || cur.weight != scale.weight || cur.state != scale.state) {
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)
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) { private fun stateStr(state: Int?) = when (state) {
@@ -4,31 +4,46 @@ import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.activity.addCallback 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.base.BaseActivity import com.shuwei.dish.match.base.BaseActivity
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.databinding.ListItemScaleRowBinding import com.shuwei.dish.match.databinding.ListItemScaleRowBinding
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.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
/** /**
* 子设备专属页面(小屏幕设备使用) * 子设备专属页面(小屏幕设备使用)
* 仅展示本机秤的实时数据和主设备连接状态 * 仅展示本机秤的实时数据和主设备连接状态
* 布局简洁,适配小屏幕 * 22个秤和18个秤使用与 MasterScaleActivity 相同的网格布局,其余使用线性列表
*/ */
class SlaveActivity : BaseActivity() { class SlaveActivity : BaseActivity() {
private lateinit var binding: ActivitySlaveBinding private lateinit var binding: ActivitySlaveBinding
/** 本机秤数据列表:address → (weight, state) */ /** 本机秤数据列表:address → (weight, state),线性列表模式使用 */
private data class ScaleItem(val address: Int, val weight: Double, val state: Int) private data class ScaleItem(val address: Int, val weight: Double, val state: Int)
private val scaleList = mutableListOf<ScaleItem>() private val scaleList = mutableListOf<ScaleItem>()
private val adapter = ScaleAdapter(scaleList) private val linearAdapter = ScaleAdapter(scaleList)
/** 22个秤网格 adapter */
private var scale22Adapter: Scale22GridAdapter? = null
/** 18个秤网格 adapter */
private var scale18Adapter: Scale18GridAdapter? = null
/** 已连接的主设备数量(有连接即代表主设备在线) */ /** 已连接的主设备数量(有连接即代表主设备在线) */
private var masterConnected = false private var masterConnected = false
@@ -41,23 +56,59 @@ class SlaveActivity : BaseActivity() {
binding.tvDeviceId.text = "设备:${GlobalData.deviceId}" binding.tvDeviceId.text = "设备:${GlobalData.deviceId}"
binding.tvDeviceIp.text = "IP${getLocalIpAddress()}" binding.tvDeviceIp.text = "IP${getLocalIpAddress()}"
binding.rvScaleList.layoutManager = LinearLayoutManager(this)
binding.rvScaleList.itemAnimator = null // 关闭默认动画,避免数据频繁更新时出现闪烁错乱
binding.rvScaleList.adapter = adapter
setupScaleList()
updateMasterStatus(ScaleServiceManager.isMasterConnected) updateMasterStatus(ScaleServiceManager.isMasterConnected)
listenLocalScales() listenLocalScales()
listenMasterConnection() listenMasterConnection()
addBackKeyListener() 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.layoutParams = binding.rvScaleList.layoutParams.also { it.height = rvHeight }
binding.rvScaleList.layoutManager = FlexboxLayoutManager(this).apply {
flexDirection = FlexDirection.COLUMN
flexWrap = FlexWrap.WRAP
alignItems = AlignItems.FLEX_START
}
scale22Adapter = Scale22GridAdapter(largeSize, smallSize)
binding.rvScaleList.adapter = scale22Adapter
}
ScaleDeviceConfig.DEVICE_ID_18 -> {
binding.rvScaleList.layoutManager = GridLayoutManager(this, 6).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int) = 1
}
}
scale18Adapter = Scale18GridAdapter()
binding.rvScaleList.adapter = scale18Adapter
}
else -> {
binding.rvScaleList.layoutManager = LinearLayoutManager(this)
binding.rvScaleList.adapter = linearAdapter
}
}
}
/** /**
* 禁用返回键,防止退回 InitActivity 后重复打开本页 * 禁用返回键,防止退回 InitActivity 后重复打开本页
*/ */
private fun addBackKeyListener() { private fun addBackKeyListener() {
onBackPressedDispatcher.addCallback(this) { onBackPressedDispatcher.addCallback(this) { }
// 禁用返回键,不执行任何操作
}
} }
/** /**
@@ -71,19 +122,39 @@ class SlaveActivity : BaseActivity() {
/** /**
* 监听本机 WeightUtil 回调,实时刷新秤列表 * 监听本机 WeightUtil 回调,实时刷新秤列表
* 22/18个秤走网格 adapter,其余走线性列表
*/ */
private fun listenLocalScales() { private fun listenLocalScales() {
WeightUtil.addWeightListener(TAG) { address, state, weight -> WeightUtil.addWeightListener(TAG) { address, state, weight ->
runOnUiThread { runOnUiThread {
val idx = scaleList.indexOfFirst { it.address == address } when {
val item = ScaleItem(address, weight, state) scale22Adapter != null -> {
if (idx >= 0) { // 用最新数据更新缓存后整体刷新网格
scaleList[idx] = item val existing = scale22Adapter!!.items.toMutableList()
adapter.notifyItemChanged(idx) val idx = existing.indexOfFirst { it.address == address }
} else { val newData = ScaleData(GlobalData.deviceId, address, weight, state, System.currentTimeMillis())
scaleList.add(item) if (idx >= 0) existing[idx] = newData else existing.add(newData)
scaleList.sortBy { it.address } scale22Adapter!!.updateByAddress(existing)
adapter.notifyDataSetChanged() }
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 idx = scaleList.indexOfFirst { it.address == address }
val item = ScaleItem(address, weight, state)
if (idx >= 0) {
scaleList[idx] = item
linearAdapter.notifyItemChanged(idx)
} else {
scaleList.add(item)
scaleList.sortBy { it.address }
linearAdapter.notifyDataSetChanged()
}
}
} }
} }
} }
@@ -91,7 +162,6 @@ class SlaveActivity : BaseActivity() {
/** /**
* 更新主设备连接状态文字 * 更新主设备连接状态文字
* 由 ScaleWebSocketServer 的 onOpen/onClose 回调驱动(可在后续扩展中接入)
*/ */
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun updateMasterStatus(connected: Boolean) { fun updateMasterStatus(connected: Boolean) {
@@ -114,7 +184,6 @@ class SlaveActivity : BaseActivity() {
/** /**
* 获取本机局域网 IP,委托给 NetworkUtil 统一处理版本兼容 * 获取本机局域网 IP,委托给 NetworkUtil 统一处理版本兼容
* 获取失败时返回"未知"
*/ */
private fun getLocalIpAddress(): String { private fun getLocalIpAddress(): String {
return NetworkUtil.getLocalIpAddress(this).ifEmpty { "未知" } return NetworkUtil.getLocalIpAddress(this).ifEmpty { "未知" }
@@ -146,3 +215,4 @@ class SlaveActivity : BaseActivity() {
} }
} }
} }