Author SHA1 Message Date
lvmengandClaude Sonnet 4.6 e6cfc4dff2 重构:拆分 HomeActivity 职责,分层解耦串口与业务逻辑
- 新建 SerialEventListener:定义串口协议解析回调接口契约
- 新建 SerialProtocolParser:迁移全部协议解析逻辑(拼包、12种命令分发、重量解析),无 Android 依赖,可单元测试
- 新建 SerialViewModel:管理串口生命周期(open/close)、Channel 数据传递,暴露 weightFlow / tempCtrlEventFlow / deviceStatusRefreshFlow
- 扩展 HomeViewModel:迁入定时任务(保存货架数据、刷新过期状态),改用 viewModelScope 管理,新增 deviceName 属性与 overdueUpdateFlow
- 精简 HomeActivity:从 835 行削减至 255 行,仅保留 UI 协调职责,所有串口与业务逻辑由 ViewModel 层处理

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 13:41:01 +08:00
lvmengandClaude Sonnet 4.6 85903eb646 重构 HomeActivity:修复多项稳定性与架构问题
架构优化:
- 新增 HomeViewModel,将 companion object 中的共享状态(shelfItems、
  weightMap、selectedShelfIndex、temperatureC、humidity)迁移至 ViewModel,
  消除 Activity 重建时的状态残留与全局状态污染问题

协程修复:
- repeatOnLifecycle(CREATED) 改为 STARTED,确保 Flow 收集与 UI 可见性
  生命周期对齐,进入后台时自动停止订阅
- 串口数据接收改用 Channel<String> + 单一消费协程,替代多 launch 并发模式,
  彻底消除 StringBuilder(serialReader)的并发竞争问题

稳定性修复:
- 修复 tipDialog 从未赋值导致 Dialog 功能失效及 onDestroy 窗口泄漏问题
- 修复 weightHex.toInt() 十进制解析错误,改为 toIntOrNull(16) 并添加兜底
- 修复 updateOverdueState 按索引访问接口数据导致的越界风险,
  改为按 deviceNo 精确匹配,同时修正交叉排列导致的状态串位问题
- 废弃 API getSerializableExtra 替换为版本分支兼容写法(minSdk 25+)

依赖升级:
- androidx.core-ktx 1.10.1 → 1.12.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 11:45:44 +08:00
39 changed files with 2082 additions and 1145 deletions
+4 -4
View File
@@ -67,10 +67,10 @@ dependencies {
implementation("io.github.jeadyx:kmp-serialport:1.0.0")
// // 基础依赖包,必须要依赖
// implementation("com.geyifeng.immersionbar:immersionbar:3.2.2")
// // kotlin扩展(可选)
// implementation("com.geyifeng.immersionbar:immersionbar-ktx:3.2.2")
// 基础依赖包,必须要依赖
implementation("com.geyifeng.immersionbar:immersionbar:3.2.2")
// kotlin扩展(可选)
implementation("com.geyifeng.immersionbar:immersionbar-ktx:3.2.2")
implementation("com.squareup.okhttp3:okhttp:4.9.1")
implementation("com.squareup.okhttp3:logging-interceptor:4.9.1")
@@ -3,11 +3,11 @@ package com.shuwei.intelligent.shelves.activity
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.util.SparseIntArray
import androidx.activity.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
@@ -26,22 +26,17 @@ import com.shuwei.intelligent.shelves.model.ShelfBody
import com.shuwei.intelligent.shelves.model.ShelfModel
import com.shuwei.intelligent.shelves.model.ShelfResult
import com.shuwei.intelligent.shelves.model.WeightEvent
import com.shuwei.intelligent.shelves.net.HomeViewModel
import com.shuwei.intelligent.shelves.net.Loading
import com.shuwei.intelligent.shelves.net.NetViewModel
import com.shuwei.intelligent.shelves.net.RespData
import com.shuwei.intelligent.shelves.net.SerialViewModel
import com.shuwei.intelligent.shelves.net.UiState
import com.shuwei.intelligent.shelves.net.apiService
import com.shuwei.intelligent.shelves.serial.SerialPortManager
import com.shuwei.intelligent.shelves.utils.HexUtils
import com.shuwei.intelligent.shelves.utils.IntervalExecutor
import com.shuwei.intelligent.shelves.utils.binaryToHex
import com.shuwei.intelligent.shelves.utils.ext.copyTextToClipboard
import com.shuwei.intelligent.shelves.utils.ext.gone
import com.shuwei.intelligent.shelves.utils.ext.toJsonString
import com.shuwei.intelligent.shelves.utils.ext.toast
import com.shuwei.intelligent.shelves.utils.ext.visible
import com.shuwei.intelligent.shelves.utils.hexToBinary
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
@@ -54,67 +49,47 @@ class HomeActivity : BaseActivity() {
companion object {
private const val TAG = "HomeActivity"
private const val LOG_PREFIX = "receiveSerialPortData:收到"
private const val MM_DD_EEEE__HH_MM_SS = "MM月dd日 EEEE HH:mm:ss"
//协议头
// 协议头
const val HEADER = "D6DADEDB"
//协议尾
// 协议尾
const val FOOTER = "FFFF"
// const val ACTIVE_CMD =
// "${HEADER}000281C5D70A7D428A4FA67B50F7DACA241938938B4B9CBD73673E9C8E7F2E15062D${FOOTER}"
const val LEFT_SHELF_OPEN_CMD =
"${HEADER}020101000000000000000000000000000000000000${FOOTER}"
const val RIGHT_SHELF_OPEN_CMD =
"${HEADER}020100010000000000000000000000000000000000${FOOTER}"
//const val TEMPERATURE_CMD = "${HEADER}1001${FOOTER}"
const val DEVICE_INFO_CMD = "${HEADER}0301000000000000000000000000${FOOTER}"
const val START_TEMP_CTRL_CMD = "${HEADER}0F01360000${FOOTER}"
//const val START_CMD= "${HEADER}0F01050020${FOOTER}"
// const val ACTIVE_CMD = "${HEADER}000281C5D70A7D428948AD7254FADBB1516738938B4B9CBD73673E9D8E7F2E15062D$FOOTER"
// 410激活码
// const val ACTIVE_CMD = "${HEADER}000281C5D70A7D42894EA17650F5D0BB516838E7863C9ABD766C38958AFF0F3C062D${FOOTER}"
//1楼餐厅激活码
// 1楼餐厅激活码
const val ACTIVE_CMD =
"${HEADER}000281C5D70A7D428B44A57454F5DACA241938938B4B9CBD73673E9C8E7FC0C4992D${FOOTER}"
//华氏温度单位
// 华氏温度单位
const val F_TEMP_CMD = "${HEADER}0F01390000${FOOTER}"
//摄氏温度单位
// 摄氏温度单位
const val C_TEMP_CMD = "${HEADER}0F01390001${FOOTER}"
//开灯(开 继电器1
// 开灯(开继电器1
const val LIGHT_CLOSE = "${HEADER}020102020102020000000000000000000000000000${FOOTER}"
const val LIGHT_OPEN = "${HEADER}020102020002020000000000000000000000000000${FOOTER}"
const val TEMPERATURE_CTRL = "${HEADER}0F01040000${FOOTER}"
val list: MutableList<ShelfModel> = mutableListOf()
val weightArray = SparseIntArray()
var shelfIndex = 0
// public fun getRealWeight():Double = weightArray[shelfIndex]?:0.0
// 温度
var showTemperatureC = "0"
// 湿度
var showHumidity = "0"
}
//401-false1楼餐厅-true
// 401-false1楼餐厅-true
private var isNeedOpenLock = true
private lateinit var binding: ActivityHomeBinding
private val viewModel: NetViewModel by viewModels()
private val homeViewModel: HomeViewModel by viewModels()
private val serialViewModel: SerialViewModel by viewModels()
@SuppressLint("NotifyDataSetChanged", "HardwareIds")
override fun onCreate(savedInstanceState: Bundle?) {
@@ -124,27 +99,37 @@ class HomeActivity : BaseActivity() {
setBackground()
setContentView(binding.root)
// updateLeftStatus("-°C / -%")
updateLeftStatus("")
initRecyclerView()
// 打开串口,串口生命周期由 SerialViewModel 管理
serialViewModel.openSerial()
// 统一注册所有 ViewModel Flow 观察
observeViewModels()
// 延迟发送温控初始化命令
binding.root.postDelayed({
serialViewModel.sendCmd(START_TEMP_CTRL_CMD)
serialViewModel.sendCmd(C_TEMP_CMD)
}, 5000)
// 启动定时任务(保存货架数据 + 刷新过期状态),生命周期由 viewModelScope 管理
homeViewModel.startPeriodicTasks()
// 拉取货架列表
viewModel.getShelfList(deviceId = App.deviceId)
}
/**
* 统一注册所有 ViewModel Flow 观察。
* 在 STARTED 状态下收集,onStop 时自动暂停,onStart 时恢复。
*/
private fun observeViewModels() {
lifecycleScope.launch {
val openState = SerialPortManager.open()
log("onCreate: openState=$openState")
if (openState) {
SerialPortManager.startReceive { data ->
// 更新UI显示接收数据
lifecycleScope.launch {
try {
receiveSerialPortData(data)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
//SerialPortManager.send(DEVICE_INFO_CMD)
}
repeatOnLifecycle(Lifecycle.State.CREATED) {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// 获取 Token 结果
launch {
viewModel.getAccessTokenUiState.collect { state ->
when (state) {
@@ -155,6 +140,8 @@ class HomeActivity : BaseActivity() {
}
}
}
// 货架列表加载结果
launch {
viewModel.getShelfListUiState.collect { state ->
when (state) {
@@ -165,61 +152,52 @@ class HomeActivity : BaseActivity() {
}
}
}
// 串口重量更新:更新 ViewModel 缓存 + 局部刷新 Adapter
launch {
serialViewModel.weightFlow.collect { (deviceNo, weight) ->
homeViewModel.putWeight(deviceNo, weight)
if (deviceNo in 1..10 && homeViewModel.shelfItems.size == 10) {
homeViewModel.shelfItems.firstOrNull { it.deviceNo == deviceNo }
?.let { model ->
model.weight = weight.toDouble()
val pos = homeViewModel.shelfItems.indexOf(model)
shelfAdapter.notifyItemChanged(pos)
log("weightFlow: deviceNo=${model.deviceNo}, weight=$weight")
EventBus.getDefault().post(SendWeightEvent(model.deviceNo, weight))
}
}
}
}
// 串口温控上报:弹窗展示日志
launch {
serialViewModel.tempCtrlEventFlow.collect { logMsg ->
showTipDialog(logMsg)
}
}
// 串口触发顶部状态栏刷新
launch {
serialViewModel.deviceStatusRefreshFlow.collect {
updateLeftStatus(homeViewModel.deviceName)
}
}
// 过期状态变化:局部刷新对应 position
launch {
homeViewModel.overdueUpdateFlow.collect { positions ->
positions.forEach { shelfAdapter.notifyItemChanged(it) }
}
}
}
}
//sendCmd(ACTIVE_CMD)
binding.root.postDelayed({
sendCmd(START_TEMP_CTRL_CMD)
sendCmd(C_TEMP_CMD)
}, 5000)
// TaskManager.startTask()
saveGoodsTask()
overdueTask()
// var androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
//// androidId = "d0e7a2d5f25c775c"
// log("onCreate: androidId=${androidId}")
// if (androidId == "65e1012638f78260" || androidId == "40f89e91ee40611d") {
// androidId = "d0e7a2d5f25c775c"
// }
// App.deviceId = androidId
// //"53babf69-0d2f-3875-a649-f2e12f80ad4c1"
//
// val swDpValue = resources?.configuration?.smallestScreenWidthDp
// log("onCreate: swDpValue=$swDpValue")
// viewModel.getAccessToken(App.deviceId)
viewModel.getShelfList(deviceId = App.deviceId)
// binding.root.postDelayed({
// loadTestData()
// }, 30*1000)
}
private fun loadTestData() {
list.add(ShelfModel(deviceNo = 1, goodsName = "鸡蛋", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 6, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 2, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 7, goodsName = "生姜", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 3, goodsName = "黄瓜", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 8, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 4, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 9, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 5, goodsName = "", deviceId = App.deviceId))
list.add(ShelfModel(deviceNo = 10, goodsName = "", deviceId = App.deviceId))
shelfAdapter.notifyDataSetChanged()
}
private fun getTokenSuccess(data: RespData<*>) {
log("getTokenSuccess: $data")
data.data?.let {
App.accessToken = it.toString()
//UIUtils.toast(it.toString())
viewModel.getShelfList(deviceId = App.deviceId)
}
}
@@ -236,10 +214,9 @@ class HomeActivity : BaseActivity() {
loadEmptyView()
return
}
deviceName = data.data.deviceName
// updateLeftStatus("$deviceName -°C / -%")
updateLeftStatus("$deviceName")
// 将 deviceName 写入 HomeViewModel,跨配置变更保持
homeViewModel.deviceName = data.data.deviceName
updateLeftStatus(homeViewModel.deviceName)
App.canteenId = data.data.placeId
val tempList = data.data.containerGoodsList
@@ -247,16 +224,12 @@ class HomeActivity : BaseActivity() {
loadEmptyView()
return
}
// if (tempList.size < 10) {
// toast("数据错误")
// return
// }
list.clear()
homeViewModel.shelfItems.clear()
val subList01 = tempList.subList(0, tempList.size / 2)
val subList02 = tempList.subList(tempList.size / 2, tempList.size)
repeat(tempList.size / 2) { index ->
list.add(subList01[index].also { it.deviceId = App.deviceId })
list.add(subList02[index].also { it.deviceId = App.deviceId })
homeViewModel.shelfItems.add(subList01[index].also { it.deviceId = App.deviceId })
homeViewModel.shelfItems.add(subList02[index].also { it.deviceId = App.deviceId })
}
shelfAdapter.notifyDataSetChanged()
}
@@ -268,375 +241,60 @@ class HomeActivity : BaseActivity() {
}
/**
* 终端id
* 显示温控信息弹窗。
* 若弹窗已在展示中则不重复打开,仅更新内容。
*/
private var terminalId: String = ""
/**
* 固件版本
*/
private var terminalVersion: String = ""
private val serialReader by lazy { StringBuilder() }
private fun receiveSerialPortData(srcData: String) {
log("receiveSerialPortData: $srcData")
var data = ""
if (srcData.contains(HEADER) && srcData.contains(FOOTER)) {
serialReader.clear()
val startIndex = srcData.lastIndexOf(HEADER)
var endIndex = srcData.indexOf(FOOTER) + FOOTER.length
if (endIndex < startIndex) {
//类似这种:31314FFFFFD6DADEDB01010CFOOTER在HEADER前
data = srcData.substring(startIndex)
if (data.contains(FOOTER)) {
endIndex = srcData.indexOf(FOOTER) + FOOTER.length
data = data.substring(0, endIndex)
handleReceivedData(data)
serialReader.clear()
return
}
serialReader.clear()
serialReader.append(data).append(",")
return
private fun showTipDialog(logMsg: String) {
if (tipDialog == null) {
tipDialog = CommonDialog(this).apply {
dialogTitle = "温馨提示"
confirmBlock = { logMsg.copyTextToClipboard(this@HomeActivity) }
}
serialReader.append(srcData.substring(startIndex, endIndex))
data = serialReader.toString()
handleReceivedData(data)
serialReader.clear()
return
}
if (srcData.contains(HEADER) && srcData.contains(FOOTER).not()) {
serialReader.clear()
val startIndex = srcData.lastIndexOf(HEADER)
serialReader.append(srcData.substring(startIndex)).append(",")
return
}
if (srcData.contains(HEADER).not() && srcData.contains(FOOTER).not()) {
serialReader.append(srcData).append(",")
return
}
//if (srcData.contains(HEADER).not() && srcData.contains(FOOTER))
val endIndex = srcData.indexOf(FOOTER) + FOOTER.length
serialReader.append(srcData.substring(0, endIndex))
log("receiveSerialPortData:serialReader: $serialReader")
data = serialReader.toString().replace(",", "")
handleReceivedData(data)
}
private fun handleReceivedData(data: String) {
if (data.isEmpty()) {
return
}
val cmd = data.substring(HEADER.length, HEADER.length + 4)
val cmdFlag = cmd.substring(0, 2)
when (cmd) {
"0001" -> {
//终端发起激活指令,拿到固定数据体(终端ID-15字节、固件版本-5字节、命令次数-1字节)
val reqBody =
data.substring(HEADER.length + cmd.length, data.length - FOOTER.length)
if (reqBody.length == 44) {
terminalId = reqBody.substring(2, 32)
terminalVersion = reqBody.substring(32, 42)
log("${LOG_PREFIX}[激活]${cmdFlag}指令:${data},终端id:${terminalId},固件版本:${terminalVersion},发送激活命令:$ACTIVE_CMD")
sendCmd(ACTIVE_CMD)
}
}
"0101", "8101" -> {
//心跳0102${p1}${p2}${version}
//心跳81(终端发起,当终端发送此命令时需要服务端存储串口屏显示信息)
val respCmd = "${HEADER}01020506${terminalVersion}$FOOTER"
log("${LOG_PREFIX}[心跳]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
sendCmd(respCmd)
heartBeat(data)
}
"0202" -> {
//开关锁反馈020200${deviceId}${version}
val respCmd = "${HEADER}020200${terminalId}${terminalVersion}$FOOTER"
log("${LOG_PREFIX}[开锁]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
sendCmd(respCmd)
//lockFeedback(data)
}
"0302" -> {
//获取设备状态
log("${LOG_PREFIX}[获取设备状态]${cmdFlag}指令:${data}")
getDeviceInfo(data)
}
"0501" -> {
//卡号上发
val respCmd = "${HEADER}050203$FOOTER"
log("${LOG_PREFIX}[卡号上发]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
sendCmd(respCmd)
}
"0B01" -> {
//操作记录上传
val respCmd = "${HEADER}0B0203$FOOTER"
log("${LOG_PREFIX}[操作记录上传]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
sendCmd(respCmd)
}
"0C01" -> {
//对时
val timeLong = System.currentTimeMillis() / 1000 + (8 * 3600)
val timeStr = "0000000" + String.format("%X", timeLong)
val time =
timeStr.substring(timeStr.length - 8).chunked(2).reversed().joinToString("")
val respCmd = "${HEADER}0B02${time}$FOOTER"
log("${LOG_PREFIX}[对时]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
sendCmd(respCmd)
}
"0E01" -> {
//终端位置上报
val respCmd = "${HEADER}0E0200$FOOTER"
log("${LOG_PREFIX}[终端位置上报]${cmdFlag}指令:${data},发送应答指令:${respCmd}")
sendCmd(respCmd)
}
"1001" -> {
//温控信息上报
val respCmd = "${HEADER}100200$FOOTER"
val logMsg =
"${LOG_PREFIX}[温控信息上报]${cmdFlag}指令:${data},发送应答指令:${respCmd}"
log(logMsg)
sendCmd(respCmd)
//val tempC = HexUtils.signedHexToDec(data.take(4)) * 0.1
// updateLeftStatus("$deviceName ${"%.1f".format(tempC)}°C / -%")
// updateLeftStatus("$deviceName -°C / -%")
updateLeftStatus("$deviceName")
//D6DADEDB1001015D020000000027001E00020000001E001C00B40000000000010000000100040000001900440003000F00000000000FFFE6000200010002FFD80044000300010005001E0001FFD80056001400280000000000000000008300090000003C00D2000000000000007F003700000000A100000031323136334238423134303233303456322E313100FFFF
runOnUiThread {
if (tipDialog == null) {
CommonDialog(this).apply {
dialogTitle = "温馨提示"
// dialogContent = logMsg
confirmBlock = {
logMsg?.copyTextToClipboard(this@HomeActivity)
}
}
}
if (tipDialog?.isShowing == false) {
tipDialog?.dialogContent = logMsg
tipDialog?.show()
}
}
}
"1002" -> {
log("${LOG_PREFIX}[温控信息应答]1002数据:${data}")
}
"0F02" -> {
log("${LOG_PREFIX}[温控信息应答]0F02数据:${data}")
//D6DADEDB0F020331323136334238423134303233303456322E31313EFFFF00000055010000000000002707000001000000220100000000000091040000120100000100000095140080611400800000000000000000000000000000000000000000010100005031323136334238423134303233303456322E31313EFFFF
runOnUiThread { }
}
else -> {}
if (tipDialog?.isShowing == false) {
tipDialog?.dialogContent = logMsg
tipDialog?.show()
}
}
private var tipDialog: CommonDialog? = null
private var fixedDataBody = ""
// private fun lockFeedback(data: String) {
// runCatching {
// val fixedDataBodyEndIndex = data.length - FOOTER.length
// val fixedDataBodyStartIndex = HEADER.length + "0202".length + 42
// fixedDataBody = data.substring(fixedDataBodyStartIndex, fixedDataBodyEndIndex)
//
// val lockNo = if (list[shelfIndex].deviceNo in 1..5) "01" else "02"
// val lockOneState = data.substring(HEADER.length + 4, HEADER.length + 6)
// if (lockOneState == "00") {
// //锁1关
// SerialPortManager.send("${HEADER}0202${lockNo}${FOOTER}")
// }
// val lockTwoState = data.substring(HEADER.length + 6, HEADER.length + 8)
// if (lockTwoState == "00") {
// //锁2关
// SerialPortManager.send("${HEADER}0202${lockNo}${FOOTER}")
// }
// }.onFailure {
// it.printStackTrace()
// }
// }
private fun heartBeat(data: String) {
runCatching {
if (data.length < 200) {
return
}
val weightStartIndex = 14
val weightEndIndex = weightStartIndex + 12 * 8
getWeightInfo(
start = weightStartIndex,
end = weightEndIndex,
data = data
)
val temperatureStartIndex = 158
val temperatureEndIndex = temperatureStartIndex + 2
// val temperatureHex = data.substring(temperatureStartIndex, temperatureEndIndex)
// val temperature = hexToDec(temperatureHex)
// val tempC = f2C(temperature.toDouble())
// showTemperatureC = "%.1f".format(tempC)
//val percent = 10 * list.count { it.goodsName.isNullOrBlank().not() }
// updateLeftStatus("${deviceName} [$temperatureHex] ${showTemperatureC}°C / -%")
getFixDataBody(data, temperatureEndIndex)
}.onFailure {
it.printStackTrace()
}
}
private var deviceName = ""
private fun getWeightInfo(start: Int, end: Int, data: String) {
var index = 1
var count = start
while (count < end) {
val weightInfo = data.substring(count, count + 8)
var weightHex = ""
weightInfo.run {
weightHex =
substring(6, 8) + substring(4, 6) + substring(2, 4) + substring(0, 2)
}
var realWeight = 0
var firstByteBinary = hexToBinary(weightHex.substring(0, 2))
if (firstByteBinary.startsWith("0")) {
//正数
realWeight = weightHex.toInt()
} else {
//负数
firstByteBinary = "0${firstByteBinary.substring(1)}"
val firstByteHex = binaryToHex(firstByteBinary)
weightHex = firstByteHex + weightHex.substring(2)
realWeight = -1 * weightHex.toInt()
}
weightArray.put(index, realWeight)
if (index in 1..10 && list.size == 10) {
list.firstOrNull { it.deviceNo == index }?.let {
it.weight = realWeight.toDouble()
val pos = list.indexOf(it)
shelfAdapter.notifyItemChanged(pos)
log("getWeightInfo: deviceNo=${it.deviceNo},realWeight=${realWeight}")
EventBus.getDefault().post(SendWeightEvent(it.deviceNo, realWeight))
}
// val pos = index - 1
// val item = list[pos]
// item.weight = realWeight.toDouble()
// shelfAdapter.notifyItemChanged(pos)
// log("getWeightInfo: deviceNo=${item.deviceNo},realWeight=${realWeight}")
// EventBus.getDefault().post(SendWeightEvent(item.deviceNo, realWeight))
}
count += 8
index++
}
// ShelfActivity.updateWeight(getRealWeight())
// if (list.isNotEmpty()) {
// val intent = Intent(ShelfActivity.RECEIVER_DEVICE_INFO + list[shelfIndex].deviceNo)
// intent.putExtra(ShelfActivity.SHELF_WEIGHT, weightArray[shelfIndex])
// LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
// }
}
private fun getDeviceInfo(data: String) {
log("getDeviceInfo: data=$data")
val weightStartIndex = HEADER.length + 14
val temperatureStartIndex = weightStartIndex + 12 * 8
val temperatureEndIndex = temperatureStartIndex + 2
val temperatureHex = try {
data.substring(temperatureStartIndex, temperatureEndIndex)
} catch (e: Exception) {
e.printStackTrace()
return
}
// val temperature = hexToDec(temperatureHex)
// val tempC = f2C(temperature.toDouble())
// showTemperatureC = "%.1f".format(tempC)
//val percent = 10 * list.count { it.goodsName.isNullOrBlank().not() }
// updateLeftStatus("${deviceName} ${showTemperatureC}°C / -%")
// val tempC = HexUtils.signedHexToDec(temperatureHex) * 0.1
// updateLeftStatus("$deviceName -°C / -%")
updateLeftStatus("$deviceName")
getWeightInfo(
start = weightStartIndex,
end = temperatureStartIndex,
data = data
)
getFixDataBody(data, temperatureEndIndex)
}
fun getFixDataBody(data: String, temperatureEndIndex: Int) {
try {
if (terminalId.isNotBlank() && terminalVersion.isNotBlank()) {
return
}
val fixDataBodyStartIndex = temperatureEndIndex + 1
val fixDataBody = data.substring(fixDataBodyStartIndex, data.length - FOOTER.length)
if (fixDataBody.length == 42) {
terminalId = fixDataBody.substring(0, 30)
terminalVersion = fixDataBody.substring(30, 40)
log("receiveSerialPortData:从设备状态获取数据,终端id:${terminalId},固件版本:${terminalVersion}")
}
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onDestroy() {
lifecycleScope.launch {
SerialPortManager.close()
}
// TaskManager.cancelTask()
saveTaskJob?.cancel()
overdueTaskJob?.cancel()
// 弹窗需在 Activity 销毁前手动关闭,防止窗口泄漏
tipDialog?.dismiss()
tipDialog = null
EventBus.getDefault().unregister(this)
// 串口关闭、Channel 关闭、定时任务取消均由各 ViewModel.onCleared() 自动管理
super.onDestroy()
}
@Suppress("DEPRECATION")
private val shelfAdapter by lazy {
ShelfAdapter(list).apply {
ShelfAdapter(homeViewModel.shelfItems).apply {
setOnItemClickListener { _, _, position ->
shelfIndex = position
homeViewModel.selectedShelfIndex = position
lifecycleScope.launch {
// 根据货架编号决定开启左侧或右侧柜锁
if (isNeedOpenLock) {
val openCmd = if (list[position].deviceNo in 1..5)
val openCmd = if (homeViewModel.shelfItems[position].deviceNo in 1..5)
LEFT_SHELF_OPEN_CMD
else
RIGHT_SHELF_OPEN_CMD
val openState = SerialPortManager.send(openCmd)
serialViewModel.sendCmd(openCmd)
}
// if (openState.not()) {
// toast("${list[position].goodsName}柜门开启失败")
// return@launch
// }
//SerialPortManager.send(START_CMD)
launch(Intent(this@HomeActivity, ShelfActivity::class.java).also {
it.putExtra(ShelfActivity.SHELF_MODEL, list[position])
it.putExtra(ShelfActivity.SHELF_MODEL, homeViewModel.shelfItems[position])
}) {
it?.run {
val model =
getSerializableExtra(ShelfActivity.SHELF_MODEL) as ShelfModel
list[position].apply {
// API 33 起支持带类型参数的重载,低版本回退旧 API + as? 安全转换
val model = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getSerializableExtra(ShelfActivity.SHELF_MODEL, ShelfModel::class.java)
} else {
@Suppress("DEPRECATION")
getSerializableExtra(ShelfActivity.SHELF_MODEL) as? ShelfModel
} ?: return@run
// 将 ShelfActivity 返回的数据同步回列表
homeViewModel.shelfItems[position].apply {
goodsId = model.goodsId
goodsName = model.goodsName
weight = model.weight
@@ -646,21 +304,18 @@ class HomeActivity : BaseActivity() {
}
notifyItemChanged(position)
// 货架操作后立即将最新数据同步到服务端
try {
val submitList = list.sortedBy { item -> item.deviceNo }
val submitList = homeViewModel.shelfItems.sortedBy { item -> item.deviceNo }
val body = ShelfBody().also { body ->
body.deviceId = App.deviceId ?: ""
body.canteenId = App.canteenId
body.temperature = showTemperatureC
body.humidity = showHumidity
body.temperature = homeViewModel.temperatureC
body.humidity = homeViewModel.humidity
body.goodsList = submitList
}
Log.d("mzf", "performSync: body:${body.toJsonString()}")
lifecycleScope.launch {
val resp = apiService.saveShelfGoodsList(body = body)
Log.d("mzf", "performSync: resp:${resp.toJsonString()}")
}
Log.d(TAG, "shelfItemClick sync: body=${body.toJsonString()}")
viewModel.saveShelfGoodsList(body)
} catch (e: Exception) {
e.printStackTrace()
}
@@ -676,7 +331,6 @@ class HomeActivity : BaseActivity() {
layoutManager =
GridLayoutManager(this@HomeActivity, 2, GridLayoutManager.VERTICAL, false)
adapter = shelfAdapter
itemAnimator.let {
if (it is DefaultItemAnimator) {
it.supportsChangeAnimations = false
@@ -703,8 +357,6 @@ class HomeActivity : BaseActivity() {
super.onResume()
hideStatusBar()
handler.post(updateTask)
// D6DADEDB00010031323136334238423134303233303456322E313100FFFF
// sendCmd("${HEADER}00010031323136334238423134303233303456322E313152${FOOTER}")
}
override fun onPause() {
@@ -714,13 +366,11 @@ class HomeActivity : BaseActivity() {
@SuppressLint("NotifyDataSetChanged")
private fun loadEmptyView() {
list.clear()
homeViewModel.shelfItems.clear()
shelfAdapter.notifyDataSetChanged()
binding.include?.let {
it.root.visible()
it.root.setOnClickListener {
//viewModel.getAccessToken(App.deviceId)
viewModel.getShelfList(deviceId = App.deviceId)
}
it.ivEmptyIcon.setImageResource(R.drawable.ic_empty_white)
@@ -729,13 +379,12 @@ class HomeActivity : BaseActivity() {
}
@Subscribe(threadMode = ThreadMode.MAIN)
public fun weightEvent(event: WeightEvent) {
fun weightEvent(event: WeightEvent) {
}
@Subscribe(threadMode = ThreadMode.MAIN)
public fun clearEmptyShelf(event: ClearShelfEvent) {
val item = list.firstOrNull { it.deviceNo == event.shelfNo }
fun clearEmptyShelf(event: ClearShelfEvent) {
val item = homeViewModel.shelfItems.firstOrNull { it.deviceNo == event.shelfNo }
item?.apply {
goodsId = ""
goodsName = null
@@ -744,68 +393,6 @@ class HomeActivity : BaseActivity() {
createTime = null
overdueDay = null
}
shelfAdapter.notifyItemChanged(list.indexOf(item))
shelfAdapter.notifyItemChanged(homeViewModel.shelfItems.indexOf(item))
}
private val taskExecutor by lazy { IntervalExecutor() }
private var saveTaskJob: Job?=null
/**
* 保存定时任务
*/
private fun saveGoodsTask() {
saveTaskJob = taskExecutor.startIntervalTaskWithInitialDelay(1 * 60 * 1000L, 5 * 60 * 1000L) {
saveShelfGoodsList()
}
}
private var overdueTaskJob: Job?=null
/**
* 过期时间定时任务
*/
private fun overdueTask() {
overdueTaskJob = taskExecutor.startIntervalTaskWithInitialDelay(5 * 1000L, 5 * 60* 1000L) {
updateOverdueState()
}
}
private fun updateOverdueState() {
viewModel.updateOverdueState { state, items ->
if (items.isNullOrEmpty()) return@updateOverdueState
list.forEachIndexed { index, model ->
//接口过期状态变化后更新状态
if (items[index].overdue != model.overdue) {
model.overdue = items[index].overdue
shelfAdapter.notifyItemChanged(index)
log("更新编号为${model.deviceNo}物品:${model.goodsName}的过期状态:${model.overdue}")
}
}
}
}
private fun saveShelfGoodsList() {
log("performSync: list:${list.toJsonString()}")
val submitList = list.sortedBy { it.deviceNo }
submitList.forEach {
if (it.goodsId.isNullOrBlank()) {
it.weight = it.weightBak
}
}
// submitList.forEach {
// it.temperature = HomeActivity.showTemperatureC
// it.humidity = HomeActivity.showHumidity
// }
val body = ShelfBody().also {
it.deviceId = App.deviceId
it.canteenId = App.canteenId
it.temperature = showTemperatureC
it.humidity = showHumidity
it.goodsList = submitList
}
log("performSync: body:${body.toJsonString()}")
viewModel.saveShelfGoodsList(body)
}
}
}
@@ -1,17 +1,9 @@
package com.shuwei.intelligent.shelves.activity
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.util.Log
import android.view.KeyEvent
import android.view.animation.RotateAnimation
import androidx.activity.viewModels
import androidx.core.graphics.toColorInt
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
@@ -23,7 +15,6 @@ import com.shuwei.intelligent.shelves.net.NetViewModel
import com.shuwei.intelligent.shelves.net.UiState
import com.shuwei.intelligent.shelves.net.UrlConfig
import com.shuwei.intelligent.shelves.utils.AppUtil
import com.shuwei.intelligent.shelves.utils.NetworkUtils
import com.shuwei.intelligent.shelves.utils.QRCodeUtil
import com.shuwei.intelligent.shelves.utils.SpTool
import com.shuwei.intelligent.shelves.utils.ext.dp
@@ -48,9 +39,6 @@ class InitActivity : BaseActivity() {
private lateinit var binding: ActivityInitBinding
private val viewModel: NetViewModel by viewModels()
private val handler = Handler(Looper.getMainLooper())
private var startTime = 0L // 倒计时开始时间
private var lastNetworkCheckTime = 0L // 上次检测网络的时间
@SuppressLint("HardwareIds")
override fun onCreate(savedInstanceState: Bundle?) {
@@ -85,8 +73,7 @@ class InitActivity : BaseActivity() {
binding.ivQrCode.invisible()
binding.btnInit.invisible()
// countDown()
initViews()
countDown()
}
private fun countDown() {
@@ -174,142 +161,4 @@ class InitActivity : BaseActivity() {
return true
}
/**
* 初始化控件事件
*/
private fun initViews() {
// 连接网络按钮点击事件
binding.btnConnectNetwork.setOnClickListener {
openNetworkSettings()
}
// 启动加载动画 - 持续旋转
startLoadingAnimation()
}
/**
* 启动加载动画 - ImageView 持续旋转
*/
private fun startLoadingAnimation() {
val rotateAnimation = RotateAnimation(
0f, 360f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f
).apply {
duration = 1200 // 旋转周期 1.2 秒
repeatCount = RotateAnimation.INFINITE // 无限循环
repeatMode = RotateAnimation.RESTART
}
binding.ivLoading.startAnimation(rotateAnimation)
}
override fun onResume() {
super.onResume()
if (NetworkUtils.isNetworkConnected(this)) {
navigateToHome()
return
}
startCountdown()
}
override fun onPause() {
super.onPause()
// 页面不可见时停止倒计时
handler.removeCallbacksAndMessages(null)
}
/**
* 启动 60 秒倒计时
*/
private fun startCountdown() {
startTime = SystemClock.elapsedRealtime()
lastNetworkCheckTime = 0
binding.llNetwork.setBackgroundColor(Color.TRANSPARENT)
binding.llLoading.visibility = android.view.View.VISIBLE
binding.llNetworkButton.visibility = android.view.View.GONE
binding.tvCountdown.text = "60秒"
scheduleCountdown()
}
/**
* 定时更新倒计时
* 使用 SystemClock.elapsedRealtime() 确保精确计时
*/
private fun scheduleCountdown() {
handler.postDelayed({
val elapsedTime = SystemClock.elapsedRealtime() - startTime
val remainingTime = 60 - (elapsedTime / 1000).toInt()
val currentNetworkCheckTime = (elapsedTime / 1000).toInt()
// 每 10 秒检测一次网络
if (currentNetworkCheckTime > 0 && currentNetworkCheckTime % 10 == 0 && currentNetworkCheckTime != lastNetworkCheckTime.toInt()) {
lastNetworkCheckTime = currentNetworkCheckTime.toLong()
checkNetworkConnection()
}
// 倒计时未结束,继续更新 UI
if (remainingTime > 0) {
binding.tvCountdown.text = "${remainingTime}"
scheduleCountdown()
} else {
// 倒计时结束,最后检测一次网络
if (NetworkUtils.isNetworkConnected(this)) {
navigateToHome()
} else {
// 网络未连接,显示"连接网络"按钮
showNetworkButton()
}
}
}, 100) // 每 100ms 检查一次,确保精确性
}
/**
* 检测网络连接
*/
private fun checkNetworkConnection() {
if (NetworkUtils.isNetworkConnected(this)) {
// 网络连接成功,停止倒计时并跳转到 HomeActivity
handler.removeCallbacksAndMessages(null)
navigateToHome()
}
}
/**
* 跳转到 HomeActivity
*/
private fun navigateToHome() {
startActivity<HomeActivity>()
finish()
}
/**
* 显示"连接网络"按钮
*/
private fun showNetworkButton() {
binding.llNetwork.setBackgroundColor("#5C77F7".toColorInt())
binding.llLoading.visibility = android.view.View.GONE
binding.llNetworkButton.visibility = android.view.View.VISIBLE
}
/**
* 打开系统网络设置页面
*/
private fun openNetworkSettings() {
val intent = Intent(android.provider.Settings.ACTION_WIFI_SETTINGS)
startActivity(intent)
}
/**
* 禁用返回键
*/
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
return if (keyCode == KeyEvent.KEYCODE_BACK) {
// 禁用返回键,不执行任何操作
true
} else {
super.onKeyDown(keyCode, event)
}
}
}
@@ -1,66 +1,66 @@
//package com.shuwei.intelligent.shelves.activity
//
//import android.os.Bundle
//import android.os.Handler
//import android.os.Looper
//import com.shuwei.intelligent.shelves.base.BaseActivity
//import com.shuwei.intelligent.shelves.databinding.ActivitySettingBinding
//import com.shuwei.intelligent.shelves.utils.KeyboardUtil
//import java.text.SimpleDateFormat
//import java.util.Date
//import java.util.Locale
//
//class SettingActivity: BaseActivity() {
//
// private lateinit var binding: ActivitySettingBinding
// override fun onCreate(savedInstanceState: Bundle?) {
// super.onCreate(savedInstanceState)
// binding = ActivitySettingBinding.inflate(layoutInflater)
// setBackground()
// setContentView(binding.root)
// binding.btnSendCmd.setOnClickListener{
// val cmd = binding.etInputCmd.text.toString().trim()
// if (cmd.isBlank()) {
// return@setOnClickListener
// }
// var record = binding.tvCmdRecord.text.toString().trim()
// record = cmd+"\n"+record
// binding.tvCmdRecord.text = record
// val realCmd = "${HomeActivity.HEADER}0F01${cmd}${HomeActivity.FOOTER}"
// sendCmd(realCmd)
// }
// binding.ivBack.setOnClickListener { finish() }
// binding.root.setOnClickListener { v ->
// KeyboardUtil.hideKeyboard(v)
// }
// }
//
//
// private fun updateDateTime() {
// val sdf = SimpleDateFormat(ShelfActivity.YYYY_MM_DD__EEEE_HH_MM_SS, Locale.CHINA)
// val dateTime = sdf.format(Date())
// val arr = dateTime.split("***")
// updateLeftStatus(arr[0])
// updateRightStatus(arr[1])
// }
//
// private val handler = Handler(Looper.getMainLooper())
// private val updateTask = object : Runnable {
// override fun run() {
// updateDateTime()
// handler.postDelayed(this, 1000)
// }
// }
//
// override fun onResume() {
// super.onResume()
// hideStatusBar()
// handler.post(updateTask)
// }
//
// override fun onPause() {
// super.onPause()
// handler.removeCallbacks(updateTask)
// }
//
//}
package com.shuwei.intelligent.shelves.activity
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import com.shuwei.intelligent.shelves.base.BaseActivity
import com.shuwei.intelligent.shelves.databinding.ActivitySettingBinding
import com.shuwei.intelligent.shelves.utils.KeyboardUtil
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class SettingActivity: BaseActivity() {
private lateinit var binding: ActivitySettingBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySettingBinding.inflate(layoutInflater)
setBackground()
setContentView(binding.root)
binding.btnSendCmd.setOnClickListener{
val cmd = binding.etInputCmd.text.toString().trim()
if (cmd.isBlank()) {
return@setOnClickListener
}
var record = binding.tvCmdRecord.text.toString().trim()
record = cmd+"\n"+record
binding.tvCmdRecord.text = record
val realCmd = "${HomeActivity.HEADER}0F01${cmd}${HomeActivity.FOOTER}"
sendCmd(realCmd)
}
binding.ivBack.setOnClickListener { finish() }
binding.root.setOnClickListener { v ->
KeyboardUtil.hideKeyboard(v)
}
}
private fun updateDateTime() {
val sdf = SimpleDateFormat(ShelfActivity.YYYY_MM_DD__EEEE_HH_MM_SS, Locale.CHINA)
val dateTime = sdf.format(Date())
val arr = dateTime.split("***")
updateLeftStatus(arr[0])
updateRightStatus(arr[1])
}
private val handler = Handler(Looper.getMainLooper())
private val updateTask = object : Runnable {
override fun run() {
updateDateTime()
handler.postDelayed(this, 1000)
}
}
override fun onResume() {
super.onResume()
hideStatusBar()
handler.post(updateTask)
}
override fun onPause() {
super.onPause()
handler.removeCallbacks(updateTask)
}
}
@@ -11,21 +11,21 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import com.gyf.immersionbar.BarHide
import com.gyf.immersionbar.ImmersionBar
import com.shuwei.intelligent.shelves.activity.HomeActivity
import com.shuwei.intelligent.shelves.activity.HomeActivity.Companion.LIGHT_CLOSE
import com.shuwei.intelligent.shelves.activity.HomeActivity.Companion.LIGHT_OPEN
import com.shuwei.intelligent.shelves.R
import com.shuwei.intelligent.shelves.activity.LogActivity
import com.shuwei.intelligent.shelves.activity.SettingActivity
import com.shuwei.intelligent.shelves.databinding.ActivityBaseBinding
import com.shuwei.intelligent.shelves.serial.SerialPortManager
import com.shuwei.intelligent.shelves.utils.FileLogger
import com.shuwei.intelligent.shelves.utils.ext.clickWithDebounce
import com.shuwei.intelligent.shelves.utils.ext.startActivity
import kotlinx.coroutines.launch
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.shuwei.intelligent.shelves.utils.ext.setOnDoubleClickListener
@Suppress("DEPRECATION")
open class BaseActivity : AppCompatActivity() {
val TAG = this.javaClass.simpleName
@@ -46,7 +46,6 @@ open class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
hideSystemBars()
binding = ActivityBaseBinding.inflate(layoutInflater)
setContentView(binding.root)
statusBarDarkFont()
@@ -67,21 +66,17 @@ open class BaseActivity : AppCompatActivity() {
// }
lightSwitchClick()
binding.tvLeftStatus.setOnDoubleClickListener {
finish()
}
}
fun hideStatusBar() {
//// enableEdgeToEdge()
//// val uiOptions = (View.SYSTEM_UI_FLAG_FULLSCREEN // 隐藏状态栏
//// or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) // 隐藏导航栏(可选)
//// window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
//
// ImmersionBar.with(this)
// .hideBar(BarHide.FLAG_HIDE_STATUS_BAR)
// .init()
// enableEdgeToEdge()
// val uiOptions = (View.SYSTEM_UI_FLAG_FULLSCREEN // 隐藏状态栏
// or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) // 隐藏导航栏(可选)
// window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
ImmersionBar.with(this)
.hideBar(BarHide.FLAG_HIDE_STATUS_BAR)
.init()
}
fun statusBarDarkFont() {
@@ -162,13 +157,13 @@ open class BaseActivity : AppCompatActivity() {
switchLight(tag.not())
}
binding.tvRightStatus.clickWithDebounce {
// if (this is HomeActivity) {
// startActivity<LogActivity> {
// putExtra(LogActivity.IS_LOG_DIR, true)
// }
// return@clickWithDebounce
// }
// startActivity<SettingActivity> { }
if (this is HomeActivity) {
startActivity<LogActivity> {
putExtra(LogActivity.IS_LOG_DIR, true)
}
return@clickWithDebounce
}
startActivity<SettingActivity> { }
}
}
@@ -186,34 +181,4 @@ open class BaseActivity : AppCompatActivity() {
fileLogger.log(message)
}
/**
* 窗口焦点变化时回调
* 场景:弹出 Dialog 后焦点丢失,Dialog 消失后焦点恢复,此时需重新隐藏系统栏,
* 避免系统栏在 Dialog 关闭后意外"复活"
*/
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
hideSystemBars()
}
}
/**
* 隐藏状态栏和导航栏
* 使用 WindowInsetsControllerCompatAndroidx 推荐方式,向下兼容性好)
* - 状态栏:完全隐藏
* - 导航栏:完全隐藏,底部上滑时临时出现,松手后自动收回(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
*/
private fun hideSystemBars() {
// 允许布局延伸到系统栏区域(edge-to-edge),避免内容被系统栏遮挡
WindowCompat.setDecorFitsSystemWindows(window, false)
WindowInsetsControllerCompat(window, window.decorView).apply {
// 同时隐藏状态栏和导航栏
hide(WindowInsetsCompat.Type.systemBars())
// 底部上滑时临时显示导航栏,松手后自动隐藏(沉浸式 Sticky 模式)
systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
}
@@ -0,0 +1,214 @@
package com.shuwei.intelligent.shelves.net
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.shuwei.intelligent.shelves.App
import com.shuwei.intelligent.shelves.model.ShelfBody
import com.shuwei.intelligent.shelves.model.ShelfModel
import com.shuwei.intelligent.shelves.utils.ext.toJsonString
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
/**
* HomeActivity 专用 ViewModel。
*
* 职责:
* 1. 管理货架列表、重量、温湿度等 UI 状态数据;
* 2. 持有 deviceName,供 Activity 更新顶部状态栏;
* 3. 管理定时任务(保存货架数据、刷新过期状态),生命周期由 viewModelScope 保障,
* 跨配置变更存活,Activity 销毁时自动取消,无需手动持有 Job 引用。
*/
class HomeViewModel : ViewModel() {
companion object {
private const val TAG = "HomeViewModel"
/** 首次保存延迟(1 分钟) */
private const val SAVE_INITIAL_DELAY = 1 * 60 * 1000L
/** 保存间隔(5 分钟) */
private const val SAVE_INTERVAL = 5 * 60 * 1000L
/** 过期检查首次延迟(5 秒) */
private const val OVERDUE_INITIAL_DELAY = 5 * 1000L
/** 过期检查间隔(5 分钟) */
private const val OVERDUE_INTERVAL = 5 * 60 * 1000L
}
// ----------------------------------------------------------------
// UI 状态数据
// ----------------------------------------------------------------
/**
* 货架列表。
* Adapter 持有同一个列表引用,修改后需调用 notifyDataSetChanged / notifyItemChanged 触发刷新。
*/
val shelfItems: MutableList<ShelfModel> = mutableListOf()
/**
* 重量数据缓存。
* keydeviceNo(货架编号),value:重量(克)。
*/
private val weightMap: MutableMap<Int, Int> = mutableMapOf()
/**
* 当前选中货架的列表索引(点击 item 时更新)。
*/
var selectedShelfIndex: Int = 0
/**
* 当前摄氏温度,由串口心跳数据更新,字符串格式(如 "24.5")。
*/
var temperatureC: String = "0"
/**
* 当前湿度,由串口心跳数据更新,字符串格式(如 "60")。
*/
var humidity: String = "0"
/**
* 设备名称,由网络接口响应写入,供顶部状态栏显示。
* 原散落于 HomeActivity 局部变量,现迁入 ViewModel 以跨配置变更保持。
*/
var deviceName: String = ""
// ----------------------------------------------------------------
// 对外暴露的 Flow
// ----------------------------------------------------------------
/**
* 过期状态变化通知。
* 发射需要局部刷新的 adapter position 列表,Activity 收到后调用 notifyItemChanged。
* 仅在状态实际发生变化时发射,避免无效刷新。
*/
private val _overdueUpdateFlow = MutableSharedFlow<List<Int>>(extraBufferCapacity = 4)
val overdueUpdateFlow: SharedFlow<List<Int>> = _overdueUpdateFlow
// ----------------------------------------------------------------
// 重量操作
// ----------------------------------------------------------------
/**
* 更新指定货架编号的重量。
* @param deviceNo 货架编号
* @param weight 重量(克)
*/
fun putWeight(deviceNo: Int, weight: Int) {
weightMap[deviceNo] = weight
}
/**
* 获取指定货架编号的重量。
* @param deviceNo 货架编号
* @return 重量(克),若无记录返回 0
*/
fun getWeight(deviceNo: Int): Int = weightMap[deviceNo] ?: 0
// ----------------------------------------------------------------
// 定时任务
// ----------------------------------------------------------------
/**
* 启动所有定时任务。
* 应在 Activity.onCreate 中调用一次(ViewModel 跨配置变更存活,不会重复启动)。
* 任务在 viewModelScope 中运行,ViewModel 销毁时自动取消,无需手动管理 Job。
*/
fun startPeriodicTasks() {
startSaveTask()
startOverdueTask()
}
/**
* 启动货架数据定时保存任务(延迟1分钟后,每5分钟执行一次)。
*/
private fun startSaveTask() {
viewModelScope.launch {
delay(SAVE_INITIAL_DELAY)
while (true) {
periodicSave()
delay(SAVE_INTERVAL)
}
}
}
/**
* 启动过期状态定时检查任务(延迟5秒后,每5分钟执行一次)。
*/
private fun startOverdueTask() {
viewModelScope.launch {
delay(OVERDUE_INITIAL_DELAY)
while (true) {
periodicUpdateOverdue()
delay(OVERDUE_INTERVAL)
}
}
}
// ----------------------------------------------------------------
// 定时任务内部业务逻辑
// ----------------------------------------------------------------
/**
* 将当前货架数据同步到服务端。
* 若货架无商品(goodsId 为空),则上报备份重量(weightBak)。
*/
private suspend fun periodicSave() {
Log.d(TAG, "periodicSave: list=${shelfItems.toJsonString()}")
val submitList = shelfItems.sortedBy { it.deviceNo }
submitList.forEach {
if (it.goodsId.isNullOrBlank()) {
it.weight = it.weightBak
}
}
val body = ShelfBody().also {
it.deviceId = App.deviceId
it.canteenId = App.canteenId
it.temperature = temperatureC
it.humidity = humidity
it.goodsList = submitList
}
Log.d(TAG, "periodicSave: body=${body.toJsonString()}")
runCatching {
val resp = apiService.saveShelfGoodsList(body = body)
Log.d(TAG, "periodicSave: resp=${resp.toJsonString()}")
}.onFailure {
it.printStackTrace()
Log.d(TAG, "periodicSave 异常:${it.message}")
}
}
/**
* 从服务端拉取货架列表,对比本地数据,将变化的过期状态通过 [overdueUpdateFlow] 发射。
* 按 deviceNo 精确匹配,避免列表顺序不一致导致的状态串位。
*/
private suspend fun periodicUpdateOverdue() {
runCatching {
val response = apiService.getShelfList(deviceId = App.deviceId)
if (!response.isSuccess()) return
val serverItems = response.data?.containerGoodsList ?: return
if (serverItems.isEmpty()) return
// 找出过期状态发生变化的 position 列表
val changedPositions = mutableListOf<Int>()
shelfItems.forEachIndexed { index, model ->
val serverModel = serverItems.find { it.deviceNo == model.deviceNo }
?: return@forEachIndexed
if (serverModel.overdue != model.overdue) {
model.overdue = serverModel.overdue
changedPositions.add(index)
Log.d(TAG, "过期状态变更:deviceNo=${model.deviceNo}, 商品=${model.goodsName}, overdue=${model.overdue}")
}
}
if (changedPositions.isNotEmpty()) {
_overdueUpdateFlow.emit(changedPositions)
}
}.onFailure {
it.printStackTrace()
}
}
}
@@ -0,0 +1,157 @@
package com.shuwei.intelligent.shelves.net
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.shuwei.intelligent.shelves.serial.SerialEventListener
import com.shuwei.intelligent.shelves.serial.SerialPortManager
import com.shuwei.intelligent.shelves.serial.SerialProtocolParser
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
/**
* 串口通信 ViewModel。
* 职责:
* 1. 管理串口的生命周期(open / close);
* 2. 持有 [SerialProtocolParser] 实例,实现 [SerialEventListener] 接收解析结果;
* 3. 通过 [weightFlow]、[tempCtrlEventFlow]、[deviceStatusRefreshFlow] 将事件暴露给 UI 层。
*
* 不持有任何 View / Context 引用,可安全跨配置变更存活。
*/
class SerialViewModel : ViewModel() {
companion object {
private const val TAG = "SerialViewModel"
}
// ----------------------------------------------------------------
// 对外暴露的 FlowActivity 通过 repeatOnLifecycle 收集)
// ----------------------------------------------------------------
/**
* 货架重量更新事件。
* Pair.first = 货架编号(deviceNo),Pair.second = 重量(克)。
*/
private val _weightFlow = MutableSharedFlow<Pair<Int, Int>>(extraBufferCapacity = 64)
val weightFlow: SharedFlow<Pair<Int, Int>> = _weightFlow
/**
* 温控信息上报事件,携带完整日志字符串,Activity 收到后弹窗展示。
*/
private val _tempCtrlEventFlow = MutableSharedFlow<String>(extraBufferCapacity = 8)
val tempCtrlEventFlow: SharedFlow<String> = _tempCtrlEventFlow
/**
* 设备状态刷新信号。
* 串口收到需要刷新顶部状态栏的命令时触发,Activity 收到后用 deviceName 重绘。
*/
private val _deviceStatusRefreshFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 4)
val deviceStatusRefreshFlow: SharedFlow<Unit> = _deviceStatusRefreshFlow
// ----------------------------------------------------------------
// 内部:串口数据通道 + 协议解析器
// ----------------------------------------------------------------
/**
* 串口数据接收通道,无界缓冲。
* IO 线程(串口回调)通过 trySend 写入,单一消费协程串行读取,
* 确保 parser 内部 serialReader 始终在同一协程中访问,规避并发竞争。
*/
private val serialDataChannel = Channel<String>(Channel.UNLIMITED)
/**
* 协议解析器实例。
* 持有 terminalId / terminalVersion / serialReader 等协议状态。
*/
private val parser = SerialProtocolParser(object : SerialEventListener {
/** 解析器需要发送命令时,通过 viewModelScope 异步发送 */
override fun onSendCommand(cmd: String) {
viewModelScope.launch { SerialPortManager.send(cmd) }
}
/** 终端信息就绪(激活或设备状态上报时),仅打印日志,信息已在 parser 内持有 */
override fun onTerminalInfoReady(terminalId: String, terminalVersion: String) {
Log.d(TAG, "终端信息就绪: id=$terminalId, version=$terminalVersion")
}
/** 货架重量更新,发射到 weightFlow */
override fun onWeightUpdate(deviceNo: Int, weight: Int) {
_weightFlow.tryEmit(deviceNo to weight)
}
/** 设备状态需要刷新,发射刷新信号 */
override fun onDeviceStatusUpdate(text: String) {
_deviceStatusRefreshFlow.tryEmit(Unit)
}
/** 温控信息上报,发射日志到 tempCtrlEventFlow */
override fun onTempCtrlReport(logMsg: String) {
_tempCtrlEventFlow.tryEmit(logMsg)
}
})
// ----------------------------------------------------------------
// 对外操作
// ----------------------------------------------------------------
/**
* 打开串口并启动数据接收。
* 应在 Activity.onCreate 中调用一次。
* 串口数据通过 Channel 无锁传递给消费协程,由消费协程串行调用 parser。
*/
fun openSerial() {
viewModelScope.launch {
val opened = SerialPortManager.open()
Log.d(TAG, "openSerial: opened=$opened")
if (!opened) return@launch
// 串口回调在 IO 线程,通过 Channel 传递数据,不嵌套 launch
SerialPortManager.startReceive { data ->
serialDataChannel.trySend(data)
}
// 单一消费协程,串行处理所有串口数据
launch {
for (data in serialDataChannel) {
try {
parser.parseIncoming(data)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
}
/**
* 向串口发送命令。
* @param cmd 十六进制命令字符串
*/
fun sendCmd(cmd: String) {
viewModelScope.launch {
SerialPortManager.send(cmd)
}
}
// ----------------------------------------------------------------
// 生命周期清理
// ----------------------------------------------------------------
/**
* ViewModel 销毁时关闭 Channel 和串口。
* Channel 关闭后,消费协程的 for 循环自然结束。
* 串口关闭在独立 IO 协程中执行,避免阻塞主线程。
*/
override fun onCleared() {
serialDataChannel.close()
CoroutineScope(Dispatchers.IO).launch {
SerialPortManager.close()
}
super.onCleared()
}
}
@@ -0,0 +1,40 @@
package com.shuwei.intelligent.shelves.serial
/**
* 串口协议解析事件监听器。
* [SerialProtocolParser] 解析完成后通过此接口将结果回调给上层(ViewModel 或 Activity)。
*/
interface SerialEventListener {
/**
* 需要向串口发送命令。
* @param cmd 十六进制命令字符串
*/
fun onSendCommand(cmd: String)
/**
* 获取到终端信息(激活指令或设备状态上报时触发)。
* @param terminalId 终端 ID15字节)
* @param terminalVersion 固件版本(5字节)
*/
fun onTerminalInfoReady(terminalId: String, terminalVersion: String)
/**
* 重量数据更新。
* @param deviceNo 货架编号(从 1 开始)
* @param weight 重量(克)
*/
fun onWeightUpdate(deviceNo: Int, weight: Int)
/**
* 设备状态文本更新(用于顶部状态栏左侧显示)。
* @param text 显示内容
*/
fun onDeviceStatusUpdate(text: String)
/**
* 温控信息上报(需要弹窗提示用户)。
* @param logMsg 温控日志信息
*/
fun onTempCtrlReport(logMsg: String)
}
@@ -0,0 +1,326 @@
package com.shuwei.intelligent.shelves.serial
import android.util.Log
import com.shuwei.intelligent.shelves.activity.HomeActivity.Companion.ACTIVE_CMD
import com.shuwei.intelligent.shelves.activity.HomeActivity.Companion.FOOTER
import com.shuwei.intelligent.shelves.activity.HomeActivity.Companion.HEADER
import com.shuwei.intelligent.shelves.utils.binaryToHex
import com.shuwei.intelligent.shelves.utils.hexToBinary
/**
* 串口协议解析器。
* 职责:将原始串口字节流解析为结构化协议事件,通过 [SerialEventListener] 回调通知上层。
* 无 Android UI 依赖,可进行单元测试。
*
* @param listener 协议事件监听器,由 SerialViewModel 实现并传入
*/
class SerialProtocolParser(private val listener: SerialEventListener) {
companion object {
private const val TAG = "SerialProtocolParser"
private const val LOG_PREFIX = "receiveSerialPortData:收到"
}
/**
* 终端 ID(15字节),由激活指令或设备状态上报时获取。
* 对外只读,内部可写。
*/
var terminalId: String = ""
private set
/**
* 固件版本(5字节),由激活指令或设备状态上报时获取。
* 对外只读,内部可写。
*/
var terminalVersion: String = ""
private set
/**
* 串口拼包缓冲器。
* 仅在单一消费协程中调用 [parseIncoming],线程安全。
*/
private val serialReader = StringBuilder()
// ----------------------------------------------------------------
// 对外入口:拼包
// ----------------------------------------------------------------
/**
* 处理一条原始串口数据,执行拼包逻辑后交由 [handleReceivedData] 分发命令。
* @param srcData 原始十六进制字符串(大写)
*/
fun parseIncoming(srcData: String) {
log("parseIncoming: $srcData")
var data = ""
if (srcData.contains(HEADER) && srcData.contains(FOOTER)) {
serialReader.clear()
val startIndex = srcData.lastIndexOf(HEADER)
var endIndex = srcData.indexOf(FOOTER) + FOOTER.length
if (endIndex < startIndex) {
// 类似这种:31314FFFFFD6DADEDB01010CFOOTER 在 HEADER 前
data = srcData.substring(startIndex)
if (data.contains(FOOTER)) {
endIndex = data.indexOf(FOOTER) + FOOTER.length
data = data.substring(0, endIndex)
handleReceivedData(data)
serialReader.clear()
return
}
serialReader.clear()
serialReader.append(data).append(",")
return
}
serialReader.append(srcData.substring(startIndex, endIndex))
data = serialReader.toString()
handleReceivedData(data)
serialReader.clear()
return
}
if (srcData.contains(HEADER) && !srcData.contains(FOOTER)) {
// 只有头,没有尾,等待后续分片
serialReader.clear()
val startIndex = srcData.lastIndexOf(HEADER)
serialReader.append(srcData.substring(startIndex)).append(",")
return
}
if (!srcData.contains(HEADER) && !srcData.contains(FOOTER)) {
// 中间分片
serialReader.append(srcData).append(",")
return
}
// 只有尾,没有头:拼接并处理
val endIndex = srcData.indexOf(FOOTER) + FOOTER.length
serialReader.append(srcData.substring(0, endIndex))
log("parseIncoming:serialReader: $serialReader")
data = serialReader.toString().replace(",", "")
handleReceivedData(data)
}
// ----------------------------------------------------------------
// 命令分发
// ----------------------------------------------------------------
/**
* 根据命令码分发处理各类串口协议。
*/
private fun handleReceivedData(data: String) {
if (data.isEmpty()) return
val cmd = data.substring(HEADER.length, HEADER.length + 4)
val cmdFlag = cmd.substring(0, 2)
when (cmd) {
"0001" -> handleActivation(data, cmd, cmdFlag)
"0101", "8101" -> handleHeartBeat(data, cmd, cmdFlag)
"0202" -> handleLockFeedback(data, cmd, cmdFlag)
"0302" -> handleDeviceStatus(data, cmd, cmdFlag)
"0501" -> handleCardUpload(data, cmd, cmdFlag)
"0B01" -> handleOperationRecord(data, cmd, cmdFlag)
"0C01" -> handleTimeSync(data, cmd, cmdFlag)
"0E01" -> handlePositionReport(data, cmd, cmdFlag)
"1001" -> handleTempCtrlReport(data, cmd, cmdFlag)
"1002" -> log("${LOG_PREFIX}[温控信息应答]1002数据:$data")
"0F02" -> log("${LOG_PREFIX}[温控信息应答]0F02数据:$data")
else -> {}
}
}
// ----------------------------------------------------------------
// 各命令处理
// ----------------------------------------------------------------
/**
* 处理终端发起的激活指令(0001)。
* 解析终端 ID 和固件版本,并发送激活命令。
*/
private fun handleActivation(data: String, cmd: String, cmdFlag: String) {
val reqBody = data.substring(HEADER.length + cmd.length, data.length - FOOTER.length)
if (reqBody.length == 44) {
terminalId = reqBody.substring(2, 32)
terminalVersion = reqBody.substring(32, 42)
log("${LOG_PREFIX}[激活]${cmdFlag}指令:$data,终端id:$terminalId,固件版本:$terminalVersion,发送激活命令:$ACTIVE_CMD")
listener.onSendCommand(ACTIVE_CMD)
listener.onTerminalInfoReady(terminalId, terminalVersion)
}
}
/**
* 处理心跳指令(0101 / 8101)。
* 发送心跳应答,并解析重量数据。
*/
private fun handleHeartBeat(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}01020506${terminalVersion}$FOOTER"
log("${LOG_PREFIX}[心跳]${cmdFlag}指令:$data,发送应答指令:$respCmd")
listener.onSendCommand(respCmd)
parseHeartBeat(data)
}
/**
* 处理开关锁反馈(0202)。
* 发送开锁应答。
*/
private fun handleLockFeedback(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}020200${terminalId}${terminalVersion}$FOOTER"
log("${LOG_PREFIX}[开锁]${cmdFlag}指令:$data,发送应答指令:$respCmd")
listener.onSendCommand(respCmd)
}
/**
* 处理获取设备状态(0302)。
* 解析设备重量和终端信息。
*/
private fun handleDeviceStatus(data: String, cmd: String, cmdFlag: String) {
log("${LOG_PREFIX}[获取设备状态]${cmdFlag}指令:$data")
parseDeviceInfo(data)
}
/**
* 处理卡号上发(0501)。
*/
private fun handleCardUpload(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}050203$FOOTER"
log("${LOG_PREFIX}[卡号上发]${cmdFlag}指令:$data,发送应答指令:$respCmd")
listener.onSendCommand(respCmd)
}
/**
* 处理操作记录上传(0B01)。
*/
private fun handleOperationRecord(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}0B0203$FOOTER"
log("${LOG_PREFIX}[操作记录上传]${cmdFlag}指令:$data,发送应答指令:$respCmd")
listener.onSendCommand(respCmd)
}
/**
* 处理对时指令(0C01)。
* 将当前系统时间转换为小端序十六进制并应答。
*/
private fun handleTimeSync(data: String, cmd: String, cmdFlag: String) {
val timeLong = System.currentTimeMillis() / 1000 + (8 * 3600)
val timeStr = "0000000" + String.format("%X", timeLong)
val time = timeStr.substring(timeStr.length - 8).chunked(2).reversed().joinToString("")
val respCmd = "${HEADER}0B02${time}$FOOTER"
log("${LOG_PREFIX}[对时]${cmdFlag}指令:$data,发送应答指令:$respCmd")
listener.onSendCommand(respCmd)
}
/**
* 处理终端位置上报(0E01)。
*/
private fun handlePositionReport(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}0E0200$FOOTER"
log("${LOG_PREFIX}[终端位置上报]${cmdFlag}指令:$data,发送应答指令:$respCmd")
listener.onSendCommand(respCmd)
}
/**
* 处理温控信息上报(1001)。
* 发送应答,并通过回调通知上层弹窗展示日志。
*/
private fun handleTempCtrlReport(data: String, cmd: String, cmdFlag: String) {
val respCmd = "${HEADER}100200$FOOTER"
val logMsg = "${LOG_PREFIX}[温控信息上报]${cmdFlag}指令:$data,发送应答指令:$respCmd"
log(logMsg)
listener.onSendCommand(respCmd)
listener.onDeviceStatusUpdate("")
listener.onTempCtrlReport(logMsg)
}
// ----------------------------------------------------------------
// 数据解析辅助
// ----------------------------------------------------------------
/**
* 解析心跳报文中的重量和固定数据体。
*/
private fun parseHeartBeat(data: String) {
runCatching {
if (data.length < 200) return
val weightStartIndex = 14
val weightEndIndex = weightStartIndex + 12 * 8
parseWeightInfo(start = weightStartIndex, end = weightEndIndex, data = data)
val temperatureEndIndex = 158 + 2
parseFixDataBody(data, temperatureEndIndex)
}.onFailure { it.printStackTrace() }
}
/**
* 解析设备状态报文(0302)中的重量和固定数据体。
*/
private fun parseDeviceInfo(data: String) {
val weightStartIndex = HEADER.length + 14
val temperatureStartIndex = weightStartIndex + 12 * 8
val temperatureEndIndex = temperatureStartIndex + 2
try {
data.substring(temperatureStartIndex, temperatureEndIndex)
} catch (e: Exception) {
e.printStackTrace()
return
}
listener.onDeviceStatusUpdate("")
parseWeightInfo(start = weightStartIndex, end = temperatureStartIndex, data = data)
parseFixDataBody(data, temperatureEndIndex)
}
/**
* 解析重量数据区,逐格读取并通过 [listener] 回调。
* @param start 重量数据起始索引
* @param end 重量数据结束索引
* @param data 完整协议报文(十六进制字符串)
*/
fun parseWeightInfo(start: Int, end: Int, data: String) {
var deviceNo = 1
var count = start
while (count < end) {
val weightInfo = data.substring(count, count + 8)
// 将小端序 4 字节转换为大端序
var weightHex = weightInfo.run {
substring(6, 8) + substring(4, 6) + substring(2, 4) + substring(0, 2)
}
var realWeight: Int
var firstByteBinary = hexToBinary(weightHex.substring(0, 2))
if (firstByteBinary.startsWith("0")) {
// 正数:按十六进制解析,失败时兜底为 0
realWeight = weightHex.toIntOrNull(16) ?: 0
} else {
// 负数:清除符号位后取绝对值,再取反
firstByteBinary = "0${firstByteBinary.substring(1)}"
val firstByteHex = binaryToHex(firstByteBinary)
weightHex = firstByteHex + weightHex.substring(2)
realWeight = -1 * (weightHex.toIntOrNull(16) ?: 0)
}
listener.onWeightUpdate(deviceNo, realWeight)
count += 8
deviceNo++
}
}
/**
* 从数据体尾部提取 terminalId / terminalVersion(首次获取时有效)。
* @param data 完整协议报文
* @param temperatureEndIndex 温度字段结束索引,固定数据体从此之后开始
*/
private fun parseFixDataBody(data: String, temperatureEndIndex: Int) {
try {
// 已有终端信息则无需重复解析
if (terminalId.isNotBlank() && terminalVersion.isNotBlank()) return
val fixDataBodyStartIndex = temperatureEndIndex + 1
val fixDataBody = data.substring(fixDataBodyStartIndex, data.length - FOOTER.length)
if (fixDataBody.length == 42) {
terminalId = fixDataBody.substring(0, 30)
terminalVersion = fixDataBody.substring(30, 40)
log("从设备状态获取数据,终端id:$terminalId,固件版本:$terminalVersion")
listener.onTerminalInfoReady(terminalId, terminalVersion)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun log(msg: String) {
Log.d(TAG, msg)
}
}
@@ -1,24 +0,0 @@
package com.shuwei.intelligent.shelves.utils
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
/**
* 网络连接检测工具类
* 提供检测设备网络连接状态的功能
*/
object NetworkUtils {
/**
* 检测设备是否连接到网络
* @param context 应用上下文
* @return true 表示已连接网络,false 表示未连接
*/
fun isNetworkConnected(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val network = connectivityManager.activeNetwork ?: return false
val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
}
@@ -25,7 +25,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import android.content.ClipData
import android.content.ClipboardManager
import android.os.SystemClock
fun Context.toast(
message: String?,
@@ -176,22 +175,3 @@ fun String.copyTextToClipboard(context: Context) {
Toast.makeText(context, "文本已复制到剪贴板", Toast.LENGTH_SHORT).show()
}
/**
* 为View添加双击点击事件的扩展函数
*/
fun View.setOnDoubleClickListener(onDoubleClickListener: (View) -> Unit) {
var lastClickTime = 0L
var lastClickView: View? = null
this.setOnClickListener { view ->
val currentTime = SystemClock.elapsedRealtime()
if (lastClickView === view && currentTime - lastClickTime < 500) {
// 双击事件触发
onDoubleClickListener(view)
lastClickTime = 0
} else {
lastClickTime = currentTime
lastClickView = view
}
}
}
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#4ECDC4" />
<corners android:radius="12dp" />
<stroke
android:width="2dp"
android:color="#2BA39F" />
</shape>
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="80dp"
android:height="80dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- 网络断开的图标 -->
<path
android:fillColor="#FF6B6B"
android:pathData="M1,9l2,2c4.97,-4.97 13.03,-4.97 18,0l2,-2C16.93,2.93 7.08,2.93 1,9zM5,13l2,2c2.76,-2.76 7.24,-2.76 10,0l2,-2c-4.42,-4.42 -11.58,-4.42 -16,0zM9,17h6v2h-6z" />
</vector>
@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="ring"
android:innerRadiusRatio="3"
android:thicknessRatio="8"
android:useLevel="false">
<gradient
android:type="sweep"
android:startColor="#FF6B6B"
android:centerColor="#4ECDC4"
android:endColor="#FF6B6B"
android:angle="0" />
</shape>
@@ -0,0 +1,45 @@
<?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="match_parent"
android:orientation="vertical"
tools:background="@color/bg_page">
<FrameLayout
android:id="@+id/flStatusBar"
android:layout_width="match_parent"
android:layout_height="68dp"
android:paddingStart="25dp"
android:paddingEnd="25dp"
android:paddingTop="25dp">
<TextView
android:id="@+id/tv_left_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:layout_gravity="start"
android:textSize="24sp"
android:fontFamily="sans-serif-medium"
tools:text="1-1冷藏柜 -2°C / 87%" />
<TextView
android:id="@+id/tv_right_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:layout_gravity="end"
android:fontFamily="sans-serif-medium"
android:textSize="24sp"
tools:text="6月22日 星期日 15:26:33" />
</FrameLayout>
<FrameLayout
android:id="@+id/flContainer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:background="@color/bg_page">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvShelf"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
android:overScrollMode="never"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
tools:listitem="@layout/list_item_shelf"/>
<include
android:id="@+id/include"
android:layout_width="match_parent"
android:layout_height="match_parent"
layout="@layout/layout_empty_view"
android:visibility="gone"/>
</FrameLayout>
@@ -0,0 +1,48 @@
<?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="match_parent"
android:orientation="vertical">
<ImageView
android:layout_width="450dp"
android:layout_height="430dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="150dp"
android:src="@drawable/img_init"
tools:ignore="ContentDescription"
android:visibility="invisible"/>
<TextView
android:id="@+id/btnInit"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="100dp"
android:background="@drawable/bg_init_button"
android:gravity="center"
android:padding="10dp"
android:text="设备初始化"
android:textColor="@color/white"
android:textSize="30sp"
android:textStyle="bold"
android:visibility="visible"
tools:ignore="HardcodedText" />
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
<ImageView
android:id="@+id/ivQrCode"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginBottom="50dp"
tools:ignore="ContentDescription"
tools:src="@mipmap/ic_logo512" />
</LinearLayout>
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/ivBack"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="top|start"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:src="@drawable/ic_back_512"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
tools:text="设置"
android:textColor="@color/black"
android:textSize="28sp" />
<Button
android:id="@+id/btnFind"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="15dp"
android:textSize="18sp"
android:layout_gravity="end|center_vertical"
android:text="查找1001指令"/>
</FrameLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvLogList"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
@@ -0,0 +1,269 @@
<?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="match_parent"
android:orientation="vertical"
tools:background="@color/bg_page">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="376dp"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="24dp"
app:cardBackgroundColor="@color/white"
app:cardCornerRadius="12dp"
app:cardElevation="0dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="@+id/tvShelfName"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:layout_marginTop="24dp"
android:fontFamily="sans-serif-medium"
android:gravity="center"
android:textColor="@color/bg_page"
android:textSize="30sp"
tools:text="货架 - 03" />
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
<TextView
android:id="@+id/tvFoodWeight"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/food_weight_orange"
android:textSize="60sp"
android:textStyle="bold"
tools:text="1506克" />
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
<TextView
android:id="@+id/tvFoodName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/food_name_black"
android:textSize="36sp"
android:textStyle="bold"
tools:text="金针菇" />
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnClearZero"
android:layout_width="120dp"
android:layout_height="60dp"
android:layout_gravity="top|end"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:backgroundTint="@color/bg_page"
android:ellipsize="end"
android:fontFamily="sans-serif-medium"
android:insetTop="0dp"
android:insetBottom="0dp"
android:maxLines="1"
android:text="清零"
android:textColor="@color/white"
android:textSize="30sp"
app:cornerRadius="12dp"
app:elevation="0dp"
tools:ignore="HardcodedText" />
<ImageView
android:id="@+id/ivBack"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_gravity="top|start"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:src="@drawable/ic_back_512"
tools:ignore="ContentDescription" />
</FrameLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
app:cardBackgroundColor="@color/white"
app:cardCornerRadius="12dp"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginStart="48dp"
android:layout_marginTop="48dp"
android:layout_marginEnd="48dp"
android:background="@drawable/shape_search"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:src="@drawable/ic_search_food" />
<EditText
android:id="@+id/etInputFood"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginEnd="24dp"
android:background="@null"
android:fontFamily="sans-serif-medium"
android:hint="输入食材名称"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"
android:paddingStart="1dp"
android:paddingEnd="1dp"
android:text=""
android:textColor="@color/food_name_black"
android:textColorHint="#B4BEC8"
android:textSize="30sp"
tools:ignore="Autofill,HardcodedText,TextFields" />
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="24dp"
android:layout_weight="1">
<com.scwang.smart.refresh.layout.SmartRefreshLayout
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.scwang.smart.refresh.header.ClassicsHeader
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvSearch"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:overScrollMode="never"
android:paddingStart="36dp"
android:paddingTop="12dp"
android:paddingEnd="36dp"
android:paddingBottom="12dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:itemCount="10"
tools:listitem="@layout/list_item_search" />
<com.scwang.smart.refresh.footer.ClassicsFooter
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
<include
android:id="@+id/include"
layout="@layout/layout_empty_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnClearEmpty"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_marginStart="48dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
android:layout_weight="1"
android:backgroundTint="@color/bg_page"
android:ellipsize="end"
android:insetTop="0dp"
android:insetBottom="0dp"
android:maxLines="1"
android:text="清空"
android:textColor="@color/white"
android:textSize="30sp"
android:textStyle="bold"
app:cornerRadius="12dp"
app:elevation="0dp"
tools:ignore="HardcodedText" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnConfirm"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="48dp"
android:layout_marginBottom="24dp"
android:layout_weight="1"
android:backgroundTint="@color/bg_page"
android:ellipsize="end"
android:insetTop="0dp"
android:insetBottom="0dp"
android:maxLines="1"
android:text="确定"
android:textColor="@color/white"
android:textSize="30sp"
android:textStyle="bold"
app:cornerRadius="12dp"
app:elevation="0dp"
tools:ignore="HardcodedText" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/shape_dialog">
<TextView
android:id="@+id/tvMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:paddingStart="25dp"
android:paddingTop="13dp"
android:paddingBottom="13dp"
android:paddingEnd="25dp"
android:textColor="@color/white_f6"
android:textSize="32sp"
tools:text="暂无数据" />
</FrameLayout>
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="400dp"
android:layout_height="300dp"
app:cardCornerRadius="10dp"
app:cardElevation="0dp"
xmlns:tools="http://schemas.android.com/tools"
app:cardBackgroundColor="@color/white">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginVertical="30dp"
android:orientation="vertical">
<TextView
android:id="@+id/tvDialogTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textColor="@color/black"
android:textSize="22sp"
tools:text="标题"/>
<TextView
android:id="@+id/tvDialogContent"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_gravity="center_horizontal"
android:textColor="@color/black"
android:gravity="center"
android:layout_marginHorizontal="50dp"
android:textSize="16sp"
tools:text="内容"/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnConfirm"
android:layout_width="120dp"
android:layout_height="50dp"
android:text="确定"
android:background="@color/bg_card_blue"
android:textColor="@color/white"
android:textSize="20sp"
android:layout_gravity="center_horizontal"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="20dp"
android:paddingTop="10dp"
android:paddingEnd="20dp"
android:paddingBottom="10dp"
android:background="@drawable/shape_dialog"
tools:ignore="MissingDefaultResource">
<ProgressBar
android:layout_width="60dp"
android:layout_height="60dp"
android:indeterminateTint="@color/white_f6" />
<TextView
android:id="@+id/tvMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="加载中……"
android:textColor="@color/white_f6"
android:textSize="26sp"
tools:ignore="HardcodedText" />
</LinearLayout>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:layout_gravity="center"
android:orientation="vertical"
tools:ignore="MissingDefaultResource,UseCompoundDrawables">
<ImageView
android:id="@+id/ivEmptyIcon"
android:layout_width="250dp"
android:layout_height="wrap_content"
android:src="@drawable/ic_empty_gray"
android:adjustViewBounds="true"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/tvEmptyContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black999"
android:layout_marginTop="50dp"
android:textSize="50sp"
android:text="暂无数据"
tools:ignore="HardcodedText" />
</LinearLayout>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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="60dp">
<TextView
android:id="@+id/tvLogName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="30dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="20sp"
tools:text="2025-11-18" />
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_gravity="bottom"
android:layout_marginHorizontal="30dp"
app:dividerColor="@color/gray_edit" />
</FrameLayout>
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<!--<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"-->
<!-- xmlns:tools="http://schemas.android.com/tools"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="80dp"-->
<!-- android:layout_margin="12dp">-->
<!-- <TextView-->
<!-- android:id="@+id/btnFoodName"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_gravity="center"-->
<!-- android:ellipsize="end"-->
<!-- android:gravity="center"-->
<!-- android:maxLines="1"-->
<!-- android:paddingStart="5dp"-->
<!-- android:paddingEnd="5dp"-->
<!-- android:fontFamily="sans-serif-medium"-->
<!-- android:textColor="@color/black999"-->
<!-- android:textSize="30sp"-->
<!-- tools:text="土豆丝" />-->
<!--</FrameLayout>-->
<com.google.android.material.button.MaterialButton 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:id="@+id/btnFoodName"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_margin="12dp"
android:ellipsize="end"
android:fontFamily="sans-serif-medium"
android:gravity="center"
android:insetTop="0dp"
android:insetBottom="0dp"
android:maxLines="1"
app:paddingStart="5dp"
app:paddingEnd="5dp"
android:insetRight="0dp"
android:insetLeft="0dp"
android:paddingHorizontal="8dp"
android:textColor="@color/black999"
android:textSize="30sp"
app:cornerRadius="12dp"
app:strokeWidth="2dp"
tools:text="土豆丝土豆丝土豆丝土豆丝土豆丝土豆丝"
/>
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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:id="@+id/layoutCard"
android:layout_width="match_parent"
android:layout_height="216dp"
android:layout_margin="12dp"
app:cardBackgroundColor="@color/bg_card_blue"
app:cardCornerRadius="12dp"
app:cardElevation="0dp"
app:rippleColor="#B0BEC5">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:id="@+id/tvShelfName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/shelf_name_white"
android:textSize="20sp"
android:textStyle="bold"
tools:text="货架-01" />
<Space
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<TextView
android:id="@+id/tvFoodName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="30sp"
android:textStyle="bold"
tools:text="空" />
<Space
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="0.833" />
<TextView
android:id="@+id/tvFoodWeight"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/food_weight_blue"
android:textSize="24sp"
android:textStyle="bold"
tools:text="0千克" />
<Space
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="0.933" />
<TextView
android:id="@+id/tvStoreDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/food_weight_blue"
android:textSize="18sp"
tools:text="-" />
</LinearLayout>
<ImageView
android:id="@+id/ivStaleFood"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/ic_stale_food_512"
android:layout_gravity="top|end"
tools:ignore="ContentDescription"
android:visibility="gone"/>
</com.google.android.material.card.MaterialCardView>
+13 -8
View File
@@ -5,34 +5,39 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="false"
tools:background="@color/bg_page">
<FrameLayout
android:id="@+id/flStatusBar"
android:layout_width="match_parent"
android:layout_height="68dp"
android:paddingStart="25dp"
android:paddingEnd="25dp"
android:paddingTop="25dp">
android:layout_height="30dp">
<!-- android:paddingTop="9dp"-->
<TextView
android:id="@+id/tv_left_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:textColor="@color/white"
android:layout_gravity="start"
android:textSize="24sp"
android:textSize="12sp"
android:paddingStart="12dp"
android:paddingEnd="0dp"
android:fontFamily="sans-serif-medium"
tools:text="1-1冷藏柜 -2°C / 87%" />
<TextView
android:id="@+id/tv_right_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:textColor="@color/white"
android:layout_gravity="end"
android:fontFamily="sans-serif-medium"
android:textSize="24sp"
android:textSize="12sp"
android:paddingStart="0dp"
android:paddingEnd="12dp"
tools:text="6月22日 星期日 15:26:33" />
</FrameLayout>
+10 -2
View File
@@ -13,8 +13,8 @@
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
android:overScrollMode="never"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginStart="6dp"
android:layout_marginEnd="6dp"
tools:listitem="@layout/list_item_shelf"/>
@@ -24,4 +24,12 @@
android:layout_height="match_parent"
layout="@layout/layout_empty_view"
android:visibility="gone"/>
<!-- <include-->
<!-- android:id="@+id/includeLight"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- layout="@layout/layout_light_switch"-->
<!-- android:visibility="gone"/>-->
</FrameLayout>
+39 -136
View File
@@ -1,145 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<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="match_parent">
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/llDeviceInit"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone">
<ImageView
android:layout_width="225dp"
android:layout_height="215dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="75dp"
android:src="@drawable/img_init"
tools:ignore="ContentDescription"
android:visibility="invisible"/>
<ImageView
android:layout_width="450dp"
android:layout_height="430dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="150dp"
android:src="@drawable/img_init"
tools:ignore="ContentDescription"
android:visibility="invisible"/>
<TextView
android:id="@+id/btnInit"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="50dp"
android:background="@drawable/bg_init_button"
android:gravity="center"
android:padding="10dp"
android:text="设备初始化"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold"
android:visibility="visible"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/btnInit"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="100dp"
android:background="@drawable/bg_init_button"
android:gravity="center"
android:padding="10dp"
android:text="设备初始化"
android:textColor="@color/white"
android:textSize="30sp"
android:textStyle="bold"
android:visibility="visible"
tools:ignore="HardcodedText" />
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
<ImageView
android:id="@+id/ivQrCode"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginBottom="25dp"
tools:ignore="ContentDescription"
tools:src="@mipmap/ic_logo512" />
<ImageView
android:id="@+id/ivQrCode"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginBottom="50dp"
tools:ignore="ContentDescription"
tools:src="@mipmap/ic_logo512" />
</LinearLayout>
<LinearLayout
android:id="@+id/llNetwork"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">
<!-- Loading 动画容器 -->
<LinearLayout
android:id="@+id/llLoading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:visibility="visible"
android:layout_marginTop="260dp">
<!-- 加载动画 -->
<ImageView
android:id="@+id/ivLoading"
android:layout_width="160dp"
android:layout_height="160dp"
android:src="@drawable/loading_spinner"
android:contentDescription="加载中" />
<!-- 倒计时文字 -->
<TextView
android:id="@+id/tvCountdown"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="60秒"
android:textSize="40sp"
android:textColor="@color/black"
android:textStyle="bold"
android:layout_marginTop="50dp" />
<!-- 提示文字 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="网络连接检测中,请稍后……"
android:textSize="22sp"
android:textColor="#555555"
android:layout_marginTop="20dp" />
</LinearLayout>
<!-- 连接网络按钮容器 -->
<LinearLayout
android:id="@+id/llNetworkButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:visibility="gone">
<!-- 网络错误图标 -->
<ImageView
android:layout_width="140dp"
android:layout_height="140dp"
android:src="@drawable/ic_network_error"
android:contentDescription="网络连接失败"
android:layout_marginBottom="30dp" />
<!-- 连接失败提示 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="网络连接失败"
android:textSize="36sp"
android:textColor="@color/black"
android:textStyle="bold"
android:layout_marginBottom="40dp" />
<!-- 连接网络按钮 -->
<Button
android:id="@+id/btnConnectNetwork"
android:layout_width="350dp"
android:layout_height="70dp"
android:text="连接网络"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@android:color/white"
android:background="@drawable/btn_connect_network" />
</LinearLayout>
</LinearLayout>
</FrameLayout>
</LinearLayout>
@@ -33,8 +33,8 @@
android:id="@+id/btnFind"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="15dp"
android:textSize="18sp"
android:layout_marginEnd="10dp"
android:textSize="12sp"
android:layout_gravity="end|center_vertical"
android:text="查找1001指令"/>
</FrameLayout>
@@ -0,0 +1,111 @@
<?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="match_parent"
android:orientation="vertical"
tools:background="@color/bg_page">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="260dp"
android:layout_marginStart="12dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="12dp"
app:cardBackgroundColor="@color/white"
app:cardCornerRadius="6dp"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/ivBack"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="top|start"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:src="@drawable/ic_back_512"
tools:ignore="ContentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="28sp"
android:layout_gravity="center"
android:text="设置"
android:textColor="@color/black"/>
</FrameLayout>
<!-- <EditText-->
<!-- android:id="@+id/etInputCmdPrefix"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="60dp"-->
<!-- android:hint="请输入命令"-->
<!-- android:maxLines="1"-->
<!-- android:text=""-->
<!-- android:textSize="22sp" />-->
<EditText
android:id="@+id/etInputCmd"
android:layout_width="match_parent"
android:layout_height="60dp"
android:hint="请输入命令内容"
android:maxLines="1"
android:textSize="22sp" />
<Button
android:id="@+id/btnSendCmd"
android:layout_width="180dp"
android:layout_height="60dp"
android:textSize="22sp"
android:layout_gravity="center"
android:layout_marginTop="30dp"
android:text="发送命令" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="@color/white"
app:cardCornerRadius="6dp"
app:cardElevation="0dp">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@+id/tvCmdRecord"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textSize="20sp"
android:text=""/>
</LinearLayout>
</ScrollView>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
+61 -61
View File
@@ -9,12 +9,12 @@
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="376dp"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="24dp"
android:layout_height="190dp"
android:layout_marginStart="12dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="12dp"
app:cardBackgroundColor="@color/white"
app:cardCornerRadius="12dp"
app:cardCornerRadius="6dp"
app:cardElevation="0dp">
<FrameLayout
@@ -25,20 +25,20 @@
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="@+id/tvShelfName"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:layout_marginTop="24dp"
android:layout_height="30dp"
android:layout_marginTop="12dp"
android:fontFamily="sans-serif-medium"
android:gravity="center"
android:textColor="@color/bg_page"
android:textSize="30sp"
android:textSize="15sp"
tools:text="货架 - 03" />
<Space
@@ -54,7 +54,7 @@
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/food_weight_orange"
android:textSize="60sp"
android:textSize="30sp"
android:textStyle="bold"
tools:text="1506克" />
@@ -70,7 +70,7 @@
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/food_name_black"
android:textSize="36sp"
android:textSize="18sp"
android:textStyle="bold"
tools:text="金针菇" />
@@ -82,11 +82,11 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/btnClearZero"
android:layout_width="120dp"
android:layout_height="60dp"
android:layout_width="65dp"
android:layout_height="30dp"
android:layout_gravity="top|end"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:backgroundTint="@color/bg_page"
android:ellipsize="end"
android:fontFamily="sans-serif-medium"
@@ -95,20 +95,21 @@
android:maxLines="1"
android:text="清零"
android:textColor="@color/white"
android:textSize="30sp"
app:cornerRadius="12dp"
android:textSize="15sp"
app:cornerRadius="6dp"
app:elevation="0dp"
tools:ignore="HardcodedText" />
<ImageView
android:id="@+id/ivBack"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="top|start"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:src="@drawable/ic_back_512"
tools:ignore="ContentDescription" />
</FrameLayout>
</com.google.android.material.card.MaterialCardView>
@@ -116,12 +117,12 @@
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="@color/white"
app:cardCornerRadius="12dp"
app:cardCornerRadius="6dp"
app:cardElevation="0dp">
<LinearLayout
@@ -131,26 +132,26 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginStart="48dp"
android:layout_marginTop="48dp"
android:layout_marginEnd="48dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:background="@drawable/shape_search"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:src="@drawable/ic_search_food" />
<EditText
android:id="@+id/etInputFood"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginEnd="24dp"
android:layout_height="36dp"
android:layout_marginEnd="12dp"
android:background="@null"
android:fontFamily="sans-serif-medium"
android:hint="输入食材名称"
@@ -162,14 +163,14 @@
android:text=""
android:textColor="@color/food_name_black"
android:textColorHint="#B4BEC8"
android:textSize="30sp"
android:textSize="15sp"
tools:ignore="Autofill,HardcodedText,TextFields" />
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="24dp"
android:layout_marginTop="8dp"
android:layout_weight="1">
<com.scwang.smart.refresh.layout.SmartRefreshLayout
@@ -187,10 +188,10 @@
android:layout_height="match_parent"
android:clipToPadding="false"
android:overScrollMode="never"
android:paddingStart="36dp"
android:paddingTop="12dp"
android:paddingEnd="36dp"
android:paddingBottom="12dp"
android:paddingStart="12dp"
android:paddingTop="4dp"
android:paddingEnd="12dp"
android:paddingBottom="4dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:itemCount="10"
@@ -208,24 +209,23 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
android:orientation="horizontal"
android:gravity="center_vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnClearEmpty"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_marginStart="48dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
android:layout_weight="1"
android:layout_height="40dp"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
android:backgroundTint="@color/bg_page"
android:ellipsize="end"
android:insetTop="0dp"
@@ -233,21 +233,22 @@
android:maxLines="1"
android:text="清空"
android:textColor="@color/white"
android:textSize="30sp"
android:textSize="15sp"
android:textStyle="bold"
app:cornerRadius="12dp"
app:cornerRadius="6dp"
app:elevation="0dp"
tools:ignore="HardcodedText" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnConfirm"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="48dp"
android:layout_marginBottom="24dp"
android:layout_weight="1"
android:layout_height="40dp"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="12dp"
android:backgroundTint="@color/bg_page"
android:ellipsize="end"
android:insetTop="0dp"
@@ -255,12 +256,11 @@
android:maxLines="1"
android:text="确定"
android:textColor="@color/white"
android:textSize="30sp"
android:textSize="15sp"
android:textStyle="bold"
app:cornerRadius="12dp"
app:cornerRadius="6dp"
app:elevation="0dp"
tools:ignore="HardcodedText" />
</LinearLayout>
</LinearLayout>
+5 -5
View File
@@ -11,12 +11,12 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:paddingStart="25dp"
android:paddingTop="13dp"
android:paddingBottom="13dp"
android:paddingEnd="25dp"
android:paddingStart="12dp"
android:paddingTop="6dp"
android:paddingBottom="6dp"
android:paddingEnd="12dp"
android:textColor="@color/white_f6"
android:textSize="32sp"
android:textSize="16sp"
tools:text="暂无数据" />
</FrameLayout>
+9 -9
View File
@@ -5,25 +5,25 @@
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="20dp"
android:paddingTop="10dp"
android:paddingEnd="20dp"
android:paddingBottom="10dp"
android:background="@drawable/shape_dialog"
android:paddingStart="10dp"
android:paddingTop="5dp"
android:paddingEnd="10dp"
android:paddingBottom="5dp"
android:background="@drawable/shape_dialog_small"
tools:ignore="MissingDefaultResource">
<ProgressBar
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_width="26dp"
android:layout_height="26dp"
android:indeterminateTint="@color/white_f6" />
<TextView
android:id="@+id/tvMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginStart="5dp"
android:text="加载中……"
android:textColor="@color/white_f6"
android:textSize="26sp"
android:textSize="14sp"
tools:ignore="HardcodedText" />
</LinearLayout>
@@ -10,7 +10,7 @@
<ImageView
android:id="@+id/ivEmptyIcon"
android:layout_width="250dp"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:src="@drawable/ic_empty_gray"
android:adjustViewBounds="true"
@@ -21,8 +21,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black999"
android:layout_marginTop="50dp"
android:textSize="50sp"
android:layout_marginTop="20dp"
android:textSize="20sp"
android:text="暂无数据"
tools:ignore="HardcodedText" />
</LinearLayout>
@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#DDFFFFFF">
<ImageView
android:id="@+id/ivLightHide"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginVertical="10dp"
android:layout_marginEnd="10dp"
android:layout_gravity="end|center_vertical"
android:src="@drawable/close"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="继电器1"
android:layout_marginStart="50dp"
android:textColor="@color/black"/>
<TextView
android:id="@+id/btnLightOpen"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="开"
android:gravity="center"
android:background="@drawable/bg_init_button"
android:layout_marginStart="50dp"
android:textColor="@color/black"/>
<TextView
android:id="@+id/btnLightClose"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="关"
android:gravity="center"
android:background="@drawable/bg_init_button"
android:layout_marginStart="50dp"
android:textColor="@color/black"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="继电器2"
android:layout_marginStart="50dp"
android:textColor="@color/black"/>
<TextView
android:id="@+id/btnLightOpen2"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="开"
android:gravity="center"
android:background="@drawable/bg_init_button"
android:layout_marginStart="50dp"
android:textColor="@color/black"/>
<TextView
android:id="@+id/btnLightClose2"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="关"
android:gravity="center"
android:background="@drawable/bg_init_button"
android:layout_marginStart="50dp"
android:textColor="@color/black"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="继电器3"
android:layout_marginStart="50dp"
android:textColor="@color/black"/>
<TextView
android:id="@+id/btnLightOpen3"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="开"
android:gravity="center"
android:background="@drawable/bg_init_button"
android:layout_marginStart="50dp"
android:textColor="@color/black"/>
<TextView
android:id="@+id/btnLightClose3"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="关"
android:gravity="center"
android:background="@drawable/bg_init_button"
android:layout_marginStart="50dp"
android:textColor="@color/black"/>
</LinearLayout>
</LinearLayout>
+10 -11
View File
@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"-->
<?xml version="1.0" encoding="utf-8"?><!--<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"-->
<!-- xmlns:tools="http://schemas.android.com/tools"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="80dp"-->
@@ -26,23 +25,23 @@
android:id="@+id/btnFoodName"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_margin="12dp"
android:layout_height="40dp"
android:layout_margin="6dp"
android:ellipsize="end"
android:fontFamily="sans-serif-medium"
android:gravity="center"
android:insetTop="0dp"
android:insetBottom="0dp"
android:maxLines="1"
app:paddingStart="5dp"
app:paddingEnd="5dp"
android:insetRight="0dp"
android:insetLeft="0dp"
android:paddingHorizontal="8dp"
android:textColor="@color/black999"
android:textSize="30sp"
app:cornerRadius="12dp"
app:strokeWidth="2dp"
tools:text="土豆丝土豆丝土豆丝土豆丝土豆丝土豆丝"
/>
android:textSize="15sp"
app:cornerRadius="6dp"
app:strokeWidth="1dp"
tools:text="土豆丝土豆丝土豆丝土豆丝土豆丝土豆丝" />
<!-- android:paddingHorizontal="2dp"-->
<!-- android:paddingLeft="5dp"-->
<!-- android:paddingRight="5dp"-->
+11 -11
View File
@@ -4,11 +4,11 @@
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layoutCard"
android:layout_width="match_parent"
android:layout_height="216dp"
android:layout_margin="12dp"
android:layout_height="140dp"
android:layout_margin="6dp"
app:cardBackgroundColor="@color/bg_card_blue"
app:cardCornerRadius="12dp"
app:cardElevation="0dp"
app:cardCornerRadius="6dp"
app:cardElevation="2dp"
app:rippleColor="#B0BEC5">
<LinearLayout
@@ -16,14 +16,14 @@
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="24dp">
android:padding="12dp">
<TextView
android:id="@+id/tvShelfName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/shelf_name_white"
android:textSize="20sp"
android:textSize="12sp"
android:textStyle="bold"
tools:text="货架-01" />
@@ -39,7 +39,7 @@
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="30sp"
android:textSize="16sp"
android:textStyle="bold"
tools:text="空" />
@@ -55,7 +55,7 @@
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/food_weight_blue"
android:textSize="24sp"
android:textSize="14sp"
android:textStyle="bold"
tools:text="0千克" />
@@ -71,14 +71,14 @@
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/food_weight_blue"
android:textSize="18sp"
android:textSize="12sp"
tools:text="-" />
</LinearLayout>
<ImageView
android:id="@+id/ivStaleFood"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_width="60dp"
android:layout_height="60dp"
android:src="@drawable/ic_stale_food_512"
android:layout_gravity="top|end"
tools:ignore="ContentDescription"
+1 -1
View File
@@ -2,7 +2,7 @@
#agp = "8.11.1"
agp = "8.4.0"
kotlin = "2.0.21"
coreKtx = "1.10.1"
coreKtx = "1.12.0"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"