diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ea2679f..002a5b1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -107,6 +107,7 @@ dependencies { implementation(libs.retrofit) implementation(libs.java.websocket) implementation(libs.converter.gson) + implementation(libs.flexbox) implementation(libs.baserecyclerviewadapterhelper4) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c6172fa..3b24cb5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -106,6 +106,11 @@ android:theme="@style/Theme.DishMatch.NoSplash" android:screenOrientation="portrait" tools:ignore="DiscouragedApi,LockedOrientationActivity" /> + { + Log.d(TAG, "收到清零指令, address=${cmd.address}") + com.shuwei.dish.match.utils.WeightUtil.tareTwo(cmd.address) + } + } + } catch (e: Exception) { + Log.w(TAG, "解析指令失败: ${e.message}") + } } override fun onError(conn: WebSocket?, ex: Exception) { 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 aebff72..57470ae 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 @@ -1,21 +1,31 @@ package com.shuwei.dish.match.ui import android.annotation.SuppressLint +import android.content.Context import android.os.Bundle import android.view.Gravity import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import android.widget.LinearLayout import android.widget.TextView -import androidx.core.view.updateLayoutParams import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.chad.library.adapter4.BaseMultiItemAdapter +import com.chad.library.adapter4.BaseQuickAdapter +import com.chad.library.adapter4.viewholder.QuickViewHolder +import com.google.android.flexbox.AlignItems +import com.google.android.flexbox.FlexDirection +import com.google.android.flexbox.FlexWrap +import com.google.android.flexbox.FlexboxLayoutManager import com.shuwei.dish.match.R import com.shuwei.dish.match.base.BaseActivity import com.shuwei.dish.match.databinding.ActivityMasterScaleBinding -import com.shuwei.dish.match.databinding.ListItemScaleDataBinding +import com.shuwei.dish.match.databinding.ListItemScale22RowBinding +import com.shuwei.dish.match.databinding.ListTypeScale1Binding +import com.shuwei.dish.match.databinding.ListTypeScale18Binding +import com.shuwei.dish.match.databinding.ListTypeScale2Binding +import com.shuwei.dish.match.databinding.ListTypeScale22Binding import com.shuwei.dish.match.scale.ScaleData import com.shuwei.dish.match.scale.ScaleServiceManager import com.shuwei.dish.match.utils.NetworkUtil @@ -24,44 +34,28 @@ import com.shuwei.dish.match.utils.ext.dp import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -/** 根据列数计算正方形格子边长(屏幕可用宽度 / 列数) */ -private fun android.content.Context.cellSize(columns: Int): Int { - val screenWidth = resources.displayMetrics.widthPixels - // 减去左右 padding(16dp * 2)和格子间距(2dp * 2 * columns) - val usable = screenWidth - 32.dp - columns * 4.dp - return usable / columns -} +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 const val DEVICE_ID_2 = "8fc2ab34-2137-3112-acca-f884ea8736d4" // 2个秤 -private const val DEVICE_ID_22 = "a46fa55c-113c-3511-bb1f-41e5eff77c4b" // 22个秤 -private const val DEVICE_ID_18 = "1038da9f-c6eb-326e-a1d9-d6d3af978b22" // 18个秤 -private const val DEVICE_ID_1 = "7cc0f6ea-f13d-3013-a867-fc998eb554ac" // 1个秤 - -/** 22个秤的显示顺序 */ -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) - -/** 18个秤的显示顺序 */ -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 SCALE_ORDER_22 = listOf(20, 22, 19, 21, 6, 12, 18, 5, 11, 17, 4, 10, 16, 3, 9, 15, 2, 8, 14, 1, 7, 13) +private val SCALE_ORDER_18 = listOf(6, 5, 4, 3, 2, 1, 12, 11, 10, 9, 8, 7, 18, 17, 16, 15, 14, 13) private val deviceOrder = listOf(DEVICE_ID_2, DEVICE_ID_22, DEVICE_ID_18, DEVICE_ID_1) +private const val VIEW_TYPE_SCALE2 = 0 +private const val VIEW_TYPE_SCALE22 = 1 +private const val VIEW_TYPE_SCALE18 = 2 +private const val VIEW_TYPE_SCALE1 = 3 + /** - * 主设备秤数据总览页面(仅 MASTER 角色显示) - * 聚合展示所有设备(本机 + 远端子设备)的实时秤数据 - * 数据按设备分组展示:设备号 + IP + 该设备下所有秤数据 - * 数据来源:ScaleServiceManager.allScales(StateFlow) + * 主设备秤数据总览页面 + * 按设备分组展示,每种设备对应独立的 item 布局和 viewType */ class MasterScaleActivity : BaseActivity() { private lateinit var binding: ActivityMasterScaleBinding - /** - * 按设备分组后的数据模型 - * @param deviceId 设备 ID - * @param ip 设备 IP 地址 - * @param scales 该设备下所有秤数据,按 address 排序 - */ private data class DeviceGroup( val deviceId: String, val ip: String, @@ -78,17 +72,13 @@ class MasterScaleActivity : BaseActivity() { setHeaderBackground() binding.ivMasterBack.setOnClickListener { finish() } - binding.rvScaleList.layoutManager = LinearLayoutManager(this) + binding.rvScaleList.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this) binding.rvScaleList.itemAnimator = null binding.rvScaleList.adapter = adapter observeScaleData() } - /** - * 订阅 ScaleServiceManager 的全量秤数据 StateFlow - * 每次有任一秤数据更新时,重新按设备分组并刷新列表 - */ private fun observeScaleData() { val flow = ScaleServiceManager.allScales ?: return lifecycleScope.launch { @@ -98,8 +88,8 @@ class MasterScaleActivity : BaseActivity() { val groups = scaleMap.values .groupBy { it.deviceId } .entries - .sortedWith(compareBy { idx -> - deviceOrder.indexOf(idx.key).let { if (it == -1) Int.MAX_VALUE else it } + .sortedWith(compareBy { entry -> + deviceOrder.indexOf(entry.key).let { if (it == -1) Int.MAX_VALUE else it } }) .map { (deviceId, scales) -> DeviceGroup( @@ -129,250 +119,271 @@ class MasterScaleActivity : BaseActivity() { binding.tvConnectionStatus.text = "已发现设备:$deviceCount 台 / 共 $scaleCount 个秤" } - /** 设备分组适配器,每项展示一台设备及其所有秤数据 */ - private inner class DeviceGroupAdapter(private val data: List) : - RecyclerView.Adapter() { + private inner class DeviceGroupAdapter(data: MutableList) : + BaseMultiItemAdapter(data) { - inner class VH(val binding: ListItemScaleDataBinding) : - RecyclerView.ViewHolder(binding.root) + init { + addItemType(VIEW_TYPE_SCALE2, object : BaseMultiItemAdapter.OnMultiItemAdapterListener { + override fun onCreate(context: Context, parent: ViewGroup, viewType: Int) = + QuickViewHolder(ListTypeScale2Binding.inflate(LayoutInflater.from(context), parent, false).root) + override fun onBind(holder: QuickViewHolder, position: Int, item: DeviceGroup?) { + item ?: return + val b = ListTypeScale2Binding.bind(holder.itemView) + Scale2VH(b).bind(item) + } + }) + addItemType(VIEW_TYPE_SCALE22, object : BaseMultiItemAdapter.OnMultiItemAdapterListener { + override fun onCreate(context: Context, parent: ViewGroup, viewType: Int) = + QuickViewHolder(ListTypeScale22Binding.inflate(LayoutInflater.from(context), parent, false).root) + override fun onBind(holder: QuickViewHolder, position: Int, item: DeviceGroup?) { + item ?: return + val b = ListTypeScale22Binding.bind(holder.itemView) + Scale22VH(b).bind(item) + } + }) + addItemType(VIEW_TYPE_SCALE18, object : BaseMultiItemAdapter.OnMultiItemAdapterListener { + override fun onCreate(context: Context, parent: ViewGroup, viewType: Int) = + QuickViewHolder(ListTypeScale18Binding.inflate(LayoutInflater.from(context), parent, false).root) + override fun onBind(holder: QuickViewHolder, position: Int, item: DeviceGroup?) { + item ?: return + val b = ListTypeScale18Binding.bind(holder.itemView) + Scale18VH(b).bind(item) + } + }) + addItemType(VIEW_TYPE_SCALE1, object : BaseMultiItemAdapter.OnMultiItemAdapterListener { + override fun onCreate(context: Context, parent: ViewGroup, viewType: Int) = + QuickViewHolder(ListTypeScale1Binding.inflate(LayoutInflater.from(context), parent, false).root) + override fun onBind(holder: QuickViewHolder, position: Int, item: DeviceGroup?) { + item ?: return + val b = ListTypeScale1Binding.bind(holder.itemView) + Scale1VH(b).bind(item) + } + }) + onItemViewType { position, list -> when (list.getOrNull(position)?.deviceId) { + DEVICE_ID_2 -> VIEW_TYPE_SCALE2 + DEVICE_ID_22 -> VIEW_TYPE_SCALE22 + DEVICE_ID_18 -> VIEW_TYPE_SCALE18 + else -> VIEW_TYPE_SCALE1 + }} + } + } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = VH( - ListItemScaleDataBinding.inflate(LayoutInflater.from(parent.context), parent, false) - ) + // ── ViewHolder: 2个秤 ────────────────────────────────────────────────── - override fun getItemCount() = data.size + private inner class Scale2VH(val b: ListTypeScale2Binding) : RecyclerView.ViewHolder(b.root) { + @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)) + } + } + } + + // ── ViewHolder: 22个秤 ───────────────────────────────────────────────── + + private inner class Scale22VH(val b: ListTypeScale22Binding) : RecyclerView.ViewHolder(b.root) { + private var bound = false @SuppressLint("SetTextI18n") - override fun onBindViewHolder(holder: VH, position: Int) { - val group = data[position] - holder.binding.tvDeviceLabel.text = "设备:${group.deviceId}" - holder.binding.tvDeviceIp.text = "IP:${group.ip.ifEmpty { "未知" }}" + fun bind(group: DeviceGroup) { + b.tvDeviceIp.text = "IP:${group.ip.ifEmpty { "未知" }}" + b.tvDeviceLabel.text = "设备:${group.deviceId}" - val container: LinearLayout = holder.binding.llScaleContainer - container.removeAllViews() - - // 根据设备号选择不同的网格布局策略 - when (group.deviceId) { - DEVICE_ID_22 -> buildLayout22(container, group.scales) - DEVICE_ID_18 -> buildLayout18(container, group.scales) - DEVICE_ID_2 -> buildLayout2(container, group.scales) - else -> buildLayoutDefault(container, group.scales) + if (!bound) { + // 计算格子尺寸(与 Scale22GridActivity 一致) + val screenWidth = resources.displayMetrics.widthPixels + val usable = screenWidth - 8 * 4.dp - 10.dp // 减去8列格子margin + 外层RV左右margin各5dp + val smallSize = usable / 9 + val largeSize = (usable - smallSize * 6) / 2 + val rvHeight = smallSize * 3 + 3 * 4.dp + b.rvScales.layoutParams = b.rvScales.layoutParams.also { it.height = rvHeight } + b.rvScales.layoutManager = FlexboxLayoutManager(b.root.context).apply { + flexDirection = FlexDirection.COLUMN + flexWrap = FlexWrap.WRAP + alignItems = AlignItems.FLEX_START + } + b.rvScales.itemAnimator = null + b.rvScales.adapter = Scale22InnerAdapter(largeSize, smallSize) + bound = true } + val scaleByAddress = group.scales.associateBy { it.address } + val ordered = SCALE_ORDER_22.map { scaleByAddress[it] } + (b.rvScales.adapter as Scale22InnerAdapter).update(ordered) } } - /** - * 22个秤布局:8列,前2列权重1.5(大格子),后6列权重1.0(小格子) - * 显示顺序:[20,22,19,21,6,12,18,5,11,17,4,10,16,3,9,15,2,8,14,1,7,13] - */ - private fun buildLayout22(container: LinearLayout, scales: List) { + // ── ViewHolder: 18个秤 ───────────────────────────────────────────────── + + private inner class Scale18VH(val b: ListTypeScale18Binding) : RecyclerView.ViewHolder(b.root) { + private var bound = false + + @SuppressLint("SetTextI18n") + fun bind(group: DeviceGroup) { + b.tvDeviceIp.text = "IP:${group.ip.ifEmpty { "未知" }}" + b.tvDeviceLabel.text = "设备:${group.deviceId}" + + if (!bound) { + val gridLayoutManager = GridLayoutManager(b.root.context, 6).apply { + spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + // 每个格子占1 span,共6列 + override fun getSpanSize(position: Int) = 1 + } + } + b.rvScales.layoutManager = gridLayoutManager + b.rvScales.itemAnimator = null + b.rvScales.adapter = Scale18InnerAdapter() + bound = true + } + val scaleByAddress = group.scales.associateBy { it.address } + val ordered = SCALE_ORDER_18.map { scaleByAddress[it] } + (b.rvScales.adapter as Scale18InnerAdapter).update(ordered) + } + } + + // ── ViewHolder: 1个秤 ────────────────────────────────────────────────── + + private inner class Scale1VH(val b: ListTypeScale1Binding) : RecyclerView.ViewHolder(b.root) { + @SuppressLint("SetTextI18n") + fun bind(group: DeviceGroup) { + b.tvDeviceIp.text = "IP:${group.ip.ifEmpty { "未知" }}" + b.tvDeviceLabel.text = "设备:${group.deviceId}" + val scale = group.scales.firstOrNull() + b.tvAddr.text = scale?.let { "秤 ${it.address}" } ?: "" + b.tvWeight.text = scale?.let { "${it.weight} g" } ?: "" + b.tvState.text = stateStr(scale?.state) + } + } + + // ── 22格内嵌 Adapter ─────────────────────────────────────────────────── + + private inner class Scale22InnerAdapter( + private val largeSize: Int, + private val smallSize: Int + ) : BaseQuickAdapter(mutableListOf()) { + + inner class VH(val b: ListItemScale22RowBinding) : QuickViewHolder(b.root) + + // 用 address=0 的哨兵表示空位 + private val empty = ScaleData("", 0, 0.0, -1, 0) + + fun update(newList: List) { + val filled = newList.map { it ?: empty } + if (items.isEmpty()) { + submitList(filled) + } else { + filled.forEachIndexed { i, scale -> + if (items.getOrNull(i) != scale) set(i, scale) + } + } + } + + override fun getItemViewType(position: Int, list: List) = if (position < 4) 1 else 0 + + override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH { + val b = ListItemScale22RowBinding.inflate(LayoutInflater.from(context), parent, false) + val cellSize = if (viewType == 1) largeSize else smallSize + (b.root.layoutParams as? FlexboxLayoutManager.LayoutParams)?.apply { + width = cellSize + height = cellSize + setMargins(2.dp, 2.dp, 2.dp, 2.dp) + } + val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt() + b.tvAddr.textSize = (baseSp * 0.16f).coerceIn(9f, 14f) + b.tvWeight.textSize = (baseSp * 0.22f).coerceIn(12f, 20f) + b.tvState.textSize = (baseSp * 0.14f).coerceIn(8f, 12f) + return VH(b) + } + + @SuppressLint("SetTextI18n") + override fun onBindViewHolder(holder: VH, position: Int, item: ScaleData?) { + val valid = (item?.address ?: 0) != 0 + holder.b.tvAddr.text = if (valid) "秤${item!!.address}" else "" + holder.b.tvWeight.text = if (valid) "${item!!.weight}g" else "" + holder.b.tvState.text = if (valid) stateStr(item!!.state) else "" + } + } + + // ── 18格内嵌 Adapter ─────────────────────────────────────────────────── + + private inner class Scale18InnerAdapter : BaseQuickAdapter(mutableListOf()) { + + inner class VH(val b: ListItemScale22RowBinding) : QuickViewHolder(b.root) + + private val empty = ScaleData("", 0, 0.0, -1, 0) + + 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) + } + } + } + + override fun getItemViewType(position: Int, list: List) = if (position < 6) 0 else 1 + + override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH { + val b = ListItemScale22RowBinding.inflate(LayoutInflater.from(context), parent, false) + // 第1行 margin=12dp(格子视觉更小),第2-3行 margin=2dp + val margin = if (viewType == 0) 12.dp else 2.dp + val screenWidth = parent.resources.displayMetrics.widthPixels + val cellSize = (screenWidth - 6 * margin * 2 - 10.dp) / 6 + b.root.layoutParams = (b.root.layoutParams as ViewGroup.MarginLayoutParams).also { + it.width = cellSize + it.height = cellSize + it.setMargins(margin, margin, margin, margin) + } + val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt() + b.tvAddr.textSize = (baseSp * 0.16f).coerceIn(9f, 14f) + b.tvWeight.textSize = (baseSp * 0.22f).coerceIn(12f, 20f) + b.tvState.textSize = (baseSp * 0.14f).coerceIn(8f, 12f) + return VH(b) + } + + @SuppressLint("SetTextI18n") + override fun onBindViewHolder(holder: VH, position: Int, item: ScaleData?) { + val valid = (item?.address ?: 0) != 0 + holder.b.tvAddr.text = if (valid) "秤${item!!.address}" else "" + holder.b.tvWeight.text = if (valid) "${item!!.weight}g" else "" + holder.b.tvState.text = if (valid) stateStr(item!!.state) else "" + } + } + + // ── 工具方法 ─────────────────────────────────────────────────────────── + + private fun stateStr(state: Int?) = when (state) { + WeightUtil.STATE_STABLE -> "稳定" + WeightUtil.STATE_UNSTABLE -> "不稳定" + WeightUtil.STATE_OVER_WEIGHT -> "超量" + null -> "" + else -> "$state" + } + + /** 创建正方形秤格子(用于 Scale2VH) */ + @SuppressLint("SetTextI18n") + private fun makeScaleCell(scale: ScaleData, cellSize: Int): LinearLayout { val margin = 2.dp - val usable = resources.displayMetrics.widthPixels - 32.dp - val leftColW = (usable * 1.5f / 9f).toInt() - val rightColW = (usable * 1.0f / 9f).toInt() - val largeCellSize = leftColW - margin * 2 - val smallCellSize = rightColW - margin * 2 - - // 按指定顺序重排 - val scaleByAddress = scales.associateBy { it.address } - val sorted = SCALE_ORDER_22.map { scaleByAddress[it] } - - val outerRow = LinearLayout(container.context).apply { - orientation = LinearLayout.HORIZONTAL - layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT - ) - } - var index = 0 - // 前2列:权重1.5,每列2个大格子(正方形) - repeat(2) { colIdx -> - val col = LinearLayout(container.context).apply { - orientation = LinearLayout.VERTICAL - layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1.5f) - } - repeat(2) { rowIdx -> - val cell = makeScaleCellFixed(sorted.getOrNull(index++), largeCellSize, largeCellSize, margin) - // 两列中间水平间距翻倍(第1列右边距、第2列左边距),列内两格子中间垂直间距翻倍,外侧保持不变 - cell.updateLayoutParams { - bottomMargin = if (rowIdx == 0) margin * 2 else margin - topMargin = if (rowIdx == 1) margin * 2 else margin - rightMargin = if (colIdx == 0) margin * 2 else margin - leftMargin = if (colIdx == 1) margin * 2 else margin - } - col.addView(cell) - } - outerRow.addView(col) - } - // 后6列:权重1.0,每列3个小格子(正方形) - repeat(6) { - val col = LinearLayout(container.context).apply { - orientation = LinearLayout.VERTICAL - layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1.0f) - } - repeat(3) { - col.addView(makeScaleCellFixed(sorted.getOrNull(index++), smallCellSize, smallCellSize, margin)) - } - outerRow.addView(col) - } - container.addView(outerRow) - } - - /** - * 创建固定宽高的秤格子(用于22格子布局,宽高可独立指定) - * @param scale 秤数据 - * @param width 内容宽度(px) - * @param height 内容高度(px) - * @param margin 外边距(px) - */ - @SuppressLint("SetTextI18n") - private fun makeScaleCellFixed(scale: ScaleData?, width: Int, height: Int, margin: Int): View { - val lp = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, height) - .also { it.setMargins(margin, margin, margin, margin) } - return LinearLayout(this).apply { orientation = LinearLayout.VERTICAL - layoutParams = lp gravity = Gravity.CENTER + layoutParams = LinearLayout.LayoutParams(0, cellSize - margin * 2, 1f) + .also { it.setMargins(margin, margin, margin, margin) } setPadding(4.dp, 2.dp, 4.dp, 2.dp) setBackgroundResource(R.drawable.shape_scale_cell) - if (scale != null) { - val stateStr = when (scale.state) { - WeightUtil.STATE_STABLE -> "稳定" - WeightUtil.STATE_UNSTABLE -> "不稳定" - WeightUtil.STATE_OVER_WEIGHT -> "超量" - else -> "${scale.state}" - } - val baseSize = (height / resources.displayMetrics.density).toInt() - val addrSp = (baseSize * 0.16f).coerceIn(9f, 14f).toInt() - val weightSp = (baseSize * 0.22f).coerceIn(12f, 20f).toInt() - val stateSp = (baseSize * 0.14f).coerceIn(8f, 12f).toInt() - addView(makeCellTextView("秤${scale.address}", addrSp)) - addView(makeCellTextView("${scale.weight}g", weightSp, bold = true)) - addView(makeCellTextView(stateStr, stateSp)) - } + val baseSize = ((cellSize - margin * 2) / resources.displayMetrics.density).toInt() + 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())) } } - /** - * 18个秤布局:6列3行,第1行小格子,第2-3行大格子 - * 显示顺序:[6,5,4,3,2,1, 12,11,10,9,8,7, 18,17,16,15,14,13] - */ - private fun buildLayout18(container: LinearLayout, scales: List) { - val cellSize = container.context.cellSize(6) - // 第2、3行列间距 10dp(margin=5dp),单独计算格子尺寸 - val cellSize23 = (resources.displayMetrics.widthPixels - 32.dp - 6 * 10.dp) / 6 - // 按指定顺序重排(address 从1开始,转为0-based index取值) - val scaleByAddress = scales.associateBy { it.address } - val sorted = SCALE_ORDER_18.map { scaleByAddress[it] } - - var index = 0 - // 第1行:增大 margin 使格子看起来更小 - val row1 = makeRowLayout(container) - repeat(6) { row1.addView(makeScaleCell(sorted.getOrNull(index++), cellSize, inHorizontalRow = true, margin = 12.dp)) } - container.addView(row1) - repeat(2) { rowIdx -> - val row = makeRowLayout(container) - // 第2行(rowIdx=0)底部加 6dp,使第2、3行间距与第1、2行一致(均为10dp) - if (rowIdx == 0) row.updateLayoutParams { bottomMargin = 6.dp } - repeat(6) { row.addView(makeScaleCell(sorted.getOrNull(index++), cellSize23, inHorizontalRow = true, margin = 5.dp)) } - container.addView(row) - } - } - - /** 2个秤布局:1行2列 */ - private fun buildLayout2(container: LinearLayout, scales: List) { - val cellSize = container.context.cellSize(2) - val row = makeRowLayout(container) - scales.take(2).forEach { row.addView(makeScaleCell(it, cellSize, inHorizontalRow = true)) } - container.addView(row) - } - - /** 默认布局:逐行展示,每行加格子背景 */ - @SuppressLint("SetTextI18n") - private fun buildLayoutDefault(container: LinearLayout, scales: List) { - scales.forEach { scale -> - val stateStr = when (scale.state) { - WeightUtil.STATE_STABLE -> "稳定" - WeightUtil.STATE_UNSTABLE -> "不稳定" - WeightUtil.STATE_OVER_WEIGHT -> "超量" - else -> "${scale.state}" - } - val row = LinearLayout(container.context).apply { - orientation = LinearLayout.HORIZONTAL - gravity = Gravity.CENTER_VERTICAL - layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, 120.dp - ).also { it.setMargins(2.dp, 4.dp, 2.dp, 4.dp) } - setPadding(30.dp, 0, 0, 0) - setBackgroundResource(R.drawable.shape_scale_cell) - } - row.addView(makeTextView("秤 ${scale.address}", 22, weight = 1f)) - row.addView(makeTextView("${scale.weight} g", 26, bold = true, weight = 2f)) - row.addView(makeTextView(stateStr, 18, weight = 1f)) - container.addView(row) - } - } - - /** 创建横向行容器 */ - private fun makeRowLayout(parent: ViewGroup): LinearLayout = - LinearLayout(parent.context).apply { - orientation = LinearLayout.HORIZONTAL - layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT - ) - } - - /** 创建纵向列容器(等宽分配) */ - private fun makeColumnLayout(parent: ViewGroup): LinearLayout = - LinearLayout(parent.context).apply { - orientation = LinearLayout.VERTICAL - layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) - } - - /** - * 创建单个秤格子视图 - * @param scale 秤数据,为 null 时显示空格子占位 - * @param cellSize 格子边长(px),宽高相等实现正方形效果 - * @param inHorizontalRow 是否处于横向行中(true=按权重分配宽度,false=宽度撑满) - * @param margin 格子外边距(px),增大可让格子视觉上更小 - */ - @SuppressLint("SetTextI18n") - private fun makeScaleCell(scale: ScaleData?, cellSize: Int, inHorizontalRow: Boolean, margin: Int = 2.dp): View { - // 实际格子内容高度 = 格子分配宽度 - 左右 margin,保证宽高一致接近正方形 - val contentSize = cellSize - margin * 2 - val lp = if (inHorizontalRow) { - LinearLayout.LayoutParams(0, contentSize, 1f) - } else { - LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, contentSize) - }.also { it.setMargins(margin, margin, margin, margin) } - - return LinearLayout(this).apply { - orientation = LinearLayout.VERTICAL - layoutParams = lp - gravity = Gravity.CENTER - setPadding(4.dp, 2.dp, 4.dp, 2.dp) - setBackgroundResource(R.drawable.shape_scale_cell) - if (scale != null) { - val stateStr = when (scale.state) { - WeightUtil.STATE_STABLE -> "稳定" - WeightUtil.STATE_UNSTABLE -> "不稳定" - WeightUtil.STATE_OVER_WEIGHT -> "超量" - else -> "${scale.state}" - } - // 字号根据格子尺寸自适应(格子越小字越小) - val baseSize = (cellSize / resources.displayMetrics.density).toInt() - val addrSp = (baseSize * 0.16f).coerceIn(9f, 14f).toInt() - val weightSp = (baseSize * 0.22f).coerceIn(12f, 20f).toInt() - val stateSp = (baseSize * 0.14f).coerceIn(8f, 12f).toInt() - addView(makeCellTextView("秤${scale.address}", addrSp)) - addView(makeCellTextView("${scale.weight}g", weightSp, bold = true)) - addView(makeCellTextView(stateStr, stateSp)) - } - } - } - - /** 创建格子内居中 TextView */ - private fun makeCellTextView(text: String, spSize: Int, bold: Boolean = false): TextView = + private fun makeCellText(text: String, spSize: Int, bold: Boolean = false): TextView = TextView(this).apply { this.text = text textSize = spSize.toFloat() @@ -384,15 +395,4 @@ class MasterScaleActivity : BaseActivity() { gravity = Gravity.CENTER includeFontPadding = false } - - /** 创建默认布局行内的 TextView */ - private fun makeTextView( - text: String, spSize: Int, bold: Boolean = false, weight: Float = 1f - ): TextView = TextView(this).apply { - this.text = text - textSize = spSize.toFloat() - if (bold) setTypeface(null, android.graphics.Typeface.BOLD) - setTextColor(if (bold) getColor(R.color.home_title) else getColor(R.color.home_sub_title)) - layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, weight) - } } diff --git a/app/src/main/java/com/shuwei/dish/match/ui/Scale22GridActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/Scale22GridActivity.kt new file mode 100644 index 0000000..12e5778 --- /dev/null +++ b/app/src/main/java/com/shuwei/dish/match/ui/Scale22GridActivity.kt @@ -0,0 +1,142 @@ +package com.shuwei.dish.match.ui + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.RecyclerView +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.base.BaseActivity +import com.shuwei.dish.match.databinding.ActivityScale22GridBinding +import com.shuwei.dish.match.databinding.ListItemScale22RowBinding +import com.shuwei.dish.match.scale.ScaleData +import com.shuwei.dish.match.scale.ScaleServiceManager +import com.shuwei.dish.match.utils.WeightUtil +import com.shuwei.dish.match.utils.ext.dp +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +/** 22格设备的 deviceId */ +private const val DEVICE_ID_22 = "a46fa55c-113c-3511-bb1f-41e5eff77c4b" + +/** + * 列优先顺序:大列1(20,22) → 大列2(19,21) → 小列1~6 + * FlexboxLayoutManager COLUMN 方向按列填充,顺序天然对应 + */ +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) + +/** 前4个 item(大列1+大列2,各2行)为大格子 */ +private fun isLargeCell(position: Int) = position < 4 + +/** + * 22格秤混合网格布局页 + * FlexboxLayoutManager(COLUMN) 竖向排列: + * - 大格子 flexBasisPercent=1.5/9,每列2个,自动换列 + * - 小格子 flexBasisPercent=1.0/9,每列3个,自动换列 + */ +class Scale22GridActivity : BaseActivity() { + + private lateinit var binding: ActivityScale22GridBinding + private val scaleList = mutableListOf() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityScale22GridBinding.inflate(layoutInflater) + setContentView(binding.root) + setHeaderBackground() + + binding.ivBack.setOnClickListener { finish() } + + // 格子尺寸计算: + // 屏幕宽 - 8列×左右margin(2dp×2) = 可用宽,共9份(大1.5+小1.0×6) + val screenWidth = resources.displayMetrics.widthPixels + val usable = screenWidth - 8 * 4.dp - 4.dp // 减去8列格子margin + RecyclerView左右margin各2dp + val smallSize = usable / 9 + val largeSize = (usable - smallSize * 6) / 2 // 剩余宽度平分给2列大格子 + // 高度以小格子3行为准(3行×格子高 + 3行×上下margin各2dp) + val rvHeight = smallSize * 3 + 3 * 4.dp + binding.rvScale22.layoutParams = binding.rvScale22.layoutParams.also { + it.height = rvHeight + } + + binding.rvScale22.layoutManager = FlexboxLayoutManager(this).apply { + flexDirection = FlexDirection.COLUMN + flexWrap = FlexWrap.WRAP + alignItems = AlignItems.FLEX_START + } + binding.rvScale22.itemAnimator = null + binding.rvScale22.adapter = Scale22Adapter(scaleList, largeSize, smallSize) + + observeScaleData() + } + + private fun observeScaleData() { + val flow = ScaleServiceManager.allScales ?: return + lifecycleScope.launch { + flow.collectLatest { scaleMap -> + val scaleByAddress = scaleMap.values + .filter { it.deviceId == DEVICE_ID_22 } + .associateBy { it.address } + val newList = SCALE_ORDER_22.map { scaleByAddress[it] } + if (scaleList.isEmpty()) { + scaleList.addAll(newList) + binding.rvScale22.adapter?.notifyItemRangeInserted(0, scaleList.size) + } else { + newList.forEachIndexed { i, scale -> + if (scaleList[i] != scale) { + scaleList[i] = scale + binding.rvScale22.adapter?.notifyItemChanged(i) + } + } + } + } + } + } + + private inner class Scale22Adapter( + private val data: List, + private val largeSize: Int, // 大格子宽高(px) + private val smallSize: Int // 小格子宽高(px) + ) : RecyclerView.Adapter() { + + inner class VH(val b: ListItemScale22RowBinding) : RecyclerView.ViewHolder(b.root) + + override fun getItemViewType(position: Int) = if (isLargeCell(position)) 1 else 0 + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { + val b = ListItemScale22RowBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val cellSize = if (viewType == 1) largeSize else smallSize + // 固定宽高实现正方形,FlexboxLayoutManager COLUMN 方向按高度装满后自动换列 + (b.root.layoutParams as? FlexboxLayoutManager.LayoutParams)?.apply { + width = cellSize + height = cellSize + setMargins(2.dp, 2.dp, 2.dp, 2.dp) + } + val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt() + b.tvAddr.textSize = (baseSp * 0.16f).coerceIn(9f, 14f) + b.tvWeight.textSize = (baseSp * 0.22f).coerceIn(12f, 20f) + b.tvState.textSize = (baseSp * 0.14f).coerceIn(8f, 12f) + return VH(b) + } + + override fun getItemCount() = data.size + + @SuppressLint("SetTextI18n") + override fun onBindViewHolder(holder: VH, position: Int) { + val scale = data[position] + holder.b.tvAddr.text = scale?.let { "秤${it.address}" } ?: "" + holder.b.tvWeight.text = scale?.let { "${it.weight}g" } ?: "" + holder.b.tvState.text = when (scale?.state) { + WeightUtil.STATE_STABLE -> "稳定" + WeightUtil.STATE_UNSTABLE -> "不稳定" + WeightUtil.STATE_OVER_WEIGHT -> "超量" + null -> "" + else -> "${scale.state}" + } + } + } +} diff --git a/app/src/main/res/layout/activity_master_scale.xml b/app/src/main/res/layout/activity_master_scale.xml index 770efd6..403b48a 100644 --- a/app/src/main/res/layout/activity_master_scale.xml +++ b/app/src/main/res/layout/activity_master_scale.xml @@ -53,10 +53,12 @@ android:id="@+id/rvScaleList" android:layout_width="match_parent" android:layout_height="0dp" + android:layout_marginStart="5dp" + android:layout_marginEnd="5dp" + android:layout_marginTop="8dp" app:layout_constraintTop_toBottomOf="@id/tvConnectionStatus" app:layout_constraintBottom_toBottomOf="parent" android:overScrollMode="never" - android:padding="0dp" - tools:listitem="@layout/list_item_scale_data"/> + tools:listitem="@layout/list_type_scale22"/> diff --git a/app/src/main/res/layout/activity_scale22_grid.xml b/app/src/main/res/layout/activity_scale22_grid.xml new file mode 100644 index 0000000..79662cb --- /dev/null +++ b/app/src/main/res/layout/activity_scale22_grid.xml @@ -0,0 +1,46 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/list_item_scale22_row.xml b/app/src/main/res/layout/list_item_scale22_row.xml new file mode 100644 index 0000000..a66ac96 --- /dev/null +++ b/app/src/main/res/layout/list_item_scale22_row.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/list_type_scale1.xml b/app/src/main/res/layout/list_type_scale1.xml new file mode 100644 index 0000000..aedb50a --- /dev/null +++ b/app/src/main/res/layout/list_type_scale1.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/list_type_scale18.xml b/app/src/main/res/layout/list_type_scale18.xml new file mode 100644 index 0000000..40dcbe2 --- /dev/null +++ b/app/src/main/res/layout/list_type_scale18.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/list_item_scale_data.xml b/app/src/main/res/layout/list_type_scale2.xml similarity index 76% rename from app/src/main/res/layout/list_item_scale_data.xml rename to app/src/main/res/layout/list_type_scale2.xml index dbe3eae..149cd12 100644 --- a/app/src/main/res/layout/list_item_scale_data.xml +++ b/app/src/main/res/layout/list_type_scale2.xml @@ -1,15 +1,12 @@ - + - - - + + android:layout_marginTop="10dp" /> diff --git a/app/src/main/res/layout/list_type_scale22.xml b/app/src/main/res/layout/list_type_scale22.xml new file mode 100644 index 0000000..115d424 --- /dev/null +++ b/app/src/main/res/layout/list_type_scale22.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3b73c9c..f1409e3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,6 +31,7 @@ refreshHeaderClassics = "3.0.0-alpha" refreshHeaderRadar = "3.0.0-alpha" roomRuntime = "2.5.2" java-websocket = "1.5.7" +flexbox = "3.0.0" [libraries] android-core = { module = "com.google.zxing:android-core", version.ref = "androidCore" } @@ -77,6 +78,7 @@ refresh-header-classics = { module = "io.github.scwang90:refresh-header-classics refresh-layout-kernel = { module = "io.github.scwang90:refresh-layout-kernel", version.ref = "refreshLayoutKernel" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "converterGson" } java-websocket = { module = "org.java-websocket:Java-WebSocket", version.ref = "java-websocket" } +flexbox = { module = "com.google.android.flexbox:flexbox", version.ref = "flexbox" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }