refactor: 统一时钟管理并优化状态栏显示逻辑

- 在 BaseActivity 中实现统一的时钟管理机制,避免各子类重复实现
- LogActivity 和 ShelfActivity 覆写 onClockTick() 自定义状态栏显示
- HomeActivity 移除重复的时间更新逻辑,启用右侧状态栏菜单
- 提取协议常量到 ProtocolConstants,提高代码可维护性
- 优化开锁逻辑判断,改为基于 deviceId 的动态判断
- 新增右侧状态栏弹出菜单布局文件

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mazengfei
2026-04-01 17:07:41 +08:00
co-authored by Claude Sonnet 4.6
parent 3e5c7c6caa
commit 133f309d4b
7 changed files with 261 additions and 62 deletions
@@ -43,18 +43,16 @@ import com.shuwei.intelligent.shelves.utils.ext.toast
import com.shuwei.intelligent.shelves.utils.ext.visible
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class HomeActivity : BaseActivity() {
companion object {
private const val MM_DD_EEEE__HH_MM_SS = "MM月dd日 EEEE HH:mm:ss"
}
companion object {}
override val enableRightStatusMenu: Boolean = true
// 货架列表数据
private val list: MutableList<ShelfModel> = mutableListOf()
@@ -68,8 +66,8 @@ class HomeActivity : BaseActivity() {
// 当前湿度
private var showHumidity = "0"
// 是否在点击货架时发送开锁指令;有门锁的部署点(如1楼餐厅)为 true,无门锁的部署点(如401室)为 false
private var isNeedOpenLock = true
// 仅特定 deviceId 的部署点需要发送开锁指令
private fun isNeedOpenLock() = App.deviceId == "4787e213-90ab-3e32-88e0-ac271a937751"
private var deviceName = ""
@@ -304,7 +302,7 @@ class HomeActivity : BaseActivity() {
noDataWarningDialog?.dismiss()
noDataWarningDialog = null
SerialPortManager.close()
runBlocking { SerialPortManager.close() }
saveTaskJob?.cancel()
overdueTaskJob?.cancel()
EventBus.getDefault().unregister(this)
@@ -325,7 +323,7 @@ class HomeActivity : BaseActivity() {
private fun onShelfItemClicked(position: Int) {
shelfIndex = position
lifecycleScope.launch {
if (isNeedOpenLock) {
if (isNeedOpenLock()) {
val openCmd = if (list[position].deviceNo in 1..5)
LEFT_SHELF_OPEN_CMD
else
@@ -374,31 +372,17 @@ class HomeActivity : BaseActivity() {
}
}
private val dateTimeFormatter by lazy { SimpleDateFormat(MM_DD_EEEE__HH_MM_SS, Locale.CHINA) }
private fun updateDateTime() {
updateRightStatus(dateTimeFormatter.format(Date()))
}
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)
// 启动无数据看门狗(每30秒检查一次)
handler.postDelayed(noDataWatchdogTask, 30 * 1000L)
}
override fun onPause() {
super.onPause()
handler.removeCallbacks(updateTask)
// 停止无数据看门狗
handler.removeCallbacks(noDataWatchdogTask)
}
@@ -1,28 +1,53 @@
package com.shuwei.intelligent.shelves.activity
import android.graphics.Color
import android.os.Bundle
import android.view.Gravity
import android.widget.TextView
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import com.shuwei.intelligent.shelves.adapter.LogAdapter
import com.shuwei.intelligent.shelves.base.BaseActivity
import com.shuwei.intelligent.shelves.databinding.ActivityLogListBinding
import com.shuwei.intelligent.shelves.dialog.CommonDialog
import com.shuwei.intelligent.shelves.utils.ext.copyTextToClipboard
import com.shuwei.intelligent.shelves.utils.ext.dp
import com.shuwei.intelligent.shelves.utils.ext.gone
import com.shuwei.intelligent.shelves.utils.ext.startActivity
import com.shuwei.intelligent.shelves.utils.ext.toast
import com.shuwei.intelligent.shelves.serial.ProtocolConstants
import com.shuwei.intelligent.shelves.utils.ext.visible
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class LogActivity : BaseActivity() {
companion object {
const val IS_LOG_DIR = "isLogDir"
const val LOG_DATE = "logDate"
const val YYYY_MM_DD__EEEE_HH_MM_SS = "yyyy年MM月dd日 EEEE***HH:mm:ss"
}
/**
* 覆写时钟回调:左侧状态栏显示年月日星期,右侧状态栏显示时分秒
*/
override fun onClockTick() {
val sdf = SimpleDateFormat(YYYY_MM_DD__EEEE_HH_MM_SS, Locale.CHINA)
val arr = sdf.format(Date()).split("***")
updateLeftStatus(arr[0])
updateRightStatus(arr[1])
}
private lateinit var binding:ActivityLogListBinding
private var isLogDir = false
private var logDate = ""
private val logList = mutableListOf<String>()
private val originalLogList = mutableListOf<String>() // 原始日志备份
private val logAdapter by lazy {
LogAdapter(logList).apply {
setOnItemClickListener { _, _, position ->
@@ -62,24 +87,20 @@ class LogActivity : BaseActivity() {
logAdapter.notifyDataSetChanged()
}
if (isLogDir) {
logList.addAll(fileLogger.getLogFiles())
} else {
logList.addAll(fileLogger.readLogsByName(logDate))
}
binding.rvLogList.let {
it.layoutManager = LinearLayoutManager(this)
it.adapter = logAdapter
}
setupSearch()
setupJumpButton()
binding.btnFind.run {
if (isLogDir) gone() else visible()
}
binding.btnFind.setOnClickListener {
if (isLogDir) {
return@setOnClickListener
}
val list = logList.filter { it.contains("${HomeActivity.HEADER}1001") }
if (isLogDir) return@setOnClickListener
val list = logList.filter { it.contains("${ProtocolConstants.HEADER}${ProtocolConstants.CMD_TEMP_REPORT}") }
if (list.isEmpty()) {
toast("未找到到1001指令")
return@setOnClickListener
@@ -88,6 +109,95 @@ class LogActivity : BaseActivity() {
logList.addAll(list)
logAdapter.notifyDataSetChanged()
}
// 后台加载日志数据
lifecycleScope.launch {
binding.progressBar.visible()
val data = withContext(Dispatchers.IO) {
if (isLogDir) fileLogger.getLogFiles()
else fileLogger.readLogsByName(logDate)
}
logList.addAll(data)
originalLogList.addAll(data)
logAdapter.notifyDataSetChanged()
binding.progressBar.gone()
setupTimeNavigation()
}
}
// 搜索功能
private fun setupSearch() {
binding.etSearch.addTextChangedListener(afterTextChanged = { s ->
val keyword = s?.toString() ?: ""
logList.clear()
logList.addAll(if (keyword.isEmpty()) originalLogList else originalLogList.filter { it.contains(keyword, ignoreCase = true) })
logAdapter.notifyDataSetChanged()
})
}
// 快速跳转功能
private fun setupJumpButton() {
var isAtTop = true
binding.fabJump.setOnClickListener {
if (isAtTop) {
binding.rvLogList.scrollToPosition(logList.size - 1)
} else {
binding.rvLogList.scrollToPosition(0)
}
isAtTop = !isAtTop
}
}
// 时间导航功能
private fun setupTimeNavigation() {
if (isLogDir) {
binding.hsvTimeNav.gone()
return
}
val timeMap = mutableMapOf<String, Int>()
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
originalLogList.forEachIndexed { index, log ->
val timeStr = log.substringAfter("[").substringBefore("]")
try {
val date = dateFormat.parse(timeStr)
date?.let {
val hourKey = SimpleDateFormat("HH:00", Locale.getDefault()).format(it)
if (!timeMap.containsKey(hourKey)) {
timeMap[hourKey] = index
}
}
} catch (e: Exception) {
// 忽略解析失败的日志
}
}
if (timeMap.isEmpty()) {
binding.hsvTimeNav.gone()
return
}
timeMap.toSortedMap().forEach { (hour, position) ->
val btn = TextView(this).apply {
text = hour
textSize = 14f
setTextColor(Color.BLACK)
setPadding(12.dp, 8.dp, 12.dp, 8.dp)
gravity = Gravity.CENTER
setBackgroundColor(Color.parseColor("#E0E0E0"))
setOnClickListener {
binding.rvLogList.scrollToPosition(position)
}
}
val params = android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT,
android.widget.LinearLayout.LayoutParams.MATCH_PARENT
).apply {
marginEnd = 8.dp
}
binding.llTimeNav.addView(btn, params)
}
}
}
@@ -3,8 +3,6 @@ package com.shuwei.intelligent.shelves.activity
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.LinearLayout
import androidx.activity.viewModels
@@ -18,7 +16,7 @@ import com.chad.library.adapter4.util.setOnDebouncedItemClick
import com.scwang.smart.refresh.layout.constant.RefreshState
import com.shuwei.intelligent.shelves.App
import com.shuwei.intelligent.shelves.R
import com.shuwei.intelligent.shelves.activity.HomeActivity.Companion.DEVICE_INFO_CMD
import com.shuwei.intelligent.shelves.serial.ProtocolConstants
import com.shuwei.intelligent.shelves.adapter.SearchAdapter
import com.shuwei.intelligent.shelves.base.BaseActivity
import com.shuwei.intelligent.shelves.databinding.ActivityShelfBinding
@@ -148,7 +146,7 @@ class ShelfActivity : BaseActivity() {
//binding.tvFoodWeight.text = "0克"
EventBus.getDefault().post(ClearShelfEvent(shelfModel!!.deviceNo, tempWeight))
}
sendCmd(DEVICE_INFO_CMD)
sendCmd(ProtocolConstants.DEVICE_INFO_CMD)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.getGoodsListUiState.collect { state ->
@@ -305,31 +303,23 @@ class ShelfActivity : BaseActivity() {
}
}
private fun updateDateTime() {
/**
* 覆写时钟回调:左侧状态栏显示年月日星期,右侧状态栏显示时分秒
*/
override fun onClockTick() {
val sdf = SimpleDateFormat(YYYY_MM_DD__EEEE_HH_MM_SS, Locale.CHINA)
val dateTime = sdf.format(Date())
val arr = dateTime.split("***")
val arr = sdf.format(Date()).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)
}
@SuppressLint("NotifyDataSetChanged")
@@ -371,17 +361,17 @@ class ShelfActivity : BaseActivity() {
val shelfNo = shelfModel?.deviceNo ?: 0
val hex = shelfNo.toString(16).padStart(2, '0').uppercase()
Log.d(TAG, "onCreate: hex=$hex")
val zeroClearingCmd = "${HomeActivity.HEADER}0401${hex}${HomeActivity.FOOTER}"
val zeroClearingCmd = "${ProtocolConstants.HEADER}0401${hex}${ProtocolConstants.FOOTER}"
log("编号${shelfNo}货架执行清零指令:${zeroClearingCmd}")
sendCmd(zeroClearingCmd)
sendCmd(
if (shelfNo in 1..5) HomeActivity.LEFT_SHELF_OPEN_CMD else HomeActivity.RIGHT_SHELF_OPEN_CMD
if (shelfNo in 1..5) ProtocolConstants.LEFT_SHELF_OPEN_CMD else ProtocolConstants.RIGHT_SHELF_OPEN_CMD
)
//清零
realWeight = 0
// binding.tvFoodWeight.text = "${realWeight}克"
sendCmd(DEVICE_INFO_CMD)
sendCmd(ProtocolConstants.DEVICE_INFO_CMD)
startTime = System.currentTimeMillis()
Loading.show(this@ShelfActivity)
// window.decorView.postDelayed({ Loading.dismiss() }, 5000)
@@ -22,6 +22,7 @@ import com.shuwei.intelligent.shelves.serial.ProtocolConstants.LIGHT_OPEN
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.dp
import com.shuwei.intelligent.shelves.utils.ext.startActivity
import kotlinx.coroutines.launch
@@ -29,6 +30,9 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.shuwei.intelligent.shelves.utils.ext.setOnDoubleClickListener
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@Suppress("DEPRECATION")
open class BaseActivity : AppCompatActivity() {
@@ -67,10 +71,13 @@ open class BaseActivity : AppCompatActivity() {
finish()
}
binding.tvRightStatus.setOnDoubleClickListener {
showRightStatusPopup(binding.tvRightStatus)
if (enableRightStatusMenu) showRightStatusPopup(binding.tvRightStatus)
}
}
/** 是否允许右侧状态栏双击弹出菜单,默认关闭,仅 HomeActivity 开启 */
protected open val enableRightStatusMenu: Boolean = false
fun hideStatusBar() {}
fun statusBarDarkFont() {
@@ -107,6 +114,26 @@ open class BaseActivity : AppCompatActivity() {
onUserInactive()
}
// ---- 右侧状态栏时钟 ----
// 默认时间格式:月日星期 时:分:秒
private val CLOCK_FORMAT = "MM月dd日 EEEE HH:mm:ss"
private val clockFormatter by lazy { SimpleDateFormat(CLOCK_FORMAT, Locale.CHINA) }
/**
* 每秒时钟回调,默认将当前时间显示在右侧状态栏。
* 子类可覆写以自定义格式或同时更新左侧状态栏。
*/
open fun onClockTick() {
updateRightStatus(clockFormatter.format(Date()))
}
private val clockTask = object : Runnable {
override fun run() {
onClockTick()
mHandler.postDelayed(this, 1000)
}
}
/**
* 用户超时无操作时回调,默认空实现,子类按需覆写
*/
@@ -128,14 +155,18 @@ open class BaseActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
// 启动定时器
// 启动超时定时器
mHandler.postDelayed(mTimeoutRunnable, TIME_OUT)
// 启动右侧状态栏时钟
mHandler.post(clockTask)
}
override fun onPause() {
super.onPause()
// 取消定时器
// 取消超时定时器
mHandler.removeCallbacks(mTimeoutRunnable)
// 停止右侧状态栏时钟
mHandler.removeCallbacks(clockTask)
}
fun switchLight(isOpen: Boolean) {
@@ -159,8 +190,8 @@ open class BaseActivity : AppCompatActivity() {
val menuBinding = PopupRightStatusMenuBinding.inflate(LayoutInflater.from(this))
val popup = PopupWindow(
menuBinding.root,
android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
120.dp,
101.dp,
true
)
menuBinding.tvEnvSwitch.setOnClickListener {
@@ -69,7 +69,7 @@ object SerialPortManager {
}
// 关闭串口
fun close() {
suspend fun close() {
scope.cancel()
serialPort?.close()
}
+54 -2
View File
@@ -1,6 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
@@ -39,8 +44,55 @@
android:text="查找1001指令"/>
</FrameLayout>
<EditText
android:id="@+id/etSearch"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginHorizontal="12dp"
android:layout_marginTop="8dp"
android:background="@android:color/white"
android:hint="搜索日志..."
android:paddingHorizontal="12dp"
android:textSize="14sp"
android:singleLine="true"
tools:ignore="Autofill,TextFields" />
<HorizontalScrollView
android:id="@+id/hsvTimeNav"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="8dp"
android:scrollbars="none">
<LinearLayout
android:id="@+id/llTimeNav"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:paddingHorizontal="12dp" />
</HorizontalScrollView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvLogList"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabJump"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:src="@android:drawable/ic_menu_sort_by_size"
app:fabSize="normal"
tools:ignore="ContentDescription" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:elevation="8dp"
android:orientation="vertical">
<TextView
android:id="@+id/tv_env_switch"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:text="切换环境"
android:textColor="#333333"
android:textSize="14sp" />
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="#E0E0E0" />
<TextView
android:id="@+id/tv_log"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:text="查看日志"
android:textColor="#333333"
android:textSize="14sp" />
</LinearLayout>