5 Commits
Author SHA1 Message Date
lvmeng 87bf11454c feat(fragment): 实现调料秤重量增加高亮提醒功能
主设备订阅 allScales StateFlow 检测所有子设备秤重量变化,
超过10g且秤稳定时高亮对应格子,15秒后自动恢复
2026-04-17 16:56:36 +08:00
lvmengandClaude Sonnet 4.6 768bde79a1 feat(fragment): 新增调料选择弹窗并绑定格子点击交互
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 15:43:11 +08:00
lvmeng c61b1c8ef7 feat(seasoning): 添加调料配置网格适配器功能
- 创建调料配置格子布局文件,仅显示调料名称
- 实现调料配置下层18格网格适配器,使用GridLayoutManager排列
- 实现调料配置上层22格网格适配器,使用FlexboxLayoutManager排列
- 添加不同行列的边距控制和字体大小自适应
- 实现按预设顺序更新数据的功能,空位用哨兵填充
- 预留点击回调接口,暂不处理点击事件
2026-04-17 14:23:57 +08:00
lvmeng 77df4a4d82 refactor(ui): 移除设备配置页面并重构调料配置界面
- 移除 DeviceConfigActivity 页面及相关 AndroidManifest 注册
- 将调料配置从单页网格改为上下两层 RecyclerView 结构
- 上层使用 FlexboxLayoutManager 显示 22 格调料位
- 下层使用 GridLayoutManager 显示 18 格调料位
- 更新存储工具类 SpTool 中烹饪模式的访问方式
- 修改首页和初始化页面中烹饪模式的读取逻辑
- 更新 WebSocket 客户端连接逻辑中的重复连接处理
2026-04-17 14:23:44 +08:00
lvmeng c35572dc41 feat(setting): 添加调料配置功能并优化界面布局
- 在设置页面添加调料配置按钮和设备切换功能
- 将设备配置页面标题从"设备配置"改为"设备切换"
- 新增SeasoningConfigFragment调料配置片段
- 将秤数据监控按钮可见性从gone改为visible
- 优化collectFragment布局加载方式
- 在DeviceConfigActivity中添加调试日志输出
- 重构SettingActivity中的按钮样式管理逻辑
- 调整fragment列表顺序和点击事件处理
- 修改列表项高度以改善界面显示效果
- 注释掉DeviceConfigFragment中未使用的viewModel相关代码
2026-04-17 11:45:08 +08:00
23 changed files with 1476 additions and 564 deletions
+2 -2
View File
@@ -93,8 +93,8 @@
android:theme="@style/Theme.DishMatch.NoSplash" android:theme="@style/Theme.DishMatch.NoSplash"
android:screenOrientation="portrait" android:screenOrientation="portrait"
tools:ignore="DiscouragedApi,LockedOrientationActivity" /> tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity android:name="com.shuwei.dish.match.ui.DeviceConfigActivity" <!-- <activity android:name="com.shuwei.dish.match.ui.DeviceConfigActivity"-->
android:theme="@style/Theme.DishMatch.NoSplash" /> <!-- android:theme="@style/Theme.DishMatch.NoSplash" />-->
<activity android:name="com.shuwei.dish.match.ui.SettingActivity" <activity android:name="com.shuwei.dish.match.ui.SettingActivity"
android:theme="@style/Theme.DishMatch.NoSplash" android:theme="@style/Theme.DishMatch.NoSplash"
android:screenOrientation="portrait" android:screenOrientation="portrait"
@@ -0,0 +1,113 @@
package com.shuwei.dish.match.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemSeasoningCellBinding
import com.shuwei.dish.match.entity.SeasoningEntity
import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.utils.ext.dp
/**
* 调料配置-下层(18格)网格 Adapter
* 使用 GridLayoutManager6列)排列,第1行格子 margin 较大,第2-3行 margin 较小
* 实体类为 SeasoningEntity,格子显示调料名称 goodsName
*/
class Seasoning18GridAdapter : BaseQuickAdapter<SeasoningEntity, Seasoning18GridAdapter.VH>(mutableListOf()) {
/** 空位哨兵,sort=-1 表示无效 */
private val empty = SeasoningEntity(goodsId = "").also { it.sort = -1 }
/** 当前高亮的格子位置,-1 表示无高亮 */
private var highlightedPosition: Int = -1
/** 点击回调,预留,暂不使用 */
var onItemClick: ((SeasoningEntity, Int) -> Unit)? = null
inner class VH(val b: ListItemSeasoningCellBinding) : QuickViewHolder(b.root)
/** 前6个位置为第1行(viewType=0margin大),其余为第2-3行(viewType=1margin小) */
override fun getItemViewType(position: Int, list: List<SeasoningEntity>) =
if (position < 6) 0 else 1
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val b = ListItemSeasoningCellBinding.inflate(LayoutInflater.from(context), parent, false)
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 as? ViewGroup.MarginLayoutParams)?.also {
it.width = cellSize
it.height = cellSize
// 第1行:顶部间距为0,底部间距2dp,左右保持原有 margin
if (viewType == 0) it.setMargins(margin, 0, margin, 2.dp)
else it.setMargins(margin, margin, margin, margin)
}
val baseSp = (cellSize / parent.resources.displayMetrics.density).toInt()
b.tvGoodsName.textSize = (baseSp * 0.18f).coerceIn(10f, 16f)
val vh = VH(b)
// 点击事件预留,暂不处理
b.root.setOnClickListener {
val pos = vh.bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION) {
items.getOrNull(pos)?.let { item -> onItemClick?.invoke(item, pos) }
}
}
return vh
}
override fun onBindViewHolder(holder: VH, position: Int, item: SeasoningEntity?) {
holder.b.tvGoodsName.text = item?.goodsName?.takeIf { it.isNotBlank() } ?: "-"
// 高亮格子:红底白字;其余恢复默认
if (position == highlightedPosition) {
holder.b.tvGoodsName.setBackgroundColor(ContextCompat.getColor(holder.b.root.context, R.color.red_ff4444))
holder.b.tvGoodsName.setTextColor(ContextCompat.getColor(holder.b.root.context, R.color.white))
} else {
holder.b.tvGoodsName.setBackgroundResource(R.drawable.shape_scale_cell)
holder.b.tvGoodsName.setTextColor(ContextCompat.getColor(holder.b.root.context, R.color.home_title))
}
}
/**
* 高亮指定位置的格子(红底白字),同时清除上一个高亮
* @param position 要高亮的格子索引
*/
fun highlightPosition(position: Int) {
val prev = highlightedPosition
highlightedPosition = position
if (prev >= 0) notifyItemChanged(prev)
if (position >= 0) notifyItemChanged(position)
}
/**
* 清除所有高亮,恢复默认样式
*/
fun clearHighlight() {
highlightPosition(-1)
}
/**
* 更新指定位置的调料名称并刷新对应格子
* @param position 格子索引
* @param name 新的调料名称
*/
fun updateItemName(position: Int, name: String) {
items.getOrNull(position)?.goodsName = name
notifyItemChanged(position)
}
/**
* 按 SCALE_ORDER_18 的 sort 顺序更新数据,空位用哨兵填充
*/
fun update(seasoningList: List<SeasoningEntity>) {
val bySort = seasoningList.associateBy { it.sort }
val filled = ScaleDeviceConfig.SCALE_ORDER_18.map { sort ->
bySort[sort] ?: empty.copy().also { it.sort = sort }
}
submitList(filled)
}
}
@@ -0,0 +1,116 @@
package com.shuwei.dish.match.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
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.R
import com.shuwei.dish.match.databinding.ListItemSeasoningCellBinding
import com.shuwei.dish.match.entity.SeasoningEntity
import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.utils.ext.dp
/**
* 调料配置-上层(22格)网格 Adapter
* 使用 FlexboxLayoutManager 按列排列,前4个格子较大,其余较小
* 实体类为 SeasoningEntity,格子显示调料名称 goodsName
*
* @param largeSize 前4个大格子的尺寸(px
* @param smallSize 其余小格子的尺寸(px
*/
class Seasoning22GridAdapter(
private val largeSize: Int,
private val smallSize: Int
) : BaseQuickAdapter<SeasoningEntity, Seasoning22GridAdapter.VH>(mutableListOf()) {
/** 空位哨兵,sort=-1 表示无效 */
private val empty = SeasoningEntity(goodsId = "").also { it.sort = -1 }
/** 当前高亮的格子位置,-1 表示无高亮 */
private var highlightedPosition: Int = -1
/** 点击回调,预留,暂不使用 */
var onItemClick: ((SeasoningEntity, Int) -> Unit)? = null
inner class VH(val b: ListItemSeasoningCellBinding) : QuickViewHolder(b.root)
/** 前4个位置为大格子(viewType=1),其余为小格子(viewType=0 */
override fun getItemViewType(position: Int, list: List<SeasoningEntity>) =
if (position < 4) 1 else 0
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val b = ListItemSeasoningCellBinding.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.tvGoodsName.textSize = (baseSp * 0.18f).coerceIn(10f, 16f)
val vh = VH(b)
// 点击事件预留,暂不处理
b.root.setOnClickListener {
val pos = vh.bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION) {
items.getOrNull(pos)?.let { item -> onItemClick?.invoke(item, pos) }
}
}
return vh
}
override fun onBindViewHolder(holder: VH, position: Int, item: SeasoningEntity?) {
holder.b.tvGoodsName.text = item?.goodsName?.takeIf { it.isNotBlank() } ?: "-"
// 高亮格子:红底白字;其余恢复默认
if (position == highlightedPosition) {
holder.b.tvGoodsName.setBackgroundColor(ContextCompat.getColor(holder.b.root.context, R.color.red_ff4444))
holder.b.tvGoodsName.setTextColor(ContextCompat.getColor(holder.b.root.context, R.color.white))
} else {
holder.b.tvGoodsName.setBackgroundResource(R.drawable.shape_scale_cell)
holder.b.tvGoodsName.setTextColor(ContextCompat.getColor(holder.b.root.context, R.color.home_title))
}
}
/**
* 高亮指定位置的格子(红底白字),同时清除上一个高亮
* @param position 要高亮的格子索引
*/
fun highlightPosition(position: Int) {
val prev = highlightedPosition
highlightedPosition = position
if (prev >= 0) notifyItemChanged(prev)
if (position >= 0) notifyItemChanged(position)
}
/**
* 清除所有高亮,恢复默认样式
*/
fun clearHighlight() {
highlightPosition(-1)
}
/**
* 更新指定位置的调料名称并刷新对应格子
* @param position 格子索引
* @param name 新的调料名称
*/
fun updateItemName(position: Int, name: String) {
items.getOrNull(position)?.goodsName = name
notifyItemChanged(position)
}
/**
* 按 SCALE_ORDER_22 的 sort 顺序更新数据,空位用哨兵填充
*/
fun update(seasoningList: List<SeasoningEntity>) {
val bySort = seasoningList.associateBy { it.sort }
val filled = ScaleDeviceConfig.SCALE_ORDER_22.map { sort ->
bySort[sort] ?: empty.copy().also { it.sort = sort }
}
submitList(filled)
}
}
@@ -0,0 +1,191 @@
package com.shuwei.dish.match.dialog
import android.annotation.SuppressLint
import android.os.Handler
import android.os.Looper
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.core.view.isEmpty
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.SeasoningSearchAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.DialogSeasoningSelectBinding
import com.shuwei.dish.match.entity.SeasoningEntity
import com.shuwei.dish.match.utils.KeyboardUtil
import com.shuwei.dish.match.utils.ext.addOnActionSearchListener
import com.shuwei.dish.match.utils.ext.toast
/**
* 调料选择弹窗,不含重量显示,继承 BottomSheetDialog 确保只初始化一次
* @param activity 宿主 Activity
* @param clickIndex 点击的调料槽位索引
* @param onItemSelected 用户点击某一调料时的回调
*/
class SeasoningSelectDialog(
private val activity: BaseActivity,
private val clickIndex: Int,
private val onItemSelected: (item: SeasoningEntity) -> Unit
) : BottomSheetDialog(activity, R.style.BottomSheet) {
companion object {
const val TAG = "SeasoningSelectDialog"
}
private val binding = DialogSeasoningSelectBinding.inflate(LayoutInflater.from(activity))
private val list = mutableListOf<SeasoningEntity>()
private val adapter = SeasoningSearchAdapter(list).apply {
setOnItemClickListener { _, _, position ->
list[position].isClicked = true
notifyItemChanged(position)
Handler(Looper.getMainLooper()).postDelayed({
onItemSelected(list[position])
dismiss()
}, 300)
}
}
private var goodsName: String? = null
private var pageNo = 1
private val pageSize = 50
init {
setContentView(binding.root)
setCancelable(true)
behavior.skipCollapsed = false
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN)
window?.setGravity(Gravity.BOTTOM)
window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
window?.setWindowAnimations(R.style.DialogSoftInputAnimation)
window?.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
setOnDismissListener {
activity.hideStatusBar()
}
// 输入框:清空时重置列表
binding.etSheetInput.run {
hint = "输入调料名称"
addTextChangedListener(
onTextChanged = { text, _, _, _ -> goodsName = text.toString() },
afterTextChanged = {
if (it.isNullOrBlank()) {
list.clear()
adapter.notifyDataSetChanged()
}
}
)
addOnActionSearchListener { searchGoods(this) }
}
// RecyclerView 初始化及滑动冲突处理
binding.recyclerView.run {
layoutManager = GridLayoutManager(activity, 2, GridLayoutManager.VERTICAL, false)
adapter = this@SeasoningSelectDialog.adapter
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(rv, dx, dy)
// 解决 RecyclerView 与 SmartRefreshLayout 滑动冲突
val topRowVerticalPosition = if (rv.isEmpty()) 0 else rv.getChildAt(0).top
binding.refreshLayout.setNestedScrollingEnabled(topRowVerticalPosition >= 0)
}
})
}
binding.refreshLayout.run {
setEnableRefresh(false)
setEnableLoadMore(false)
setOnRefreshListener { pageNo = 1; getGoodsList() }
setOnLoadMoreListener { getGoodsList() }
}
binding.ivSearch.setOnClickListener { searchGoods(it) }
binding.root.setOnClickListener { KeyboardUtil.hideKeyboard(it.context, it) }
}
/** 弹窗显示时自动加载默认调料列表 */
override fun show() {
super.show()
pageNo = 1
getGoodsList()
}
/**
* 触发搜索:校验输入、隐藏键盘、发起请求
*/
private fun searchGoods(v: View) {
if (goodsName.isNullOrBlank()) {
activity.toast("${binding.etSheetInput.hint}")
return
}
pageNo = 1
getGoodsList()
KeyboardUtil.hideKeyboard(v.context, v)
}
/**
* 请求调料列表(临时使用本地模拟数据,正式联调时替换为真实接口)
*/
private fun getGoodsList() {
// TODO: 联调时注释掉下方模拟数据,取消注释真实接口调用
val mockData = mutableListOf(
SeasoningEntity(goodsId = "1", goodsName = "精盐"),
SeasoningEntity(goodsId = "2", goodsName = "鸡精"),
SeasoningEntity(goodsId = "3", goodsName = "陈醋"),
SeasoningEntity(goodsId = "4", goodsName = "老抽"),
SeasoningEntity(goodsId = "5", goodsName = "生抽"),
SeasoningEntity(goodsId = "6", goodsName = "花生油"),
SeasoningEntity(goodsId = "7", goodsName = "菜籽油"),
SeasoningEntity(goodsId = "8", goodsName = "胡椒粉"),
SeasoningEntity(goodsId = "9", goodsName = "十三香"),
SeasoningEntity(goodsId = "10", goodsName = "料酒"),
SeasoningEntity(goodsId = "11", goodsName = "白醋")
)
loadGoodsList(mockData)
// val param = mutableMapOf<String, Any>(
// "goodsType" to "1",
// "placeId" to BaseApp.canteenId,
// "pageNum" to pageNo,
// "pageSize" to pageSize
// )
// if (!goodsName.isNullOrBlank()) {
// param["goodsName"] = goodsName!!
// }
// activity.querySeasoningList(param = param, onSuccess = {
// loadGoodsList(it)
// }, onFailure = { _, msg ->
// activity.toast(msg)
// finishRefresh()
// })
}
/**
* 将返回数据填充到列表
*/
@SuppressLint("NotifyDataSetChanged")
private fun loadGoodsList(records: MutableList<SeasoningEntity>?) {
finishRefresh()
if (records.isNullOrEmpty()) {
activity.toast("暂未搜索到调料信息")
return
}
if (pageNo == 1) list.clear()
list.addAll(records)
adapter.notifyDataSetChanged()
val isLoadMoreEnable = records.size >= pageSize
binding.refreshLayout.setEnableLoadMore(isLoadMoreEnable)
if (isLoadMoreEnable) pageNo++
}
private fun finishRefresh() {
if (pageNo == 1) binding.refreshLayout.finishRefresh()
else binding.refreshLayout.finishLoadMore()
}
}
@@ -0,0 +1,20 @@
package com.shuwei.dish.match.scale
/**
* 秤事件数据类,用于子设备向主设备发送非重量类通知
* @param type 事件类型,参见 companion object 中的常量
* @param deviceId 发送方设备 ID
* @param address 触发事件的秤地址
* @param delta 重量变化量(克),仅 TYPE_SEASONING_ADDED 时有意义
*/
data class ScaleEvent(
val type: String,
val deviceId: String,
val address: Int,
val delta: Double = 0.0
) {
companion object {
/** 调料添加事件:某秤重量增加超过阈值 */
const val TYPE_SEASONING_ADDED = "seasoning_added"
}
}
@@ -46,6 +46,26 @@ object ScaleServiceManager {
val allScales: StateFlow<Map<String, ScaleData>>? val allScales: StateFlow<Map<String, ScaleData>>?
get() = aggregator?.allScales get() = aggregator?.allScales
/**
* 收到子设备秤事件(如调料添加)时的回调,仅主设备有效
* 在子线程中调用,需自行切换到主线程更新 UI
*/
private var _onScaleEvent: ((ScaleEvent) -> Unit)? = null
var onScaleEvent: ((ScaleEvent) -> Unit)?
get() = _onScaleEvent
set(value) {
_onScaleEvent = value
wsClient?.onScaleEvent = value
}
/**
* 向主设备广播秤事件(子设备调用)
* @param event 要广播的事件
*/
fun broadcastEvent(event: ScaleEvent) {
wsServer?.broadcastEvent(event)
}
/** /**
* 主设备连接状态回调(仅子设备使用) * 主设备连接状态回调(仅子设备使用)
* 设置后会立即转发给 wsServer;需在 start() 之后设置才能生效 * 设置后会立即转发给 wsServer;需在 start() 之后设置才能生效
@@ -100,6 +120,7 @@ object ScaleServiceManager {
// WebSocket 客户端 // WebSocket 客户端
val client = ScaleWebSocketClient().also { val client = ScaleWebSocketClient().also {
it.onScaleData = { data -> aggregator?.onRemoteScaleData(data) } it.onScaleData = { data -> aggregator?.onRemoteScaleData(data) }
it.onScaleEvent = { event -> _onScaleEvent?.invoke(event) }
it.onDeviceDisconnected = { remoteId -> aggregator?.removeDevice(remoteId) } it.onDeviceDisconnected = { remoteId -> aggregator?.removeDevice(remoteId) }
} }
wsClient = client wsClient = client
@@ -49,6 +49,9 @@ class ScaleWebSocketClient {
/** 收到秤数据时的回调,在子线程调用 */ /** 收到秤数据时的回调,在子线程调用 */
var onScaleData: ((data: ScaleData) -> Unit)? = null var onScaleData: ((data: ScaleData) -> Unit)? = null
/** 收到秤事件(如调料添加通知)时的回调,在子线程调用 */
var onScaleEvent: ((event: ScaleEvent) -> Unit)? = null
/** 设备断线时的回调(连接失败或关闭),在子线程调用 */ /** 设备断线时的回调(连接失败或关闭),在子线程调用 */
var onDeviceDisconnected: ((deviceId: String) -> Unit)? = null var onDeviceDisconnected: ((deviceId: String) -> Unit)? = null
@@ -60,6 +63,8 @@ class ScaleWebSocketClient {
*/ */
fun connect(deviceId: String, host: String, port: Int) { fun connect(deviceId: String, host: String, port: Int) {
removedDevices.remove(deviceId) removedDevices.remove(deviceId)
// 取消旧 IP 的重连任务,防止 IP 变化时新旧任务并发竞争
reconnectTasks.remove(deviceId)?.cancel(false)
reconnectDelays[deviceId] = RECONNECT_BASE_MS reconnectDelays[deviceId] = RECONNECT_BASE_MS
doConnect(deviceId, host, port) doConnect(deviceId, host, port)
} }
@@ -78,10 +83,17 @@ class ScaleWebSocketClient {
override fun onMessage(webSocket: WebSocket, text: String) { override fun onMessage(webSocket: WebSocket, text: String) {
try { try {
val data = gson.fromJson(text, ScaleData::class.java) // 通过 type 字段区分 ScaleEvent 与 ScaleData
onScaleData?.invoke(data) val raw = gson.fromJson(text, Map::class.java)
if (raw.containsKey("type")) {
val event = gson.fromJson(text, ScaleEvent::class.java)
onScaleEvent?.invoke(event)
} else {
val data = gson.fromJson(text, ScaleData::class.java)
onScaleData?.invoke(data)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "解析秤数据失败: ${e.message}") Log.w(TAG, "解析消息失败: ${e.message}")
} }
} }
@@ -83,6 +83,14 @@ class ScaleWebSocketServer(private val deviceId: String, private val context: Co
Log.d(TAG, "WebSocket 服务端已停止") Log.d(TAG, "WebSocket 服务端已停止")
} }
/**
* 向所有已连接的主设备广播秤事件(如调料添加通知)
* @param event 要广播的事件
*/
fun broadcastEvent(event: ScaleEvent) {
broadcast(gson.toJson(event))
}
/** /**
* 向所有已连接客户端广播消息 * 向所有已连接客户端广播消息
*/ */
@@ -1,345 +1,347 @@
package com.shuwei.dish.match.ui //package com.shuwei.dish.match.ui
//
import android.graphics.Typeface //import android.graphics.Typeface
import android.os.Bundle //import android.os.Bundle
import android.text.SpannableStringBuilder //import android.text.SpannableStringBuilder
import android.text.style.AbsoluteSizeSpan //import android.text.style.AbsoluteSizeSpan
import android.text.style.ForegroundColorSpan //import android.text.style.ForegroundColorSpan
import android.text.style.LineHeightSpan //import android.text.style.LineHeightSpan
import android.text.style.StyleSpan //import android.text.style.StyleSpan
import android.util.Log //import android.util.Log
import android.util.SparseArray //import android.util.SparseArray
import android.widget.FrameLayout //import android.widget.FrameLayout
import androidx.core.graphics.toColorInt //import androidx.core.graphics.toColorInt
import androidx.core.view.forEach //import androidx.core.view.forEach
import androidx.lifecycle.ViewModelProvider //import androidx.lifecycle.ViewModelProvider
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.BaseApp
import com.shuwei.dish.match.db.AppRepository //import com.shuwei.dish.match.db.AppRepository
import com.shuwei.dish.match.dialog.CommonDialog //import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.entity.SeasoningEntity //import com.shuwei.dish.match.entity.SeasoningEntity
import com.shuwei.dish.match.entity.ResetReasoningRecord //import com.shuwei.dish.match.entity.ResetReasoningRecord
import com.shuwei.dish.match.utils.AddressUtil //import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.JsonAssetsLoader //import com.shuwei.dish.match.utils.JsonAssetsLoader
import com.shuwei.dish.match.utils.MultiClickDetector //import com.shuwei.dish.match.utils.MultiClickDetector
import com.shuwei.dish.match.utils.WeightUtil //import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.appendText //import com.shuwei.dish.match.utils.ext.appendText
import com.shuwei.dish.match.utils.ext.buildSpannableString //import com.shuwei.dish.match.utils.ext.buildSpannableString
import com.shuwei.dish.match.utils.ext.dp //import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.startActivity //import com.shuwei.dish.match.utils.ext.startActivity
import com.shuwei.dish.match.utils.ext.toast //import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible //import com.shuwei.dish.match.utils.ext.visible
import com.shuwei.dish.match.viewmodel.AppViewModel //import com.shuwei.dish.match.viewmodel.AppViewModel
import com.shuwei.dish.match.viewmodel.factory.AppFactory //import com.shuwei.dish.match.viewmodel.factory.AppFactory
import com.shuwei.dish.match.adapter.TextCellAdapter //import com.shuwei.dish.match.adapter.TextCellAdapter
import com.shuwei.dish.match.databinding.ActivityDeviceConfigBinding //import com.shuwei.dish.match.databinding.ActivityDeviceConfigBinding
import com.shuwei.dish.match.dialog.SeasoningSearchDialog //import com.shuwei.dish.match.dialog.SeasoningSearchDialog
import com.shuwei.dish.match.utils.SpTool //import com.shuwei.dish.match.utils.SpTool
import com.shuwei.dish.match.utils.ext.clickWithDebounce //import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.gone //import com.shuwei.dish.match.utils.ext.gone
//import com.shuwei.dish.match.utils.ext.toJsonString
class DeviceConfigActivity : BaseActivity() { //
//class DeviceConfigActivity : BaseActivity() {
companion object { //
// companion object {
const val TAG = "DeviceConfigActivity" //
// const val TAG = "DeviceConfigActivity"
const val COOK_MODE = "cookMode" //
// const val COOK_MODE = "cookMode"
val modeList = listOf<ModeBean>( //
ModeBean("制作模式", "菜品快速制作,记录熟重"), // val modeList = listOf<ModeBean>(
ModeBean("采样模式", "菜品快速制作,记录熟重"), // ModeBean("制作模式", "菜品快速制作,记录熟重"),
ModeBean("品控模式", "菜品快速制作,记录熟重") // ModeBean("采样模式", "菜品快速制作,记录熟重"),
) // ModeBean("品控模式", "菜品快速制作,记录熟重")
} // )
// }
data class ModeBean( //
var modeName: String, // data class ModeBean(
var modeDesc: String // var modeName: String,
) // var modeDesc: String
// )
private lateinit var binding: ActivityDeviceConfigBinding //
// private lateinit var binding: ActivityDeviceConfigBinding
private lateinit var appViewModel: AppViewModel //
// private lateinit var appViewModel: AppViewModel
private var cookMode: Int = 0 //
// private val weightArray = SparseIntArray() // private var cookMode: Int = 0
private val weightArray = SparseArray<Double>() //// private val weightArray = SparseIntArray()
private val addressArray = AddressUtil.getWeighAddressArray() // private val weightArray = SparseArray<Double>()
// private val addressArray = AddressUtil.getWeighAddressArray()
override fun onCreate(savedInstanceState: Bundle?) { //
super.onCreate(savedInstanceState) // override fun onCreate(savedInstanceState: Bundle?) {
binding = ActivityDeviceConfigBinding.inflate(layoutInflater) // super.onCreate(savedInstanceState)
setContentView(binding.root) // binding = ActivityDeviceConfigBinding.inflate(layoutInflater)
cookMode = intent.getIntExtra(COOK_MODE, 0) // setContentView(binding.root)
initViewModel() // cookMode = intent.getIntExtra(COOK_MODE, 0)
setHeaderBackground() // initViewModel()
setTitleBar(titleBarAction = { // setHeaderBackground()
it.visible() // setTitleBar(titleBarAction = {
}, titleAction = { // it.visible()
it.text = "设备配置" // }, titleAction = {
}, rightIconActon = { // it.text = "设备配置"
it.gone() // }, rightIconActon = {
// it.alpha = 0.0F // it.gone()
// it.setImageResource(R.drawable.ic_setting) //// it.alpha = 0.0F
// it.setOnClickListener {_-> //// it.setImageResource(R.drawable.ic_setting)
// detector.setOnDelayedMultiClickListener(it) { //// it.setOnClickListener {_->
// defaultDataSettingDialog() //// detector.setOnDelayedMultiClickListener(it) {
// } //// defaultDataSettingDialog()
//// }
//// }
// })
//
// loadQualitySpan(isEnable = false)
// when (cookMode) {
// 1 -> {
// binding.rbModeCook.isChecked = true
// binding.rbModeSampling.isChecked = false
// loadCookSpan(isClicked = true)
// loadSamplingSpan()
// } // }
}) //
// 2 -> {
loadQualitySpan(isEnable = false) // binding.rbModeCook.isChecked = false
when (cookMode) { // binding.rbModeSampling.isChecked = true
1 -> { // loadCookSpan()
binding.rbModeCook.isChecked = true // loadSamplingSpan(isClicked = true)
binding.rbModeSampling.isChecked = false // }
loadCookSpan(isClicked = true) //
loadSamplingSpan() // else -> {
} // binding.rbModeCook.isChecked = true
// binding.rbModeSampling.isChecked = false
2 -> { // loadCookSpan(isClicked = true)
binding.rbModeCook.isChecked = false // loadSamplingSpan()
binding.rbModeSampling.isChecked = true // }
loadCookSpan() // }
loadSamplingSpan(isClicked = true) //
} // binding.rbModeCook.setOnClickListener {
// SpTool.put(SpTool.LAUNCH_PAGE_TYPE, 0)
else -> { // binding.rbModeCook.isChecked = true
binding.rbModeCook.isChecked = true // binding.rbModeSampling.isChecked = false
binding.rbModeSampling.isChecked = false // loadCookSpan(isClicked = true)
loadCookSpan(isClicked = true) // loadSamplingSpan()
loadSamplingSpan() // startActivity<SelectDishActivity> {
} // putExtra(SelectDishActivity.IS_CONFIG_PAGE, true)
} // }
// finish()
binding.rbModeCook.setOnClickListener { // }
SpTool.put(SpTool.LAUNCH_PAGE_TYPE, 0) // binding.rbModeSampling.setOnClickListener {
binding.rbModeCook.isChecked = true // SpTool.put(SpTool.LAUNCH_PAGE_TYPE, 1)
binding.rbModeSampling.isChecked = false // binding.rbModeCook.isChecked = false
loadCookSpan(isClicked = true) // binding.rbModeSampling.isChecked = true
loadSamplingSpan() // loadCookSpan()
startActivity<SelectDishActivity> { // loadSamplingSpan(isClicked = true)
putExtra(SelectDishActivity.IS_CONFIG_PAGE, true) // startActivity<SamplingListActivity> {
} // putExtra(SamplingListActivity.IS_CONFIG_PAGE, true) }
finish() // finish()
} // }
binding.rbModeSampling.setOnClickListener { // addWeighListener()
SpTool.put(SpTool.LAUNCH_PAGE_TYPE, 1) // addGridItemListener()
binding.rbModeCook.isChecked = false // loadSeasoning()
binding.rbModeSampling.isChecked = true // }
loadCookSpan() //
loadSamplingSpan(isClicked = true) // private fun addWeighListener() {
startActivity<SamplingListActivity> { // WeightUtil.addWeightListener(
putExtra(SamplingListActivity.IS_CONFIG_PAGE, true) } // weightKey = TAG,
finish() // getWeight = { address, state, weight ->
} // Log.d(TAG, "addWeighListener: address=$address,stat=$state,weight=$weight")
addWeighListener() // weightArray.put(address, weight)
addGridItemListener() // val item = seasoningItems.firstOrNull { address == addressArray[it.sort] }
loadSeasoning() // item?.let {
} // it.useWeight = weight.toDouble()
// updateGridData(it)
private fun addWeighListener() { // }
WeightUtil.addWeightListener( // Log.d(TAG, "addWeighListener: update-----------------------")
weightKey = TAG, // })
getWeight = { address, state, weight -> // }
Log.d(TAG, "addWeighListener: address=$address,stat=$state,weight=$weight") //
weightArray.put(address, weight) // private var seasoningItems = mutableListOf<SeasoningEntity>()
val item = seasoningItems.firstOrNull { address == addressArray[it.sort] } // private fun addGridItemListener() {
item?.let { // binding.include.root.forEach { child ->
it.useWeight = weight.toDouble() // child.clickWithDebounce {
updateGridData(it) // val sort = child.tag.toString().toInt()
} // val entity = seasoningItems.firstOrNull { it.sort == sort } ?: SeasoningEntity()
Log.d(TAG, "addWeighListener: update-----------------------") // clickGridItem(sort, entity)
}) // }
} // }
// }
private var seasoningItems = mutableListOf<SeasoningEntity>() //
private fun addGridItemListener() { // val detector = MultiClickDetector(targetCount = 10, intervalMs = 800)
binding.include.root.forEach { child -> //
child.clickWithDebounce { // private fun defaultDataSettingDialog() {
val sort = child.tag.toString().toInt() // CommonDialog(this)
val entity = seasoningItems.firstOrNull { it.sort == sort } ?: SeasoningEntity() // .setTitle("温馨提示")
clickGridItem(sort, entity) // .setContent("您好,使用默认配置作为您的调料数据,则已有配置将被清除,确认吗?")
} // .setNegativeButton("取消")
} // .setPositiveButton("确认") { resetList() }
} // .setOnDismissCallback { hideStatusBar() }
// .show()
val detector = MultiClickDetector(targetCount = 10, intervalMs = 800) // }
//
private fun defaultDataSettingDialog() { // private fun resetList() {
CommonDialog(this) // runCatching {
.setTitle("温馨提示") // val record = JsonAssetsLoader.parseJsonFromAssets<ResetReasoningRecord>(
.setContent("您好,使用默认配置作为您的调料数据,则已有配置将被清除,确认吗?") // this,
.setNegativeButton("取消") // "default_seasoning_list.json"
.setPositiveButton("确认") { resetList() } // )
.setOnDismissCallback { hideStatusBar() } //// val weightArray = WeightUtil.weightArray
.show() // appViewModel.clearAllSeasoning {
} // record?.list?.forEach { entity ->
// entity.useWeight = weightArray[addressArray[entity.sort]].toDouble()
private fun resetList() { // appViewModel.saveSeasoning(entity) {
runCatching { // updateGridData(entity)
val record = JsonAssetsLoader.parseJsonFromAssets<ResetReasoningRecord>( // }
this, // }
"default_seasoning_list.json" //// loadSeasoning()
) // }
// val weightArray = WeightUtil.weightArray // }.onFailure { it.printStackTrace() }
appViewModel.clearAllSeasoning { // }
record?.list?.forEach { entity -> //
entity.useWeight = weightArray[addressArray[entity.sort]].toDouble() // private fun initViewModel() {
appViewModel.saveSeasoning(entity) { // val db = BaseApp.instance!!.database
updateGridData(entity) // val factory =
} // AppFactory(AppRepository(db.appDao()))
} // appViewModel =
// loadSeasoning() // ViewModelProvider(this, factory)[AppViewModel::class.java]
} // }
}.onFailure { it.printStackTrace() } //
} // private fun loadSeasoning() {
// appViewModel.loadSeasoning {
private fun initViewModel() { // seasoningItems.clear()
val db = BaseApp.instance!!.database // seasoningItems.addAll(it)
val factory = // initConfigData()
AppFactory(AppRepository(db.appDao())) // setGridData(seasoningItems)
appViewModel = // }
ViewModelProvider(this, factory)[AppViewModel::class.java] // }
} //
// private fun initConfigData() {
private fun loadSeasoning() { // repeat(12) { num ->
appViewModel.loadSeasoning { // val firstOne = seasoningItems.firstOrNull { it.sort == num }
seasoningItems.clear() // if (firstOne == null) {
seasoningItems.addAll(it) // seasoningItems.add(SeasoningEntity().also { it.sort = num })
initConfigData() // }
setGridData(seasoningItems) // }
} // }
} //
// private fun setGridData(list: MutableList<SeasoningEntity>) {
private fun initConfigData() { // Log.d(TAG, "setGridData: ${list.toJsonString()}")
repeat(12) { num -> // val gridLayout = binding.include.root
val firstOne = seasoningItems.firstOrNull { it.sort == num } // list.forEach { entity ->
if (firstOne == null) { // val tag = entity.sort.toString()
seasoningItems.add(SeasoningEntity().also { it.sort = num }) // val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag)
} // TextCellAdapter.loadLayout(frameLayout, entity)
} // }
} // }
//
private fun setGridData(list: MutableList<SeasoningEntity>) { // private fun updateGridData(entity: SeasoningEntity) {
val gridLayout = binding.include.root // val gridLayout = binding.include.root
list.forEach { entity -> // val tag = entity.sort.toString()
val tag = entity.sort.toString() // val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag)
val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag) // TextCellAdapter.loadLayout(frameLayout, entity)
TextCellAdapter.loadLayout(frameLayout, entity) // }
} //
} // private fun clickGridItem(sort: Int, entity: SeasoningEntity) {
// val currentAddress = AddressUtil.getWeighAddressArray().get(sort)
private fun updateGridData(entity: SeasoningEntity) { //// val seasoningEntity = list.first{it.sort == sort}
val gridLayout = binding.include.root // SeasoningSearchDialog(
val tag = entity.sort.toString() // activity = this,
val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag) // weighIndex = sort,
TextCellAdapter.loadLayout(frameLayout, entity) // weighAddress = currentAddress,
} // clickName = entity.goodsName
// ) { item ->
private fun clickGridItem(sort: Int, entity: SeasoningEntity) { // syncItem(entity, item)
val currentAddress = AddressUtil.getWeighAddressArray().get(sort) // //3-调料
// val seasoningEntity = list.first{it.sort == sort} // entity.materialType = 3
SeasoningSearchDialog( // entity.sort = sort
activity = this, // saveSeasoning(entity) {
weighIndex = sort, // val tag = sort.toString()
weighAddress = currentAddress, // val gridLayout = binding.include.root
clickName = entity.goodsName // val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag)
) { item -> // TextCellAdapter.loadLayout(frameLayout, entity)
syncItem(entity, item) // }
//3-调料 // }.show()
entity.materialType = 3 // }
entity.sort = sort //
saveSeasoning(entity) { // private fun syncItem(oldItem: SeasoningEntity, newItem: SeasoningEntity) {
val tag = sort.toString() // oldItem.run {
val gridLayout = binding.include.root // goodsId = newItem.goodsId
val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag) // goodsName = newItem.goodsName
TextCellAdapter.loadLayout(frameLayout, entity) // goodsOrRelationCode = newItem.goodsOrRelationCode
} // relateionType = newItem.relateionType
}.show() // materialType = newItem.materialType
} // allEdible = newItem.allEdible
// useWeight = newItem.useWeight
private fun syncItem(oldItem: SeasoningEntity, newItem: SeasoningEntity) { // popularName = newItem.popularName
oldItem.run { // canteenId = newItem.canteenId
goodsId = newItem.goodsId // relateionType_dictText = newItem.relateionType_dictText
goodsName = newItem.goodsName //
goodsOrRelationCode = newItem.goodsOrRelationCode // foodId = newItem.foodId
relateionType = newItem.relateionType // sort = newItem.sort
materialType = newItem.materialType // }
allEdible = newItem.allEdible // }
useWeight = newItem.useWeight //
popularName = newItem.popularName // private fun loadQualitySpan(isClicked: Boolean = false, isEnable: Boolean = true) {
canteenId = newItem.canteenId // binding.rbModeQuality.text =
relateionType_dictText = newItem.relateionType_dictText // getModeTextSpan(
// topText = modeList[2].modeName,
foodId = newItem.foodId // bottomText = modeList[2].modeDesc,
sort = newItem.sort // topColor = if (isEnable) if (isClicked) "#00BC71" else "#000000" else "#B4B4B4",
} // bottomColor = if (isEnable) "#8B8B8B" else "#B4B4B4"
} // )
// }
private fun loadQualitySpan(isClicked: Boolean = false, isEnable: Boolean = true) { //
binding.rbModeQuality.text = // private fun loadSamplingSpan(isClicked: Boolean = false, isEnable: Boolean = true) {
getModeTextSpan( // binding.rbModeSampling.text =
topText = modeList[2].modeName, // getModeTextSpan(
bottomText = modeList[2].modeDesc, // topText = modeList[1].modeName,
topColor = if (isEnable) if (isClicked) "#00BC71" else "#000000" else "#B4B4B4", // bottomText = modeList[1].modeDesc,
bottomColor = if (isEnable) "#8B8B8B" else "#B4B4B4" // topColor = if (isEnable) if (isClicked) "#00BC71" else "#000000" else "#B4B4B4",
) // bottomColor = if (isEnable) "#8B8B8B" else "#B4B4B4"
} // )
// }
private fun loadSamplingSpan(isClicked: Boolean = false, isEnable: Boolean = true) { //
binding.rbModeSampling.text = // private fun loadCookSpan(isClicked: Boolean = false, isEnable: Boolean = true) {
getModeTextSpan( // binding.rbModeCook.text =
topText = modeList[1].modeName, // getModeTextSpan(
bottomText = modeList[1].modeDesc, // topText = modeList[0].modeName,
topColor = if (isEnable) if (isClicked) "#00BC71" else "#000000" else "#B4B4B4", // bottomText = modeList[0].modeDesc,
bottomColor = if (isEnable) "#8B8B8B" else "#B4B4B4" // topColor = if (isEnable) if (isClicked) "#00BC71" else "#000000" else "#B4B4B4",
) // bottomColor = if (isEnable) "#8B8B8B" else "#B4B4B4"
} // )
// }
private fun loadCookSpan(isClicked: Boolean = false, isEnable: Boolean = true) { //
binding.rbModeCook.text = // private fun getModeTextSpan(
getModeTextSpan( // topText: String,
topText = modeList[0].modeName, // topColor: String,
bottomText = modeList[0].modeDesc, // bottomText: String,
topColor = if (isEnable) if (isClicked) "#00BC71" else "#000000" else "#B4B4B4", // bottomColor: String
bottomColor = if (isEnable) "#8B8B8B" else "#B4B4B4" // ): SpannableStringBuilder {
) //
} //
// return buildSpannableString {
private fun getModeTextSpan( // appendText(
topText: String, // topText,
topColor: String, // ForegroundColorSpan(topColor.toColorInt()),
bottomText: String, // StyleSpan(Typeface.BOLD),
bottomColor: String // AbsoluteSizeSpan(36, true),
): SpannableStringBuilder { // LineHeightSpan { text, start, end, spanstartv, v, fm ->
// fm.descent += 10.dp // 增加行间距
// }
return buildSpannableString { // )
appendText( // append("\n")
topText, // appendText(
ForegroundColorSpan(topColor.toColorInt()), // bottomText,
StyleSpan(Typeface.BOLD), // ForegroundColorSpan(bottomColor.toColorInt()),
AbsoluteSizeSpan(36, true), // AbsoluteSizeSpan(26, true)
LineHeightSpan { text, start, end, spanstartv, v, fm -> // )
fm.descent += 10.dp // 增加行间距 // }
} // }
) //
append("\n") // fun saveSeasoning(entity: SeasoningEntity, action: () -> Unit = {}) {
appendText( // appViewModel.saveSeasoning(entity) {
bottomText, // action()
ForegroundColorSpan(bottomColor.toColorInt()), // toast("保存成功")
AbsoluteSizeSpan(26, true) // }
) // }
} //
} //}
fun saveSeasoning(entity: SeasoningEntity, action: () -> Unit = {}) {
appViewModel.saveSeasoning(entity) {
action()
toast("保存成功")
}
}
}
@@ -107,7 +107,7 @@ class HomeActivity : BaseActivity() {
modeDesc = "菜品快速制作,记录熟重", modeDesc = "菜品快速制作,记录熟重",
modeIconId = R.drawable.ic_mode_cook, modeIconId = R.drawable.ic_mode_cook,
onClick = { onClick = {
SpTool.put(SpTool.LAUNCH_PAGE_TYPE, 0) SpTool.cookMode = 0
startActivity<SelectDishActivity>() startActivity<SelectDishActivity>()
finish() finish()
}) })
@@ -118,7 +118,7 @@ class HomeActivity : BaseActivity() {
modeDesc = "菜品快速制作,记录熟重", modeDesc = "菜品快速制作,记录熟重",
modeIconId = R.drawable.ic_mode_sampling, modeIconId = R.drawable.ic_mode_sampling,
onClick = { onClick = {
SpTool.put(SpTool.LAUNCH_PAGE_TYPE, 1) SpTool.cookMode = 1
goSampling() goSampling()
}) })
) )
@@ -83,8 +83,7 @@ class InitActivity : BaseActivity() {
return@launch return@launch
} }
// 主设备:走原有路由逻辑 // 主设备:走原有路由逻辑
val launchPageType = SpTool.getInt(SpTool.LAUNCH_PAGE_TYPE, -1) when (SpTool.cookMode) {
when (launchPageType) {
0 -> { 0 -> {
startActivity<SelectDishActivity>() startActivity<SelectDishActivity>()
} }
@@ -3,10 +3,12 @@ package com.shuwei.dish.match.ui
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.AppCompatButton
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@@ -20,6 +22,7 @@ import com.shuwei.dish.match.databinding.ActivitySettingBinding
import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding
import com.shuwei.dish.match.ui.fragment.CollectFragment import com.shuwei.dish.match.ui.fragment.CollectFragment
import com.shuwei.dish.match.ui.fragment.DeviceConfigFragment import com.shuwei.dish.match.ui.fragment.DeviceConfigFragment
import com.shuwei.dish.match.ui.fragment.SeasoningConfigFragment
import com.shuwei.dish.match.utils.CameraUtils import com.shuwei.dish.match.utils.CameraUtils
import com.shuwei.dish.match.utils.SpTool import com.shuwei.dish.match.utils.SpTool
import com.shuwei.dish.match.utils.ext.dp import com.shuwei.dish.match.utils.ext.dp
@@ -72,18 +75,29 @@ class SettingActivity : BaseActivity() {
// 设备配置按钮点击事件 // 设备配置按钮点击事件
binding.btnDeviceConfig.setOnClickListener { binding.btnDeviceConfig.setOnClickListener {
showDeviceConfigFragment() showDeviceConfigFragment()
updateButtonColors(isDeviceConfigSelected = true)
loadTabStyle(button = binding.btnDeviceConfig, isSelected = true)
loadTabStyle(button = binding.btnSeasoningConfig, isSelected = false)
loadTabStyle(button = binding.btnFoodCollect, isSelected = false)
}
// 调料配置按钮点击事件
binding.btnSeasoningConfig.setOnClickListener {
showSeasoningConfigFragment()
loadTabStyle(button = binding.btnDeviceConfig, isSelected = false)
loadTabStyle(button = binding.btnSeasoningConfig, isSelected = true)
loadTabStyle(button = binding.btnFoodCollect, isSelected = false)
} }
// 菜品采集按钮点击事件 // 菜品采集按钮点击事件
binding.btnFoodCollect.setOnClickListener { binding.btnFoodCollect.setOnClickListener {
checkCameraPermissionAndShowFragment() checkCameraPermissionAndShowFragment()
updateButtonColors(isDeviceConfigSelected = false)
}
// // 设备角色只读展示,角色由设备 ID 决定,不可手动切换 loadTabStyle(button = binding.btnDeviceConfig, isSelected = false)
// updateRoleButtonText() loadTabStyle(button = binding.btnSeasoningConfig, isSelected = false)
// binding.btnDeviceRole.isEnabled = false loadTabStyle(button = binding.btnFoodCollect, isSelected = true)
}
// 主设备显示「秤数据监控」入口 // 主设备显示「秤数据监控」入口
if (GlobalData.deviceRole == DeviceRole.MASTER) { if (GlobalData.deviceRole == DeviceRole.MASTER) {
@@ -94,7 +108,10 @@ class SettingActivity : BaseActivity() {
} }
// 初始化按钮颜色(默认设备配置为选中状态) // 初始化按钮颜色(默认设备配置为选中状态)
updateButtonColors(isDeviceConfigSelected = true)
loadTabStyle(button = binding.btnDeviceConfig, isSelected = true)
loadTabStyle(button = binding.btnSeasoningConfig, isSelected = false)
loadTabStyle(button = binding.btnFoodCollect, isSelected = false)
} }
// /** 刷新角色按钮文字 */ // /** 刷新角色按钮文字 */
@@ -104,43 +121,26 @@ class SettingActivity : BaseActivity() {
// } // }
/** /**
* 更新底部菜单按钮的颜色状态 * 加载顶部Tab按钮样式
* @param isDeviceConfigSelected 设备配置按钮是否被选中 *
* @param button 按钮
* @param isSelected 是否选中
*/ */
private fun updateButtonColors(isDeviceConfigSelected: Boolean) { private fun loadTabStyle(button: AppCompatButton, isSelected: Boolean) {
val selectedColor = "#FFFFFF".toColorInt() // 选中颜色(白色) button.apply {
val unselectedColor = "#5E7585".toColorInt() // 未选中颜色(灰色) if (isSelected){
// 选中
if (isDeviceConfigSelected) { setTextColor(Color.WHITE)
// 设备配置按钮:选中状态
binding.btnDeviceConfig.apply {
setTextColor(selectedColor)
textSize = 28f textSize = 28f
setTypeface(null, android.graphics.Typeface.BOLD) setTypeface(null, android.graphics.Typeface.BOLD)
background = ContextCompat.getDrawable(this@SettingActivity, R.drawable.bg_btn_underline) background = ContextCompat.getDrawable(this@SettingActivity, R.drawable.bg_btn_underline)
} } else {
// 采集食材按钮:未选中状态 // 未选中
binding.btnFoodCollect.apply { setTextColor("#5E7585".toColorInt())
setTextColor(unselectedColor)
textSize = 26f textSize = 26f
setTypeface(null, android.graphics.Typeface.NORMAL) setTypeface(null, android.graphics.Typeface.NORMAL)
background = null background = null
} }
} else {
// 设备配置按钮:未选中状态
binding.btnDeviceConfig.apply {
setTextColor(unselectedColor)
textSize = 26f
setTypeface(null, android.graphics.Typeface.NORMAL)
background = null
}
// 采集食材按钮:选中状态
binding.btnFoodCollect.apply {
setTextColor(selectedColor)
textSize = 28f
setTypeface(null, android.graphics.Typeface.BOLD)
background = ContextCompat.getDrawable(this@SettingActivity, R.drawable.bg_btn_underline)
}
} }
} }
@@ -149,9 +149,10 @@ class SettingActivity : BaseActivity() {
*/ */
@SuppressLint("ObsoleteSdkInt") @SuppressLint("ObsoleteSdkInt")
private fun checkCameraPermissionAndShowFragment() { private fun checkCameraPermissionAndShowFragment() {
val collectFragment = fragmentList[2]
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
// Android 6.0 以下,直接显示 Fragment // Android 6.0 以下,直接显示 Fragment
showFragment(fragmentList[1]) showFragment(collectFragment)
return return
} }
if (ContextCompat.checkSelfPermission( if (ContextCompat.checkSelfPermission(
@@ -160,13 +161,13 @@ class SettingActivity : BaseActivity() {
) == PackageManager.PERMISSION_GRANTED ) == PackageManager.PERMISSION_GRANTED
) { ) {
// 权限已授予,直接显示 Fragment // 权限已授予,直接显示 Fragment
showFragment(fragmentList[1]) showFragment(collectFragment)
} else { } else {
// 权限未授予,申请权限 // 权限未授予,申请权限
requestPermission(Manifest.permission.CAMERA) { isGranted -> requestPermission(Manifest.permission.CAMERA) { isGranted ->
if (isGranted) { if (isGranted) {
// 权限已授予,显示 CollectFragment // 权限已授予,显示 CollectFragment
showFragment(fragmentList[1]) showFragment(collectFragment)
} else { } else {
// 权限被拒绝 // 权限被拒绝
toast("暂无相机权限,无法使用菜品采集功能") toast("暂无相机权限,无法使用菜品采集功能")
@@ -216,19 +217,22 @@ class SettingActivity : BaseActivity() {
} }
/** /**
* 显示设备配置Fragment * 显示设备切换Fragment
*/ */
private fun showDeviceConfigFragment() { private fun showDeviceConfigFragment() {
// val fragment = DeviceConfigFragment.newInstance()
// supportFragmentManager.beginTransaction()
// .replace(binding.fragmentContainer.id, fragment)
// .addToBackStack(null)
// .commit()
showFragment(fragmentList[0]) showFragment(fragmentList[0])
} }
/**
* 显示设调料配置Fragment
*/
private fun showSeasoningConfigFragment() {
showFragment(fragmentList[1])
}
private val fragmentList = mutableListOf<Fragment>().apply { private val fragmentList = mutableListOf<Fragment>().apply {
add(DeviceConfigFragment.newInstance()) add(DeviceConfigFragment())
add(SeasoningConfigFragment())
add(CollectFragment()) add(CollectFragment())
} }
@@ -82,7 +82,7 @@ class CollectFragment : BaseFragment<FragmentCollectBinding>() {
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup? container: ViewGroup?
): FragmentCollectBinding { ): FragmentCollectBinding {
return FragmentCollectBinding.inflate(LayoutInflater.from(context)) return FragmentCollectBinding.inflate(inflater, container, false)
} }
//typealias CameraCallback = (Uri) -> Unit //typealias CameraCallback = (Uri) -> Unit
@@ -51,21 +51,11 @@ class DeviceConfigFragment : BaseFragment<FragmentDeviceConfigBinding>() {
companion object { companion object {
const val TAG = "DeviceConfigFragment" const val TAG = "DeviceConfigFragment"
const val COOK_MODE = "cookMode"
val modeList = listOf<ModeBean>( val modeList = listOf<ModeBean>(
ModeBean("制作模式", "菜品快速制作,记录熟重"), ModeBean("制作模式", "菜品快速制作,记录熟重"),
ModeBean("采样模式", "菜品快速制作,记录熟重"), ModeBean("采样模式", "菜品快速制作,记录熟重"),
ModeBean("品控模式", "菜品快速制作,记录熟重") ModeBean("品控模式", "菜品快速制作,记录熟重")
) )
fun newInstance(cookMode: Int = 0): DeviceConfigFragment {
return DeviceConfigFragment().apply {
arguments = Bundle().apply {
putInt(COOK_MODE, cookMode)
}
}
}
} }
data class ModeBean( data class ModeBean(
@@ -73,9 +63,8 @@ class DeviceConfigFragment : BaseFragment<FragmentDeviceConfigBinding>() {
var modeDesc: String var modeDesc: String
) )
private lateinit var appViewModel: AppViewModel // private lateinit var appViewModel: AppViewModel
private var cookMode: Int = 0
private val weightArray = SparseArray<Double>() private val weightArray = SparseArray<Double>()
private val addressArray = AddressUtil.getWeighAddressArray() private val addressArray = AddressUtil.getWeighAddressArray()
private var seasoningItems = mutableListOf<SeasoningEntity>() private var seasoningItems = mutableListOf<SeasoningEntity>()
@@ -89,21 +78,20 @@ class DeviceConfigFragment : BaseFragment<FragmentDeviceConfigBinding>() {
} }
override fun initialize(){ override fun initialize(){
cookMode = arguments?.getInt(COOK_MODE, 0) ?: 0
initViewModel() initViewModel()
initUI() initUI()
addWeighListener() // addWeighListener()
addGridItemListener() // addGridItemListener()
loadSeasoning() // loadSeasoning()
} }
/** /**
* 初始化ViewModel * 初始化ViewModel
*/ */
private fun initViewModel() { private fun initViewModel() {
val db = BaseApp.instance!!.database // val db = BaseApp.instance!!.database
val factory = AppFactory(AppRepository(db.appDao())) // val factory = AppFactory(AppRepository(db.appDao()))
appViewModel = ViewModelProvider(this, factory)[AppViewModel::class.java] // appViewModel = ViewModelProvider(this, factory)[AppViewModel::class.java]
} }
/** /**
@@ -111,15 +99,15 @@ class DeviceConfigFragment : BaseFragment<FragmentDeviceConfigBinding>() {
*/ */
private fun initUI() { private fun initUI() {
loadQualitySpan(isEnable = false) loadQualitySpan(isEnable = false)
when (cookMode) { when (SpTool.cookMode) {
1 -> { 0 -> {
binding.rbModeCook.isChecked = true binding.rbModeCook.isChecked = true
binding.rbModeSampling.isChecked = false binding.rbModeSampling.isChecked = false
loadCookSpan(isClicked = true) loadCookSpan(isClicked = true)
loadSamplingSpan() loadSamplingSpan()
} }
2 -> { 1 -> {
binding.rbModeCook.isChecked = false binding.rbModeCook.isChecked = false
binding.rbModeSampling.isChecked = true binding.rbModeSampling.isChecked = true
loadCookSpan() loadCookSpan()
@@ -135,7 +123,7 @@ class DeviceConfigFragment : BaseFragment<FragmentDeviceConfigBinding>() {
} }
binding.rbModeCook.setOnClickListener { binding.rbModeCook.setOnClickListener {
SpTool.put(SpTool.LAUNCH_PAGE_TYPE, 0) SpTool.cookMode = 0
binding.rbModeCook.isChecked = true binding.rbModeCook.isChecked = true
binding.rbModeSampling.isChecked = false binding.rbModeSampling.isChecked = false
loadCookSpan(isClicked = true) loadCookSpan(isClicked = true)
@@ -146,7 +134,7 @@ class DeviceConfigFragment : BaseFragment<FragmentDeviceConfigBinding>() {
activity?.finish() activity?.finish()
} }
binding.rbModeSampling.setOnClickListener { binding.rbModeSampling.setOnClickListener {
SpTool.put(SpTool.LAUNCH_PAGE_TYPE, 1) SpTool.cookMode = 1
binding.rbModeCook.isChecked = false binding.rbModeCook.isChecked = false
binding.rbModeSampling.isChecked = true binding.rbModeSampling.isChecked = true
loadCookSpan() loadCookSpan()
@@ -156,138 +144,138 @@ class DeviceConfigFragment : BaseFragment<FragmentDeviceConfigBinding>() {
activity?.finish() activity?.finish()
} }
} }
//
/** // /**
* 添加称重监听 // * 添加称重监听
*/ // */
private fun addWeighListener() { // private fun addWeighListener() {
WeightUtil.addWeightListener( // WeightUtil.addWeightListener(
weightKey = TAG, // weightKey = TAG,
getWeight = { address, state, weight -> // getWeight = { address, state, weight ->
Log.d(TAG, "addWeighListener: address=$address,stat=$state,weight=$weight") // Log.d(TAG, "addWeighListener: address=$address,stat=$state,weight=$weight")
weightArray.put(address, weight) // weightArray.put(address, weight)
val item = seasoningItems.firstOrNull { address == addressArray[it.sort] } // val item = seasoningItems.firstOrNull { address == addressArray[it.sort] }
item?.let { // item?.let {
it.useWeight = weight.toDouble() // it.useWeight = weight.toDouble()
updateGridData(it) // updateGridData(it)
} // }
Log.d(TAG, "addWeighListener: update-----------------------") // Log.d(TAG, "addWeighListener: update-----------------------")
}) // })
} // }
//
/** // /**
* 添加网格项点击监听 // * 添加网格项点击监听
*/ // */
private fun addGridItemListener() { // private fun addGridItemListener() {
binding.include.root.forEach { child -> // binding.include.root.forEach { child ->
child.clickWithDebounce { // child.clickWithDebounce {
val sort = child.tag.toString().toInt() // val sort = child.tag.toString().toInt()
val entity = seasoningItems.firstOrNull { it.sort == sort } ?: SeasoningEntity() // val entity = seasoningItems.firstOrNull { it.sort == sort } ?: SeasoningEntity()
clickGridItem(sort, entity) // clickGridItem(sort, entity)
} // }
} // }
} // }
//
/** // /**
* 加载调料数据 // * 加载调料数据
*/ // */
private fun loadSeasoning() { // private fun loadSeasoning() {
appViewModel.loadSeasoning { //// appViewModel.loadSeasoning {
seasoningItems.clear() //// seasoningItems.clear()
seasoningItems.addAll(it) //// seasoningItems.addAll(it)
initConfigData() //// initConfigData()
setGridData(seasoningItems) //// setGridData(seasoningItems)
} //// }
} // }
//
/** // /**
* 初始化配置数据,确保有12个调料位置 // * 初始化配置数据,确保有12个调料位置
*/ // */
private fun initConfigData() { // private fun initConfigData() {
repeat(12) { num -> // repeat(12) { num ->
val firstOne = seasoningItems.firstOrNull { it.sort == num } // val firstOne = seasoningItems.firstOrNull { it.sort == num }
if (firstOne == null) { // if (firstOne == null) {
seasoningItems.add(SeasoningEntity().also { it.sort = num }) // seasoningItems.add(SeasoningEntity().also { it.sort = num })
} // }
} // }
} // }
//
/** // /**
* 设置网格数据 // * 设置网格数据
*/ // */
private fun setGridData(list: MutableList<SeasoningEntity>) { // private fun setGridData(list: MutableList<SeasoningEntity>) {
val gridLayout = binding.include.root // val gridLayout = binding.include.root
list.forEach { entity -> // list.forEach { entity ->
val tag = entity.sort.toString() // val tag = entity.sort.toString()
val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag) // val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag)
TextCellAdapter.loadLayout(frameLayout, entity) // TextCellAdapter.loadLayout(frameLayout, entity)
} // }
} // }
//
/** // /**
* 更新网格数据 // * 更新网格数据
*/ // */
private fun updateGridData(entity: SeasoningEntity) { // private fun updateGridData(entity: SeasoningEntity) {
val gridLayout = binding.include.root // val gridLayout = binding.include.root
val tag = entity.sort.toString() // val tag = entity.sort.toString()
val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag) // val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag)
TextCellAdapter.loadLayout(frameLayout, entity) // TextCellAdapter.loadLayout(frameLayout, entity)
} // }
//
/** // /**
* 点击网格项,打开调料搜索对话框 // * 点击网格项,打开调料搜索对话框
*/ // */
private fun clickGridItem(sort: Int, entity: SeasoningEntity) { // private fun clickGridItem(sort: Int, entity: SeasoningEntity) {
val currentAddress = AddressUtil.getWeighAddressArray().get(sort) // val currentAddress = AddressUtil.getWeighAddressArray().get(sort)
SeasoningSearchDialog( // SeasoningSearchDialog(
activity = requireActivity() as BaseActivity, // activity = requireActivity() as BaseActivity,
weighIndex = sort, // weighIndex = sort,
weighAddress = currentAddress, // weighAddress = currentAddress,
clickName = entity.goodsName // clickName = entity.goodsName
) { item -> // ) { item ->
syncItem(entity, item) // syncItem(entity, item)
// 3-调料 // // 3-调料
entity.materialType = 3 // entity.materialType = 3
entity.sort = sort // entity.sort = sort
saveSeasoning(entity) { // saveSeasoning(entity) {
val tag = sort.toString() // val tag = sort.toString()
val gridLayout = binding.include.root // val gridLayout = binding.include.root
val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag) // val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag)
TextCellAdapter.loadLayout(frameLayout, entity) // TextCellAdapter.loadLayout(frameLayout, entity)
} // }
}.show() // }.show()
} // }
//
/** // /**
* 同步调料项数据 // * 同步调料项数据
*/ // */
private fun syncItem(oldItem: SeasoningEntity, newItem: SeasoningEntity) { // private fun syncItem(oldItem: SeasoningEntity, newItem: SeasoningEntity) {
oldItem.run { // oldItem.run {
goodsId = newItem.goodsId // goodsId = newItem.goodsId
goodsName = newItem.goodsName // goodsName = newItem.goodsName
goodsOrRelationCode = newItem.goodsOrRelationCode // goodsOrRelationCode = newItem.goodsOrRelationCode
relateionType = newItem.relateionType // relateionType = newItem.relateionType
materialType = newItem.materialType // materialType = newItem.materialType
allEdible = newItem.allEdible // allEdible = newItem.allEdible
useWeight = newItem.useWeight // useWeight = newItem.useWeight
popularName = newItem.popularName // popularName = newItem.popularName
canteenId = newItem.canteenId // canteenId = newItem.canteenId
relateionType_dictText = newItem.relateionType_dictText // relateionType_dictText = newItem.relateionType_dictText
//
foodId = newItem.foodId // foodId = newItem.foodId
sort = newItem.sort // sort = newItem.sort
} // }
} // }
//
/** // /**
* 保存调料数据 // * 保存调料数据
*/ // */
fun saveSeasoning(entity: SeasoningEntity, action: () -> Unit = {}) { // fun saveSeasoning(entity: SeasoningEntity, action: () -> Unit = {}) {
appViewModel.saveSeasoning(entity) { // appViewModel.saveSeasoning(entity) {
action() // action()
toast("保存成功") // toast("保存成功")
} // }
} // }
/** /**
* 加载品控模式文本样式 * 加载品控模式文本样式
@@ -359,6 +347,6 @@ class DeviceConfigFragment : BaseFragment<FragmentDeviceConfigBinding>() {
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
// 移除称重监听 // 移除称重监听
WeightUtil.removeWeightListener(TAG) // WeightUtil.removeWeightListener(TAG)
} }
} }
@@ -0,0 +1,278 @@
package com.shuwei.dish.match.ui.fragment
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
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.Seasoning18GridAdapter
import com.shuwei.dish.match.adapter.Seasoning22GridAdapter
import com.shuwei.dish.match.base.BaseFragment
import com.shuwei.dish.match.base.DeviceRole
import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.databinding.FragmentSeasoningConfigBinding
import com.shuwei.dish.match.dialog.SeasoningSelectDialog
import com.shuwei.dish.match.scale.ScaleDeviceConfig
import com.shuwei.dish.match.scale.ScaleEvent
import com.shuwei.dish.match.scale.ScaleServiceManager
import com.shuwei.dish.match.ui.SettingActivity
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.dp
import kotlinx.coroutines.launch
class SeasoningConfigFragment : BaseFragment<FragmentSeasoningConfigBinding>() {
companion object {
private const val TAG = "SeasoningConfigFragment"
/** 重量增加阈值(克),超过此值才触发高亮 */
private const val WEIGHT_DELTA_THRESHOLD = 10.0
/** 高亮自动恢复时间(毫秒) */
private const val HIGHLIGHT_DURATION_MS = 15_000L
}
override fun inflateBinding(
inflater: LayoutInflater,
container: ViewGroup?
) = FragmentSeasoningConfigBinding.inflate(inflater, container, false)
private lateinit var settingActivity: SettingActivity
// private lateinit var appViewModel: AppViewModel
private val adapter22 by lazy { buildAdapter22() }
private val adapter18 by lazy { Seasoning18GridAdapter() }
/** 各秤地址的上一次重量快照,用于计算 delta */
private val weightSnapshot = mutableMapOf<Int, Double>()
/** 主线程 Handler,用于延迟清除高亮 */
private val mainHandler = Handler(Looper.getMainLooper())
/** 当前高亮清除任务,新高亮触发时取消旧任务 */
private var clearHighlightRunnable: Runnable? = null
override fun initialize() {
settingActivity = activity as SettingActivity
// initViewModel()
setupRecyclerViews()
bindAdapterClicks()
addWeightListener()
loadData()
}
/** 绑定两个 Adapter 的格子点击,弹出调料选择弹窗 */
private fun bindAdapterClicks() {
adapter22.onItemClick = { _, position ->
SeasoningSelectDialog(
activity = settingActivity,
clickIndex = position,
onItemSelected = { item ->
adapter22.updateItemName(position, item.goodsName ?: "")
}
).show()
}
adapter18.onItemClick = { _, position ->
SeasoningSelectDialog(
activity = settingActivity,
clickIndex = position,
onItemSelected = { item ->
adapter18.updateItemName(position, item.goodsName ?: "")
}
).show()
}
}
// /** 初始化 ViewModel */
// private fun initViewModel() {
// val factory = AppFactory(AppRepository(BaseApp.instance!!.database.appDao()))
// appViewModel = ViewModelProvider(this, factory)[AppViewModel::class.java]
// }
/** 构建上层(22格)Adapter,需要先计算格子尺寸 */
private fun buildAdapter22(): Seasoning22GridAdapter {
val usable = resources.displayMetrics.widthPixels - 8 * 4.dp - 10.dp
val smallSize = usable / 9
val largeSize = (usable - smallSize * 6) / 2
return Seasoning22GridAdapter(largeSize, smallSize)
}
/** 配置两个 RecyclerView */
private fun setupRecyclerViews() {
// 上层:FlexboxLayoutManager,仿 MasterScaleActivity 中 Scale22VH 的布局方式
// rvTop 高度 = 小格子尺寸 * 3行 + 3 * 间距,与 Scale22VH 保持一致
val screenWidth = resources.displayMetrics.widthPixels
val usable = screenWidth - 8 * 4.dp - 10.dp
val smallSize = usable / 9
val rvTopHeight = smallSize * 3 + 3 * 4.dp
binding.rvTop.apply {
layoutParams = layoutParams.also { it.height = rvTopHeight }
layoutManager = FlexboxLayoutManager(requireContext()).apply {
flexDirection = FlexDirection.COLUMN
flexWrap = FlexWrap.WRAP
alignItems = AlignItems.FLEX_START
}
itemAnimator = null
adapter = adapter22
}
// 下层:GridLayoutManager 6列,仿 Scale18VH
binding.rvBottom.apply {
layoutManager = GridLayoutManager(requireContext(), 6)
itemAnimator = null
adapter = adapter18
}
}
/** 加载默认空数据,使两个列表完整展示所有格子 */
private fun loadData() {
adapter22.update(emptyList())
adapter18.update(emptyList())
// appViewModel.loadSeasoning { list ->
// adapter22.update(list ?: emptyList())
// adapter18.update(list ?: emptyList())
// }
}
/**
* 注册重量监听,检测重量增加超过阈值的秤,高亮对应格子并通知主设备
*
* 策略:
* - MASTER 设备:订阅 ScaleServiceManager.allScales StateFlow,覆盖本机+远端所有秤数据
* - SLAVE 设备:注册 WeightUtil 监听(仅本机秤)并向主设备广播事件
*
* 基准更新策略:
* - 首次收到数据:直接记录基准,不触发高亮
* - delta > 阈值:触发高亮,同时更新基准防止重复触发
* - STATE_STABLE:无条件更新基准(覆盖取走调料/归零等场景)
*/
private fun addWeightListener() {
Log.d(TAG, "addWeightListener: 注册监听, deviceId=${GlobalData.deviceId}, role=${GlobalData.deviceRole}")
if (GlobalData.deviceRole == DeviceRole.MASTER) {
// 主设备:通过 allScales StateFlow 观测所有秤(含远端子设备秤)
val allScalesFlow = ScaleServiceManager.allScales
if (allScalesFlow == null) {
Log.w(TAG, "allScales 为 null,主设备聚合器未启动")
return
}
viewLifecycleOwner.lifecycleScope.launch {
allScalesFlow.collect { scaleMap ->
scaleMap.forEach { (key, data) ->
val snapshotKey = data.address + data.deviceId.hashCode() * 1000
val baseline = weightSnapshot[snapshotKey]
if (baseline == null) {
// 首次收到:记录基准
weightSnapshot[snapshotKey] = data.weight
return@forEach
}
val delta = data.weight - baseline
Log.d(TAG, "allScales: key=$key address=${data.address} state=${data.state} weight=${data.weight} baseline=$baseline delta=$delta")
when {
// 仅在秤稳定后判断增量,避免取走物品时手部冲力造成误触发
data.state == WeightUtil.STATE_STABLE && delta > WEIGHT_DELTA_THRESHOLD -> {
weightSnapshot[snapshotKey] = data.weight
Log.d(TAG, "allScales: 触发高亮 key=$key address=${data.address} delta=$delta")
triggerHighlight(data.deviceId, data.address, delta)
}
data.state == WeightUtil.STATE_STABLE -> {
weightSnapshot[snapshotKey] = data.weight
}
}
}
}
}
} else {
// 子设备:注册本机 WeightUtil 监听,触发后通过 broadcastEvent 通知主设备
WeightUtil.addWeightListener(TAG) { address, state, weight ->
val baseline = weightSnapshot[address]
Log.d(TAG, "onWeight: address=$address state=$state weight=$weight baseline=$baseline")
if (baseline == null) {
weightSnapshot[address] = weight
return@addWeightListener
}
val delta = weight - baseline
Log.d(TAG, "onWeight: address=$address state=$state weight=$weight baseline=$baseline delta=$delta")
when {
// 仅在秤稳定后判断增量,避免取走物品时手部冲力造成误触发
state == WeightUtil.STATE_STABLE && delta > WEIGHT_DELTA_THRESHOLD -> {
weightSnapshot[address] = weight
Log.d(TAG, "onWeight: 触发高亮 address=$address delta=$delta")
triggerHighlight(GlobalData.deviceId, address, delta)
}
state == WeightUtil.STATE_STABLE -> {
weightSnapshot[address] = weight
}
}
}
}
}
/**
* 根据来源设备ID和秤地址触发对应格子高亮,15 秒后自动恢复
* 通过 sourceDeviceId 精确匹配 SCALE_ORDER,解决 18格/22格 地址重叠问题
* @param sourceDeviceId 产生重量变化的设备 ID
* @param address 秤硬件地址
* @param delta 重量变化量(克)
*/
private fun triggerHighlight(sourceDeviceId: String, address: Int, delta: Double) {
// 根据来源设备决定查哪个列表,而非查本机设备ID
val pos22 = if (sourceDeviceId != ScaleDeviceConfig.DEVICE_ID_18)
ScaleDeviceConfig.SCALE_ORDER_22.indexOf(address) else -1
val pos18 = if (sourceDeviceId != ScaleDeviceConfig.DEVICE_ID_22)
ScaleDeviceConfig.SCALE_ORDER_18.indexOf(address) else -1
Log.d(TAG, "triggerHighlight: sourceDeviceId=$sourceDeviceId address=$address pos22=$pos22 pos18=$pos18")
// 两个列表都不包含该地址,忽略
if (pos22 < 0 && pos18 < 0) {
Log.d(TAG, "triggerHighlight: address=$address 不在任何 SCALE_ORDER 中,忽略")
return
}
mainHandler.post {
clearHighlightRunnable?.let { mainHandler.removeCallbacks(it) }
when {
pos22 >= 0 -> {
adapter22.highlightPosition(pos22)
adapter18.clearHighlight()
}
else -> {
adapter18.highlightPosition(pos18)
adapter22.clearHighlight()
}
}
clearHighlightRunnable = Runnable {
adapter22.clearHighlight()
adapter18.clearHighlight()
}.also { mainHandler.postDelayed(it, HIGHLIGHT_DURATION_MS) }
// 子设备:通知主设备(携带本机 deviceId 和地址)
if (GlobalData.deviceRole == DeviceRole.SLAVE) {
ScaleServiceManager.broadcastEvent(
ScaleEvent(
type = ScaleEvent.TYPE_SEASONING_ADDED,
deviceId = GlobalData.deviceId,
address = address,
delta = delta
)
)
}
}
}
override fun onDestroyView() {
super.onDestroyView()
// 子设备才注册了 WeightUtil 监听,移除时安全无副作用
WeightUtil.removeWeightListener(TAG)
ScaleServiceManager.onScaleEvent = null
clearHighlightRunnable?.let { mainHandler.removeCallbacks(it) }
}
}
@@ -31,4 +31,10 @@ object SpTool {
fun getBoolean(key: String, defValue: Boolean = false): Boolean { fun getBoolean(key: String, defValue: Boolean = false): Boolean {
return pref.getBoolean(key, defValue) return pref.getBoolean(key, defValue)
} }
var cookMode: Int
get() = getInt(LAUNCH_PAGE_TYPE, -1)
set(value) {
put(LAUNCH_PAGE_TYPE, value)
}
} }
+11 -2
View File
@@ -23,11 +23,20 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="70dp" android:layout_height="70dp"
android:layout_weight="1" android:layout_weight="1"
android:text="设备配置" android:text="设备切换"
android:textSize="28sp" android:textSize="28sp"
android:textStyle="bold" android:textStyle="bold"
android:background="@android:color/transparent" /> android:background="@android:color/transparent" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnSeasoningConfig"
android:layout_width="0dp"
android:layout_height="70dp"
android:layout_weight="1"
android:text="调料配置"
android:textSize="26sp"
android:background="@android:color/transparent" />
<androidx.appcompat.widget.AppCompatButton <androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnFoodCollect" android:id="@+id/btnFoodCollect"
android:layout_width="0dp" android:layout_width="0dp"
@@ -53,7 +62,7 @@
android:layout_weight="1" android:layout_weight="1"
android:text="秤数据监控" android:text="秤数据监控"
android:textSize="26sp" android:textSize="26sp"
android:visibility="gone" android:visibility="visible"
android:background="@android:color/transparent" /> android:background="@android:color/transparent" />
</LinearLayout> </LinearLayout>
@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
tools:background="@color/white"
android:orientation="vertical">
<View
android:id="@+id/viewLine"
android:layout_width="90dp"
android:layout_height="10dp"
android:layout_marginTop="30dp"
android:background="@drawable/shape_gray_dc_5" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginTop="30dp"
android:layout_marginStart="60dp"
android:layout_marginEnd="60dp"
android:background="@drawable/shape_white_e6_12_corners"
android:gravity="center_vertical">
<EditText
android:id="@+id/etSheetInput"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start|center_vertical"
android:background="@null"
android:ellipsize="end"
android:gravity="center_vertical"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"
android:paddingStart="30dp"
android:paddingEnd="30dp"
android:textColor="@color/black666"
android:textColorHint="@color/gray_c8"
android:textSize="36sp"
tools:hint="@string/input_seasoning_name"
tools:ignore="Autofill,LabelFor,TextFields" />
<ImageView
android:id="@+id/ivSearch"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="20dp"
android:padding="10dp"
android:src="@drawable/ic_search_gray"
tools:ignore="ContentDescription" />
</FrameLayout>
<com.scwang.smart.refresh.layout.SmartRefreshLayout
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
app:srlEnableOverScrollDrag="false">
<com.scwang.smart.refresh.header.ClassicsHeader
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="45dp"
android:layout_marginEnd="45dp"
android:layout_marginTop="15dp"
android:layout_marginBottom="15dp"
android:minHeight="380dp"
android:nestedScrollingEnabled="true"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:itemCount="10"
tools:listitem="@layout/list_item_search_goods_info" />
<com.scwang.smart.refresh.footer.ClassicsFooter
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
</LinearLayout>
@@ -10,8 +10,7 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="600dp"
android:layout_weight="1"
android:layout_marginStart="30dp" android:layout_marginStart="30dp"
android:layout_marginEnd="30dp" android:layout_marginEnd="30dp"
android:background="@drawable/shape_white_30_corners" android:background="@drawable/shape_white_30_corners"
@@ -100,7 +99,8 @@
android:orientation="vertical" android:orientation="vertical"
android:gravity="center_vertical" android:gravity="center_vertical"
android:layout_marginStart="30dp" android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"> android:layout_marginEnd="30dp"
android:visibility="gone">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/bg_color">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="上层"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/home_title"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="6dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvTop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="never" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="下层"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/home_title"
android:layout_marginStart="10dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="6dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvBottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="never" />
</LinearLayout>
@@ -3,12 +3,14 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="120dp"
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingEnd="16dp" android:paddingEnd="16dp"
android:paddingTop="8dp" android:paddingTop="8dp"
android:paddingBottom="8dp"> android:paddingBottom="8dp"
android:background="@drawable/shape_scale_cell">
<!-- 秤编号 --> <!-- 秤编号 -->
<TextView <TextView
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 调料配置格子:仅显示调料名称 -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tvGoodsName"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:includeFontPadding="false"
android:padding="4dp"
android:background="@drawable/shape_scale_cell"
android:textColor="@color/home_title" />
+1
View File
@@ -29,5 +29,6 @@
<color name="black333">#333333</color> <color name="black333">#333333</color>
<color name="gray_c8">#C8C8C8</color> <color name="gray_c8">#C8C8C8</color>
<color name="gray_e6">#E6E6E6</color> <color name="gray_e6">#E6E6E6</color>
<color name="red_ff4444">#FF4444</color>
</resources> </resources>