feat: 首页与应用壳模块

新增:
- StatusBarView 自定义状态栏(圆点+信号条+电池壳,按原型图V3)
- ViewPager2 左右滑动(设置页/主页,默认主页)
- 主页:时钟+日期+快捷区3卡片(对接statisticsNew API)
- 设置页:圆形头像+用户信息+设备信息+调试模式
- TaskApi 接口(统计+考勤)
- HomePagerAdapter(View方式,避免Fragment嵌套)
- 页面指示器+快捷区卡片背景drawable

修改:
- HomeFragment 重写为ViewPager2容器
- NetworkModule 添加TaskApi提供者
- styles.xml 添加ConfigRow/Label/Value样式

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
dongliang
2026-04-27 18:37:06 +09:30
parent e6d5d6fe8a
commit e07a2242a9
17 changed files with 824 additions and 203 deletions

View File

@@ -0,0 +1,14 @@
package com.xiaoqu.watch.data.task
import com.google.gson.annotations.SerializedName
/**
* 考勤状态信息
* 对应 watchTask/myCurrentAttendance API 返回
*/
data class AttendanceInfo(
/** 工作状态 (0=工作中) */
@SerializedName("workStatus") val workStatus: Int = 0,
/** 考勤状态 (3=未上班) */
@SerializedName("workAtStatus") val workAtStatus: Int = 0
)

View File

@@ -0,0 +1,16 @@
package com.xiaoqu.watch.data.task
import com.google.gson.annotations.SerializedName
/**
* 首页任务统计数据
* 对应 watchTask/statisticsNew API 返回
*/
data class TaskStatistics(
/** 接单池(待抢单) */
@SerializedName("waitForTask") val waitForTask: Int = 0,
/** 待打卡 */
@SerializedName("treatTask") val treatTask: Int = 0,
/** 待完成 */
@SerializedName("incompleteTask") val incompleteTask: Int = 0
)

View File

@@ -6,6 +6,7 @@ import com.xiaoqu.watch.network.EnvConfig
import com.xiaoqu.watch.network.SignatureInterceptor import com.xiaoqu.watch.network.SignatureInterceptor
import com.xiaoqu.watch.network.UnbindInterceptor import com.xiaoqu.watch.network.UnbindInterceptor
import com.xiaoqu.watch.network.api.CommonApi import com.xiaoqu.watch.network.api.CommonApi
import com.xiaoqu.watch.network.api.TaskApi
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@@ -62,4 +63,10 @@ object NetworkModule {
fun provideCommonApi(retrofit: Retrofit): CommonApi { fun provideCommonApi(retrofit: Retrofit): CommonApi {
return retrofit.create(CommonApi::class.java) return retrofit.create(CommonApi::class.java)
} }
@Provides
@Singleton
fun provideTaskApi(retrofit: Retrofit): TaskApi {
return retrofit.create(TaskApi::class.java)
}
} }

View File

@@ -0,0 +1,20 @@
package com.xiaoqu.watch.network.api
import com.xiaoqu.watch.data.task.AttendanceInfo
import com.xiaoqu.watch.data.task.TaskStatistics
import com.xiaoqu.watch.network.ApiResponse
import retrofit2.http.GET
/**
* 任务相关 API 接口
*/
interface TaskApi {
/** 首页任务统计(接单池/待打卡/待完成) */
@GET("watchTask/statisticsNew")
suspend fun getStatistics(): ApiResponse<TaskStatistics>
/** 查询当前考勤状态 */
@GET("watchTask/myCurrentAttendance")
suspend fun getAttendance(): ApiResponse<AttendanceInfo>
}

View File

@@ -4,53 +4,58 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.TextView
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.xiaoqu.watch.BuildConfig
import com.xiaoqu.watch.R import com.xiaoqu.watch.R
import com.xiaoqu.watch.data.prefs.DevicePrefs import com.xiaoqu.watch.data.prefs.DevicePrefs
import com.xiaoqu.watch.data.prefs.UserPrefs import com.xiaoqu.watch.data.prefs.UserPrefs
import com.xiaoqu.watch.databinding.FragmentHomeBinding import com.xiaoqu.watch.databinding.FragmentHomeBinding
import com.xiaoqu.watch.device.nfc.NfcController
import com.xiaoqu.watch.device.screen.ScreenController
import com.xiaoqu.watch.device.sensor.VibrationController
import com.xiaoqu.watch.device.sensor.VibrationDefaults
import com.xiaoqu.watch.event.AppEvent import com.xiaoqu.watch.event.AppEvent
import com.xiaoqu.watch.event.EventBus import com.xiaoqu.watch.event.EventBus
import com.xiaoqu.watch.network.ApiResult
import com.xiaoqu.watch.network.api.TaskApi
import com.xiaoqu.watch.network.safeApiCall
import com.xiaoqu.watch.ui.common.BaseFragment import com.xiaoqu.watch.ui.common.BaseFragment
import com.xiaoqu.watch.ui.widget.NavBarHelper import com.xiaoqu.watch.ui.widget.StatusBarView
import com.xiaoqu.watch.ui.widget.QuTipDialog
import com.xiaoqu.watch.util.DateUtil import com.xiaoqu.watch.util.DateUtil
import com.xiaoqu.watch.util.NetworkUtil
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import timber.log.Timber
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
/** /**
* 首页 Fragment * 首页 FragmentViewPager2 容器)
* 当前为硬件验证 demo 页面,展示系统状态 + 硬件控制测试按钮 * Page 0 = 设置页Page 1 = 主页(默认显示)
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class HomeFragment : BaseFragment<FragmentHomeBinding>() { class HomeFragment : BaseFragment<FragmentHomeBinding>() {
@Inject lateinit var devicePrefs: DevicePrefs @Inject lateinit var devicePrefs: DevicePrefs
@Inject lateinit var userPrefs: UserPrefs @Inject lateinit var userPrefs: UserPrefs
@Inject lateinit var screenController: ScreenController
@Inject lateinit var nfcController: NfcController
@Inject lateinit var vibrationController: VibrationController
@Inject lateinit var eventBus: EventBus @Inject lateinit var eventBus: EventBus
@Inject lateinit var taskApi: TaskApi
/** 提示弹窗 */ // ===== 主页 View 引用 =====
private lateinit var tipDialog: QuTipDialog private lateinit var mainStatusBar: StatusBarView
private lateinit var tvClock: TextView
private lateinit var tvDate: TextView
private lateinit var tvPoolNum: TextView
private lateinit var tvPunchNum: TextView
private lateinit var tvCompleteNum: TextView
/** NFC 是否正在扫描 */ // ===== 设置页 View 引用 =====
private var nfcScanning = false private lateinit var configStatusBar: StatusBarView
private lateinit var tvAvatarLetter: TextView
private lateinit var tvUserName: TextView
private lateinit var tvUserPhone: TextView
/** 当前电量和充电状态 */ // ===== 调试模式 =====
private var batteryLevel = -1 private var debugTapCount = 0
private var batteryCharging = false private var lastTapTime = 0L
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeBinding { override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeBinding {
return FragmentHomeBinding.inflate(inflater, container, false) return FragmentHomeBinding.inflate(inflater, container, false)
@@ -59,158 +64,206 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// 设置 NavBar 为首页模式initDevicePrefs 和 MQTT 已在 SplashFragment 完成) // 创建两个页面 View
NavBarHelper.setupHomePage(binding.root) val inflater = LayoutInflater.from(requireContext())
val configPage = inflater.inflate(R.layout.page_config, null)
val mainPage = inflater.inflate(R.layout.page_main, null)
// 初始化弹窗 // 绑定 View 引用
val dialogContainer = requireActivity().findViewById<FrameLayout>(R.id.dialog_container) bindMainViews(mainPage)
tipDialog = QuTipDialog(dialogContainer) bindConfigViews(configPage)
// 显示状态信息 // 设置 ViewPager2
updateStatus() val adapter = HomePagerAdapter(listOf(configPage, mainPage))
binding.viewPager.adapter = adapter
binding.viewPager.setCurrentItem(1, false) // 默认显示主页
// 绑定测试按钮 // 初始化页面数据
setupButtons() initMainPage()
initConfigPage()
// 启动时间更新定时器(每秒刷新 NavBar 时间) // 启动时钟定时器
startTimeUpdater() startClockUpdater()
// 监听系统状态事件 // 加载任务统计数据
observeSystemEvents() fetchStatistics()
// 监听 MQTT 事件
observeEvents()
} }
override fun onDestroyView() { // ===== 主页 =====
super.onDestroyView()
// 停止 NFC 扫描 /** 绑定主页 View 引用 */
if (nfcScanning) { private fun bindMainViews(page: View) {
nfcController.stopScan() mainStatusBar = page.findViewById(R.id.statusBar)
nfcScanning = false tvClock = page.findViewById(R.id.tvClock)
tvDate = page.findViewById(R.id.tvDate)
tvPoolNum = page.findViewById(R.id.tvPoolNum)
tvPunchNum = page.findViewById(R.id.tvPunchNum)
tvCompleteNum = page.findViewById(R.id.tvCompleteNum)
// 快捷区卡片点击 → 跳转任务列表(后续实现)
page.findViewById<View>(R.id.cardPool)?.setOnClickListener {
Timber.d("点击接单池")
// TODO: navigate to TaskListFragment with tableStatus=2
}
page.findViewById<View>(R.id.cardPunch)?.setOnClickListener {
Timber.d("点击待打卡")
// TODO: navigate to TaskListFragment with tableStatus=3
}
page.findViewById<View>(R.id.cardComplete)?.setOnClickListener {
Timber.d("点击待完成")
// TODO: navigate to TaskListFragment with tableStatus=4
} }
} }
/** 更新状态信息显示try-catch 保护,防止系统关机时 DeadSystemException */ /** 初始化主页数据 */
private fun updateStatus() { private fun initMainPage() {
try { updateClock()
val dateInfo = DateUtil.getDateInfo()
val sb = StringBuilder()
sb.appendLine("${dateInfo.date} ${dateInfo.week} ${dateInfo.time}")
sb.appendLine("设备: ${devicePrefs.brand} ${devicePrefs.model}")
sb.appendLine("网络: ${NetworkUtil.getNetworkTypeName(requireContext())}")
sb.appendLine("绑定: ${if (userPrefs.isBound) "是" else "否"}")
// 电量信息(等收到广播后更新)
if (batteryLevel >= 0) {
sb.appendLine("电量: ${batteryLevel}% ${if (batteryCharging) "(充电中)" else ""}")
}
sb.appendLine("屏幕: ${if (screenController.isScreenOn()) "亮" else "灭"}")
sb.appendLine("NFC: ${if (nfcController.isOpen()) "开" else "关"}")
sb.appendLine("MQTT: 已启动")
binding.tvStatus.text = sb.toString()
} catch (e: Exception) {
Timber.w(e, "更新状态信息异常")
}
} }
/** 绑定测试按钮 */ /** 更新时钟和日期 */
private fun setupButtons() { private fun updateClock() {
// 熄屏测试:熄屏 3 秒后自动亮屏 val info = DateUtil.getDateInfo()
binding.btnScreenOff.setOnClickListener { tvClock.text = DateUtil.formatTimeShort()
screenController.turnOff() tvDate.text = "${info.month}${info.day}${info.week}"
binding.root.postDelayed({
screenController.turnOn()
updateStatus()
}, 3000)
}
// 振动测试:执行 planId=4打卡成功方案
binding.btnVibrate.setOnClickListener {
val pattern = VibrationDefaults.getPattern(4)
if (pattern != null) {
vibrationController.executePattern(pattern)
}
}
// NFC 读卡测试:开启/关闭切换
binding.btnNfcScan.setOnClickListener {
if (nfcScanning) {
// 停止扫描
nfcController.stopScan()
nfcController.close()
nfcScanning = false
binding.btnNfcScan.text = "NFC 读卡测试"
updateStatus()
} else {
// 开始扫描
nfcController.open()
nfcController.startScan { nfcId ->
// 读到卡号,显示提示
tipDialog.show(
status = QuTipDialog.Status.SUCCESS,
title = "读到NFC卡",
desc = "卡号: $nfcId",
back = true, step = 0, countdown = 3
)
}
nfcScanning = true
binding.btnNfcScan.text = "停止 NFC 扫描"
updateStatus()
}
}
// 提示弹窗测试
binding.btnShowTip.setOnClickListener {
tipDialog.show(
status = QuTipDialog.Status.SUCCESS,
title = "操作成功",
desc = "这是一个提示弹窗 demo",
back = true, step = 0, countdown = 3
)
}
} }
/** 每秒更新 NavBar 时间显示,跟随 Fragment 生命周期自动取消 */ /** 每秒更新时钟 */
private fun startTimeUpdater() { private fun startClockUpdater() {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
while (isActive) { while (isActive) {
NavBarHelper.updateTime(binding.root) updateClock()
delay(1000) delay(1000)
} }
} }
} }
/** 监听系统状态事件电量、蓝牙<EFBFBD><EFBFBD><EFBFBD>更新 NavBar 和状态显示 */ /** 从 API 获取任务统计数据 */
private fun observeSystemEvents() { private fun fetchStatistics() {
viewLifecycleOwner.lifecycleScope.launch {
val result = safeApiCall { taskApi.getStatistics() }
if (result is ApiResult.Success && result.data != null) {
val data = result.data
tvPoolNum.text = data.waitForTask.toString()
tvPunchNum.text = data.treatTask.toString()
tvCompleteNum.text = data.incompleteTask.toString()
}
}
}
// ===== 设置页 =====
/** 绑定设置页 View 引用 */
private fun bindConfigViews(page: View) {
configStatusBar = page.findViewById(R.id.statusBar)
tvAvatarLetter = page.findViewById(R.id.tvAvatarLetter)
tvUserName = page.findViewById(R.id.tvUserName)
tvUserPhone = page.findViewById(R.id.tvUserPhone)
// 头像点击 → 调试模式检测
page.findViewById<View>(R.id.userBlock)?.setOnClickListener {
checkDebugMode()
}
}
/** 初始化设置页数据 */
private fun initConfigPage() {
// 用户信息
val userName = userPrefs.userName
tvAvatarLetter.text = if (userName.isNotEmpty()) userName.first().toString() else "?"
tvUserName.text = userName
// 手机号脱敏138****8000
val mobile = userPrefs.mobile
tvUserPhone.text = if (mobile.length >= 7) {
"${mobile.substring(0, 3)}****${mobile.substring(mobile.length - 4)}"
} else {
mobile
}
// 设备信息
val configPage = (binding.viewPager.adapter as? HomePagerAdapter)?.let { return@let null }
// 通过 View 直接查找configPage 是 ViewPager2 的第一个子 View
binding.viewPager.post {
val recyclerView = binding.viewPager.getChildAt(0) as? androidx.recyclerview.widget.RecyclerView
val configView = recyclerView?.findViewHolderForAdapterPosition(0)?.itemView
configView?.let { fillDeviceInfo(it) }
}
}
/** 填充设备信息 */
private fun fillDeviceInfo(configView: View) {
configView.findViewById<TextView>(R.id.tvModel)?.text =
"${devicePrefs.brand} ${devicePrefs.model}"
configView.findViewById<TextView>(R.id.tvOsVersion)?.text =
"Android ${devicePrefs.osVersion}"
configView.findViewById<TextView>(R.id.tvImei)?.text = run {
val imei = devicePrefs.imei
if (imei.length > 6) "${imei.substring(0, 3)}***${imei.substring(imei.length - 3)}"
else imei
}
configView.findViewById<TextView>(R.id.tvAppVersion)?.text =
"v${BuildConfig.VERSION_NAME}"
}
/** 调试模式500ms 内连续点击 6 次触发 */
private fun checkDebugMode() {
val now = System.currentTimeMillis()
if (now - lastTapTime > 500) {
debugTapCount = 1
} else {
debugTapCount++
}
lastTapTime = now
if (debugTapCount >= 6) {
debugTapCount = 0
Timber.d("调试模式已开启")
android.widget.Toast.makeText(requireContext(), "调试模式已开启", android.widget.Toast.LENGTH_SHORT).show()
// TODO: 打开调试页面
}
}
// ===== 事件监听 =====
/** 监听 MQTT 和系统状态事件 */
private fun observeEvents() {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
eventBus.events.collect { event -> eventBus.events.collect { event ->
when (event) { when (event) {
// 电量变化:更新 NavBar 电量图标 + 状态文字 // 电量变化:更新两个状态栏
is AppEvent.BatteryChanged -> { is AppEvent.BatteryChanged -> {
batteryLevel = event.level mainStatusBar.updateBattery(event.level, event.isCharging)
batteryCharging = event.isCharging configStatusBar.updateBattery(event.level, event.isCharging)
NavBarHelper.updateBattery(binding.root, event.level, event.isCharging)
updateStatus()
} }
// 蓝牙开关:更新 NavBar 蓝牙图标 // 蓝牙状态变化
is AppEvent.BluetoothStateChanged -> { is AppEvent.BluetoothStateChanged -> {
NavBarHelper.updateBluetooth(binding.root, event.isOn) mainStatusBar.updateBluetooth(event.isOn)
updateStatus() configStatusBar.updateBluetooth(event.isOn)
} }
// 蓝牙连接/断开 // MQTT 消息
is AppEvent.BluetoothDeviceConnected -> updateStatus()
is AppEvent.BluetoothDeviceDisconnected -> updateStatus()
// MQTT 连接/断连/消息
is AppEvent.MqttConnected -> updateStatus()
is AppEvent.MqttDisconnected -> updateStatus()
is AppEvent.MqttMessageReceived -> { is AppEvent.MqttMessageReceived -> {
Timber.d("MQTT消息: type=${event.type}") when (event.type) {
// messageType=3: 解绑 → 清除用户数据,导航到绑定页 0 -> {
if (event.type == 3) { // 新任务消息 → 刷新统计数据
userPrefs.clear() Timber.d("首页: 收到新任务消息")
findMainNavController().navigate(R.id.action_home_to_bind) fetchStatistics()
return@collect }
3 -> {
// 解绑 → 清除数据 → 跳绑定页
Timber.d("首页: 收到解绑消息")
userPrefs.clear()
findNavController().navigate(R.id.action_home_to_bind)
}
4 -> {
// 工作状态变更
Timber.d("首页: 收到工作状态变更")
// TODO: 更新考勤状态,控制蓝牙扫描
}
} }
updateStatus()
} }
else -> {} // 其他事件不处理 else -> {}
} }
} }
} }

View File

@@ -0,0 +1,29 @@
package com.xiaoqu.watch.ui.home
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
/**
* 首页 ViewPager2 适配器
* 两个页面Page 0 = 设置页Page 1 = 主页
* 使用 View非 Fragment避免嵌套导航问题
*/
class HomePagerAdapter(
private val pages: List<View>
) : RecyclerView.Adapter<HomePagerAdapter.PageViewHolder>() {
class PageViewHolder(val view: View) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
return PageViewHolder(pages[viewType])
}
override fun onBindViewHolder(holder: PageViewHolder, position: Int) {
// View 已在创建时设置好,无需额外绑定
}
override fun getItemCount(): Int = pages.size
override fun getItemViewType(position: Int): Int = position
}

View File

@@ -0,0 +1,146 @@
package com.xiaoqu.watch.ui.widget
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import com.xiaoqu.watch.R
/**
* 自定义状态栏 View按原型图 V3 样式)
* 左侧:蓝牙圆点 + 4G 信号条
* 右侧:电池壳 + 填充条
*
* 使用方式:在布局中引入,通过 update* 方法更新状态
*/
class StatusBarView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
// ===== 状态数据 =====
private var bluetoothOn = true
private var signalLevel = 4 // 0-4
private var batteryLevel = 75 // 0-100
private var isCharging = false
// ===== 画笔 =====
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
// ===== 颜色常量 =====
private val colorGreen = context.getColor(R.color.success)
private val colorOrange = context.getColor(R.color.warning)
private val colorRed = context.getColor(R.color.error)
private val colorWhite = 0xFFFFFFFF.toInt()
private val colorDim = 0x26FFFFFF // 白色 15% 透明度
private val colorBorder = 0x59FFFFFF // 白色 35% 透明度
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val h = height.toFloat()
val centerY = h / 2
// ===== 左侧:蓝牙圆点 + 信号条 =====
drawBluetoothDot(canvas, 8f, centerY)
drawSignalBars(canvas, 22f, centerY)
// ===== 右侧:电池 =====
drawBattery(canvas, width - 48f, centerY)
}
/** 绘制蓝牙状态圆点 */
private fun drawBluetoothDot(canvas: Canvas, x: Float, centerY: Float) {
val radius = 5f
paint.style = Paint.Style.FILL
paint.color = if (bluetoothOn) colorGreen else colorDim
canvas.drawCircle(x, centerY, radius, paint)
}
/** 绘制 4G 信号条4 个递增高度的竖条) */
private fun drawSignalBars(canvas: Canvas, startX: Float, centerY: Float) {
val barWidth = 3f
val gap = 2f
val maxHeight = 16f
val heights = floatArrayOf(5f, 8f, 12f, maxHeight)
for (i in 0..3) {
paint.style = Paint.Style.FILL
paint.color = if (i < signalLevel) colorWhite else colorDim
val x = startX + i * (barWidth + gap)
val barH = heights[i]
val top = centerY + maxHeight / 2 - barH
val rect = RectF(x, top, x + barWidth, centerY + maxHeight / 2)
canvas.drawRoundRect(rect, 1.5f, 1.5f, paint)
}
}
/** 绘制电池图标(壳 + 填充条 + 凸起) */
private fun drawBattery(canvas: Canvas, startX: Float, centerY: Float) {
val shellW = 28f
val shellH = 13f
val shellTop = centerY - shellH / 2
val cornerR = 3.5f
// 电池壳边框
paint.style = Paint.Style.STROKE
paint.strokeWidth = 1.5f
paint.color = colorBorder
val shellRect = RectF(startX, shellTop, startX + shellW, shellTop + shellH)
canvas.drawRoundRect(shellRect, cornerR, cornerR, paint)
// 电池填充条
paint.style = Paint.Style.FILL
paint.color = when {
isCharging -> colorGreen
batteryLevel <= 10 -> colorRed
batteryLevel <= 20 -> colorOrange
else -> colorGreen
}
val fillPadding = 2.5f
val fillMaxW = shellW - fillPadding * 2
val fillW = fillMaxW * batteryLevel / 100f
val fillRect = RectF(
startX + fillPadding,
shellTop + fillPadding,
startX + fillPadding + fillW,
shellTop + shellH - fillPadding
)
canvas.drawRoundRect(fillRect, 2f, 2f, paint)
// 电池凸起(右侧小矩形)
paint.color = colorBorder
val nubW = 2.5f
val nubH = 6f
val nubRect = RectF(
startX + shellW + 1f,
centerY - nubH / 2,
startX + shellW + 1f + nubW,
centerY + nubH / 2
)
canvas.drawRoundRect(nubRect, 1f, 1f, paint)
}
// ===== 更新方法 =====
/** 更新蓝牙状态 */
fun updateBluetooth(isOn: Boolean) {
bluetoothOn = isOn
invalidate()
}
/** 更新信号强度 (0-4) */
fun updateSignal(level: Int) {
signalLevel = level.coerceIn(0, 4)
invalidate()
}
/** 更新电池状态 */
fun updateBattery(level: Int, charging: Boolean) {
batteryLevel = level.coerceIn(0, 100)
isCharging = charging
invalidate()
}
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 设置页头像:蓝绿渐变圆形背景 -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<gradient
android:startColor="#FF3B9EFF"
android:endColor="#FF64D2FF"
android:angle="135" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 快捷区卡片背景:蓝色半透明(接单池) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#733B9EFF" />
<corners android:radius="14dp" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 快捷区卡片背景:绿色半透明(待完成) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#734ADE80" />
<corners android:radius="14dp" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 快捷区卡片背景:橙色半透明(待打卡) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#73FFB340" />
<corners android:radius="14dp" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 页面指示器:当前页(白色 85%,圆角矩形) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#D9FFFFFF" />
<corners android:radius="3dp" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 页面指示器:非当前页(白色 15%,圆形) -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#26FFFFFF" />
</shape>

View File

@@ -1,67 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- 首页布局NavBar + 硬件验证 demo --> <!-- 首页容器ViewPager2 左右滑动(设置页 / 主页) -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/background" android:background="@color/background">
android:orientation="vertical">
<!-- 顶部导航栏 --> <androidx.viewpager2.widget.ViewPager2
<include layout="@layout/layout_nav_bar" /> android:id="@+id/viewPager"
<!-- 内容区域 -->
<ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent" />
<LinearLayout </FrameLayout>
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="@dimen/safe_area_left"
android:paddingEnd="@dimen/safe_area_right"
android:paddingBottom="@dimen/safe_area_bottom">
<!-- 系统状态信息 -->
<TextView
android:id="@+id/tvStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text_primary"
android:textSize="@dimen/text_small"
android:lineSpacingExtra="3dp"
android:layout_marginBottom="@dimen/spacing_sm" />
<!-- 屏幕控制测试 -->
<TextView
android:id="@+id/btnScreenOff"
style="@style/ActionButton.Warning"
android:text="熄屏测试3秒后亮屏"
android:layout_marginBottom="@dimen/spacing_sm" />
<!-- 振动测试 -->
<TextView
android:id="@+id/btnVibrate"
style="@style/ActionButton.Primary"
android:text="振动测试"
android:layout_marginBottom="@dimen/spacing_sm" />
<!-- NFC 测试 -->
<TextView
android:id="@+id/btnNfcScan"
style="@style/ActionButton.Success"
android:text="NFC 读卡测试"
android:layout_marginBottom="@dimen/spacing_sm" />
<!-- 弹窗测试 -->
<TextView
android:id="@+id/btnShowTip"
style="@style/ActionButton.Grey"
android:text="提示弹窗测试"
android:layout_marginBottom="@dimen/spacing_sm" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 设置页ViewPager2 Page 0状态栏 + 用户信息 + 设备信息 + 手表信息入口 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background"
android:orientation="vertical"
android:paddingStart="@dimen/safe_area_left"
android:paddingTop="@dimen/safe_area_top"
android:paddingEnd="@dimen/safe_area_right"
android:paddingBottom="@dimen/safe_area_bottom">
<!-- 自定义状态栏 -->
<com.xiaoqu.watch.ui.widget.StatusBarView
android:id="@+id/statusBar"
android:layout_width="match_parent"
android:layout_height="18dp" />
<!-- 可滚动内容区 -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scrollbars="none">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 用户信息区域(居中,可点击触发调试模式) -->
<LinearLayout
android:id="@+id/userBlock"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="10dp">
<!-- 圆形头像(渐变背景 + 首字母) -->
<FrameLayout
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_marginBottom="6dp">
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_avatar" />
<TextView
android:id="@+id/tvAvatarLetter"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textColor="@color/text_primary"
android:textSize="20sp"
android:textStyle="bold" />
</FrameLayout>
<!-- 姓名 -->
<TextView
android:id="@+id/tvUserName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_primary"
android:textSize="15sp"
android:textStyle="bold" />
<!-- 手机号(脱敏) -->
<TextView
android:id="@+id/tvUserPhone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/text_secondary"
android:textSize="11sp"
android:layout_marginTop="2dp" />
</LinearLayout>
<!-- 设备信息列表 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 设备型号 -->
<LinearLayout style="@style/ConfigRow">
<TextView style="@style/ConfigLabel" android:text="设备型号" />
<TextView android:id="@+id/tvModel" style="@style/ConfigValue" />
</LinearLayout>
<!-- 系统版本 -->
<LinearLayout style="@style/ConfigRow">
<TextView style="@style/ConfigLabel" android:text="系统版本" />
<TextView android:id="@+id/tvOsVersion" style="@style/ConfigValue" />
</LinearLayout>
<!-- IMEI -->
<LinearLayout style="@style/ConfigRow">
<TextView style="@style/ConfigLabel" android:text="IMEI" />
<TextView android:id="@+id/tvImei" style="@style/ConfigValue" />
</LinearLayout>
<!-- App 版本 -->
<LinearLayout style="@style/ConfigRow">
<TextView style="@style/ConfigLabel" android:text="App 版本" />
<TextView android:id="@+id/tvAppVersion" style="@style/ConfigValue" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
<!-- 页面指示器 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="8dp">
<View
android:id="@+id/indConfig0"
android:layout_width="14dp"
android:layout_height="5dp"
android:layout_marginEnd="5dp"
android:background="@drawable/indicator_dot_active" />
<View
android:id="@+id/indConfig1"
android:layout_width="5dp"
android:layout_height="5dp"
android:background="@drawable/indicator_dot_inactive" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,176 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 主页ViewPager2 Page 1状态栏 + 时钟 + 日期 + 快捷区 + 指示器
用户群体为老年人,字体尽可能大 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background"
android:orientation="vertical"
android:paddingStart="@dimen/safe_area_left"
android:paddingTop="@dimen/safe_area_top"
android:paddingEnd="@dimen/safe_area_right"
android:paddingBottom="@dimen/safe_area_bottom">
<!-- 自定义状态栏(蓝牙圆点 + 信号条 + 电池壳) -->
<com.xiaoqu.watch.ui.widget.StatusBarView
android:id="@+id/statusBar"
android:layout_width="match_parent"
android:layout_height="18dp"
android:layout_marginBottom="2dp" />
<!-- 时钟区域(居中撑满) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<!-- 时钟 HH:mm -->
<TextView
android:id="@+id/tvClock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="14:30"
android:textColor="@color/text_primary"
android:textSize="48sp"
android:textStyle="bold"
android:letterSpacing="0.05" />
<!-- 日期 + 星期 -->
<TextView
android:id="@+id/tvDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="4月23日 周三"
android:textColor="@color/text_primary"
android:textSize="16sp"
android:layout_marginTop="6dp" />
</LinearLayout>
<!-- 快捷区3 个彩色卡片 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<!-- 接单池(蓝色) -->
<LinearLayout
android:id="@+id/cardPool"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:background="@drawable/bg_quick_blue"
android:gravity="center"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@+id/tvPoolNum"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textColor="@color/primary"
android:textSize="28sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="接单池"
android:textColor="@color/text_primary"
android:textSize="12sp"
android:textStyle="bold"
android:layout_marginTop="4dp" />
</LinearLayout>
<!-- 待打卡(橙色) -->
<LinearLayout
android:id="@+id/cardPunch"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:background="@drawable/bg_quick_orange"
android:gravity="center"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@+id/tvPunchNum"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textColor="@color/warning"
android:textSize="28sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="待打卡"
android:textColor="@color/text_primary"
android:textSize="12sp"
android:textStyle="bold"
android:layout_marginTop="4dp" />
</LinearLayout>
<!-- 待完成(绿色) -->
<LinearLayout
android:id="@+id/cardComplete"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:background="@drawable/bg_quick_green"
android:gravity="center"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@+id/tvCompleteNum"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textColor="@color/success"
android:textSize="28sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="待完成"
android:textColor="@color/text_primary"
android:textSize="12sp"
android:textStyle="bold"
android:layout_marginTop="4dp" />
</LinearLayout>
</LinearLayout>
<!-- 页面指示器 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="8dp">
<View
android:id="@+id/indMain0"
android:layout_width="5dp"
android:layout_height="5dp"
android:layout_marginEnd="5dp"
android:background="@drawable/indicator_dot_inactive" />
<View
android:id="@+id/indMain1"
android:layout_width="14dp"
android:layout_height="5dp"
android:background="@drawable/indicator_dot_active" />
</LinearLayout>
</LinearLayout>

View File

@@ -39,4 +39,34 @@
<style name="ActionButton.Half"> <style name="ActionButton.Half">
<item name="android:layout_width">@dimen/button_half_width</item> <item name="android:layout_width">@dimen/button_half_width</item>
</style> </style>
<!-- ===== 设置页行样式 ===== -->
<!-- 设置页信息行容器 -->
<style name="ConfigRow">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:orientation">horizontal</item>
<item name="android:gravity">center_vertical</item>
<item name="android:paddingTop">8dp</item>
<item name="android:paddingBottom">8dp</item>
</style>
<!-- 设置页标签(左侧) -->
<style name="ConfigLabel">
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_weight">1</item>
<item name="android:textColor">@color/text_secondary</item>
<item name="android:textSize">13sp</item>
</style>
<!-- 设置页值(右侧) -->
<style name="ConfigValue">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textColor">@color/text_primary</item>
<item name="android:textSize">13sp</item>
<item name="android:textStyle">bold</item>
</style>
</resources> </resources>