refactor: ui

This commit is contained in:
玖叁 2024-12-23 23:05:43 +08:00
parent dbdcdc7df4
commit 48d4cf4ec0
21 changed files with 844 additions and 302 deletions

View File

@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2024-12-19T11:42:03.594785900Z"> <DropdownSelection timestamp="2024-12-23T14:50:39.475020900Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="Default" identifier="serial=192.168.31.62:42449;connection=c8a0adf4" /> <DeviceId pluginId="PhysicalDevice" identifier="serial=9753497c" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

View File

@ -60,6 +60,7 @@ dependencies {
implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3) implementation(libs.androidx.material3)
implementation(libs.androidx.room.ktx) implementation(libs.androidx.room.ktx)
implementation(libs.androidx.navigation.compose)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)

View File

@ -30,13 +30,27 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import icu.fur93.esp32_car.page.ControlGamepadModePage
import icu.fur93.esp32_car.page.ControlPage
import icu.fur93.esp32_car.page.ControlPathfinderModePage
import icu.fur93.esp32_car.page.ControlSingleJoystickModePage
import icu.fur93.esp32_car.page.GamepadScreen
import icu.fur93.esp32_car.page.HomePage
import icu.fur93.esp32_car.page.SettingsPage
import icu.fur93.esp32_car.ui.component.Joystick import icu.fur93.esp32_car.ui.component.Joystick
import icu.fur93.esp32_car.ui.component.JoystickState import icu.fur93.esp32_car.ui.component.JoystickState
import icu.fur93.esp32_car.ui.theme.Esp32carTheme
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.UUID import java.util.UUID
@ -86,15 +100,17 @@ class MainActivity : ComponentActivity() {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
} }
// 隐藏状态栏和导航栏 // window.statusBarColor = ContextCompat
WindowInsetsControllerCompat(window, window.decorView).let { controller -> //
controller.hide(WindowInsetsCompat.Type.systemBars()) // // 隐藏状态栏和导航栏
controller.systemBarsBehavior = // WindowInsetsControllerCompat(window, window.decorView).let { controller ->
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE // controller.hide(WindowInsetsCompat.Type.systemBars())
} // controller.systemBarsBehavior =
// WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
// 设置横屏 // }
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE //
// // 设置横屏
// requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
// 检查并请求所有必要的权限 // 检查并请求所有必要的权限
val requiredPermissions = arrayOf( val requiredPermissions = arrayOf(
@ -122,24 +138,15 @@ class MainActivity : ComponentActivity() {
} }
setContent { setContent {
GamepadScreen( Esp32carTheme {
deviceList = deviceList, // GamepadScreen(
isConnected = isConnected.value, // deviceList = deviceList,
onStartScan = ::startBleScan, // isConnected = isConnected.value,
onConnect = ::connectToDevice, // onStartScan = ::startBleScan,
carController = carController // onConnect = ::connectToDevice,
) // carController = carController
} // )
} App()
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
// 重新应用全屏设置
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
controller.hide(WindowInsetsCompat.Type.systemBars())
controller.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
} }
} }
} }
@ -306,273 +313,21 @@ class MainActivity : ComponentActivity() {
} }
@Composable @Composable
fun ControlJoystick( fun App() {
modifier: Modifier, val activity = LocalContext.current as MainActivity
carController: CarController, activity.window.statusBarColor = MaterialTheme.colorScheme.background.toArgb()
joystickState: JoystickState,
onJoystickStateChange: (JoystickState) -> Unit
) {
// 记住上一次发送的状态和时间
val lastSentState = remember { mutableStateOf(JoystickState()) }
val lastSentTime = remember { mutableStateOf(0L) }
// 用于停止命令重发的计数器
val stopCommandCount = remember { mutableStateOf(0) }
val coroutineScope = rememberCoroutineScope()
Joystick( val navController = rememberNavController()
modifier = modifier,
size = 350f
) { state ->
// 更新状态
onJoystickStateChange(state)
val currentTime = System.currentTimeMillis()
// 检查是否是停止状态(摇杆回正)
val isStopCommand = state.x == 0f && state.y == 0f
// 如果是停止命令,启动重发机制
// 否则检查时间间隔和状态变化
if (isStopCommand) {
if (stopCommandCount.value == 0) {
// 第一次发送停止命令
carController.stop()
stopCommandCount.value = 1
// 启动延时重发
coroutineScope.launch {
delay(50) // 延时50ms
carController.stop() // 再次发送停止命令
stopCommandCount.value = 2
delay(50) // 再次延时50ms
carController.stop() // 第三次发送停止命令
stopCommandCount.value = 0 // 重置计数器
}
}
} else if ((state.x != lastSentState.value.x || state.y != lastSentState.value.y) &&
currentTime - lastSentTime.value >= 50) {
// 非停止命令的正常处理
stopCommandCount.value = 0 // 重置停止命令计数器
val x = (state.x * 100).toInt()
val y = (state.y * 100).toInt()
carController.sendXYR(x, y, 0)
// 更新上次发送的状态和时间
lastSentState.value = state
lastSentTime.value = currentTime
}
}
}
@Composable NavHost(
fun GamepadScreen( navController = navController,
deviceList: List<ScanResult>, startDestination = Route.Home.route
isConnected: Boolean, ) {
onStartScan: () -> Unit, composable(Route.Home.route) { HomePage(navController) }
onConnect: (ScanResult) -> Unit, composable(Route.Control.route) { ControlPage(navController) }
carController: CarController composable(Route.Settings.route) { SettingsPage(navController) }
) { composable(Route.ControlPathfinderMode.route) { ControlPathfinderModePage(navController) }
var showDeviceList by remember { mutableStateOf(false) } composable(Route.ControlSingleJoystickMode.route) { ControlSingleJoystickModePage(navController) }
var sliderValue by remember { mutableStateOf(0f) } composable(Route.ControlGamepadMode.route) { ControlGamepadModePage(navController) }
var joystickState by remember { mutableStateOf(JoystickState()) }
Box(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier.fillMaxWidth()) {
// 电机状态显示区域
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
// 摇杆状态显示
Column {
Text("摇杆")
Text("X: %.2f".format(joystickState.x))
Text("Y: %.2f".format(joystickState.y))
}
// 电机A状态
Column {
Text("电机A")
Text("PWM: ${carController.carState.value.motorAState.pwm}")
Text("IN1: ${carController.carState.value.motorAState.in1}")
Text("IN2: ${carController.carState.value.motorAState.in2}")
}
// 电机B状态
Column {
Text("电机B")
Text("PWM: ${carController.carState.value.motorBState.pwm}")
Text("IN1: ${carController.carState.value.motorBState.in1}")
Text("IN2: ${carController.carState.value.motorBState.in2}")
}
// 电机C状态
Column {
Text("电机C")
Text("PWM: ${carController.carState.value.motorCState.pwm}")
Text("IN1: ${carController.carState.value.motorCState.in1}")
Text("IN2: ${carController.carState.value.motorCState.in2}")
}
// 电机D状态
Column {
Text("电机D")
Text("PWM: ${carController.carState.value.motorDState.pwm}")
Text("IN1: ${carController.carState.value.motorDState.in1}")
Text("IN2: ${carController.carState.value.motorDState.in2}")
}
// 设备选择按钮
Button(
onClick = { showDeviceList = true },
) {
Text(if (isConnected) "已连接" else "选择设备")
}
}
// 方向控制布局
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
// 左侧控制区
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.weight(1f)
.fillMaxHeight()
) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
ControlJoystick(
modifier = Modifier.padding(16.dp),
carController = carController,
joystickState = joystickState,
onJoystickStateChange = { newState ->
joystickState = newState
}
)
}
}
// 中间 Log 区
// 用于显示发出的蓝牙指令
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(4.dp)
.weight(1f),
reverseLayout = true // 反转布局顺序
) {
items(carController.logs.value.filter { it.direction == LogDirection.SEND }.reversed()) { log ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 2.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "[${SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())
.format(Date(log.timestamp))}]",
style = MaterialTheme.typography.bodySmall
)
Text(
text = log.data,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.weight(1f)
)
}
}
}
// 右侧控制区
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.weight(1f)
) {
// 垂直滑杆
Slider(
value = sliderValue,
onValueChange = { newValue ->
// 在0附近添加吸附效果
val snapValue = when {
newValue > -0.1f && newValue < 0.1f -> 0f
else -> newValue
}
sliderValue = snapValue
val speed = (snapValue * 255).toInt()
if (speed > 0) {
carController.moveForward(speed)
} else if (speed < 0) {
carController.moveBackward(-speed)
} else {
carController.stop()
}
},
valueRange = -1f..1f,
modifier = Modifier
.height(10.dp)
.width(200.dp)
.padding(vertical = 16.dp)
.graphicsLayer(rotationZ = 270f)
)
}
}
}
// 设备列表对话框
if (showDeviceList) {
AlertDialog(
onDismissRequest = { showDeviceList = false },
title = { Text("选择设备") },
text = {
// 设备列表
LazyColumn {
items(deviceList) { device ->
TextButton(
onClick = {
onConnect(device)
showDeviceList = false
},
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start
) {
Text(device.device.name ?: "未知设备")
Text(
device.device.address,
style = MaterialTheme.typography.bodySmall
)
}
}
}
}
},
dismissButton = {
TextButton(onClick = { onStartScan() }) {
Text("扫描设备")
}
},
confirmButton = {
TextButton(onClick = { showDeviceList = false }) {
Text("取消")
}
}
)
}
} }
} }

View File

@ -0,0 +1,10 @@
package icu.fur93.esp32_car
sealed class Route(val route: String) {
data object Home : Route("home")
data object Control : Route("control")
data object Settings : Route("settings")
data object ControlPathfinderMode : Route("control_pathfinder_mode")
data object ControlSingleJoystickMode : Route("control_single_joystick_mode")
data object ControlGamepadMode : Route("control_gamepad_mode")
}

View File

@ -0,0 +1,14 @@
package icu.fur93.esp32_car.page
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import icu.fur93.esp32_car.ui.layout.ControlModeLayout
@Composable
fun ControlGamepadModePage(navController: NavController) {
ControlModeLayout(
content = {
},
onBack = { navController.navigateUp() }
)
}

View File

@ -0,0 +1,14 @@
package icu.fur93.esp32_car.page
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import icu.fur93.esp32_car.ui.layout.ControlModeLayout
@Composable
fun ControlGyroscopeModePage(navController: NavController) {
ControlModeLayout(
content = {
},
onBack = { navController.navigateUp() }
)
}

View File

@ -0,0 +1,21 @@
package icu.fur93.esp32_car.page
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import icu.fur93.esp32_car.Route
import icu.fur93.esp32_car.ui.component.BottomNavigationBar
import icu.fur93.esp32_car.ui.layout.MainLayout
@Composable
fun ControlPage(navController: NavHostController) {
MainLayout(
content = {
},
bottomBar = {
BottomNavigationBar(
currentRoute = Route.Control.route,
navController = navController
)
}
)
}

View File

@ -0,0 +1,14 @@
package icu.fur93.esp32_car.page
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import icu.fur93.esp32_car.ui.layout.ControlModeLayout
@Composable
fun ControlPathfinderModePage(navController: NavController) {
ControlModeLayout(
content = {
},
onBack = { navController.navigateUp() }
)
}

View File

@ -0,0 +1,14 @@
package icu.fur93.esp32_car.page
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
import icu.fur93.esp32_car.ui.layout.ControlModeLayout
@Composable
fun ControlSingleJoystickModePage(navController: NavController) {
ControlModeLayout(
content = {
},
onBack = { navController.navigateUp() }
)
}

View File

@ -0,0 +1,125 @@
package icu.fur93.esp32_car.page
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import icu.fur93.esp32_car.Route
import icu.fur93.esp32_car.ui.component.BottomNavigationBar
import icu.fur93.esp32_car.ui.component.CardItem
import icu.fur93.esp32_car.ui.component.PageTitle
import icu.fur93.esp32_car.ui.component.StatusCard
import icu.fur93.esp32_car.ui.component.StatusCardItemText
import icu.fur93.esp32_car.ui.layout.MainLayout
@Composable
fun HomePage(navController: NavHostController) {
MainLayout(
content = {
PageTitle("标题还没想好")
// HomePageConnectedStatusCard()
HomePageUnconnectedStatusCard()
},
bottomBar = {
BottomNavigationBar(
currentRoute = Route.Home.route,
navController = navController
)
}
)
}
@Composable
fun HomePageConnectedStatusCard() {
StatusCard(
cardItems = listOf(
CardItem(
title = "当前连接",
content = {
StatusCardItemText("名称 Name")
StatusCardItemText("Mac XX:XX:XX:XX:XX:XX")
StatusCardItemText("版本 001")
}
),
CardItem(
title = "电机状态",
content = {
LazyVerticalGrid(
columns = GridCells.Fixed(2)
) {
item {
StatusCardItemText("A 0 1 255")
}
item {
StatusCardItemText("D 0 1 255")
}
item {
StatusCardItemText("B 1 0 255")
}
item {
StatusCardItemText("C 1 0 255")
}
}
}
),
CardItem(
title = "红外模块状态",
content = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
listOf(0, 0, 0, 0, 0).forEach { status ->
StatusCardItemText(status.toString())
}
}
}
),
CardItem(
title = "超声波状态",
content = {
Row(
modifier = Modifier.fillMaxWidth()
) {
Column(Modifier.weight(1f)) {
StatusCardItemText("舵机角度 90")
}
Column(Modifier.weight(1f)) {
StatusCardItemText("舵机角度 20mm")
}
}
}
)
)
)
}
@Composable
fun HomePageUnconnectedStatusCard() {
StatusCard(
cardItems = listOf(
CardItem(
title = "设备未连接",
content = {
StatusCardItemText("请先连接设备")
}
)
),
bottomControl = {
Button(
onClick = {
}
) {
Text("连接设备")
}
}
)
}

View File

@ -0,0 +1,314 @@
package icu.fur93.esp32_car.page
import android.bluetooth.le.ScanResult
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import icu.fur93.esp32_car.CarController
import icu.fur93.esp32_car.LogDirection
import icu.fur93.esp32_car.ui.component.Joystick
import icu.fur93.esp32_car.ui.component.JoystickState
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@Composable
fun ControlJoystick(
modifier: Modifier,
carController: CarController,
joystickState: JoystickState,
onJoystickStateChange: (JoystickState) -> Unit
) {
// 记住上一次发送的状态和时间
val lastSentState = remember { mutableStateOf(JoystickState()) }
val lastSentTime = remember { mutableStateOf(0L) }
// 用于停止命令重发的计数器
val stopCommandCount = remember { mutableStateOf(0) }
val coroutineScope = rememberCoroutineScope()
Joystick(
modifier = modifier,
size = 350f
) { state ->
// 更新状态
onJoystickStateChange(state)
val currentTime = System.currentTimeMillis()
// 检查是否是停止状态(摇杆回正)
val isStopCommand = state.x == 0f && state.y == 0f
// 如果是停止命令,启动重发机制
// 否则检查时间间隔和状态变化
if (isStopCommand) {
if (stopCommandCount.value == 0) {
// 第一次发送停止命令
carController.stop()
stopCommandCount.value = 1
// 启动延时重发
coroutineScope.launch {
delay(50) // 延时50ms
carController.stop() // 再次发送停止命令
stopCommandCount.value = 2
delay(50) // 再次延时50ms
carController.stop() // 第三次发送停止命令
stopCommandCount.value = 0 // 重置计数器
}
}
} else if ((state.x != lastSentState.value.x || state.y != lastSentState.value.y) &&
currentTime - lastSentTime.value >= 50) {
// 非停止命令的正常处理
stopCommandCount.value = 0 // 重置停止命令计数器
val x = (state.x * 100).toInt()
val y = (state.y * 100).toInt()
carController.sendXYR(x, y, 0)
// 更新上次发送的状态和时间
lastSentState.value = state
lastSentTime.value = currentTime
}
}
}
@Composable
fun GamepadScreen(
deviceList: List<ScanResult>,
isConnected: Boolean,
onStartScan: () -> Unit,
onConnect: (ScanResult) -> Unit,
carController: CarController
) {
var showDeviceList by remember { mutableStateOf(false) }
var sliderValue by remember { mutableStateOf(0f) }
var joystickState by remember { mutableStateOf(JoystickState()) }
Box(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier.fillMaxWidth()) {
// 电机状态显示区域
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
// 摇杆状态显示
Column {
Text("摇杆")
Text("X: %.2f".format(joystickState.x))
Text("Y: %.2f".format(joystickState.y))
}
// 电机A状态
Column {
Text("电机A")
Text("PWM: ${carController.carState.value.motorAState.pwm}")
Text("IN1: ${carController.carState.value.motorAState.in1}")
Text("IN2: ${carController.carState.value.motorAState.in2}")
}
// 电机B状态
Column {
Text("电机B")
Text("PWM: ${carController.carState.value.motorBState.pwm}")
Text("IN1: ${carController.carState.value.motorBState.in1}")
Text("IN2: ${carController.carState.value.motorBState.in2}")
}
// 电机C状态
Column {
Text("电机C")
Text("PWM: ${carController.carState.value.motorCState.pwm}")
Text("IN1: ${carController.carState.value.motorCState.in1}")
Text("IN2: ${carController.carState.value.motorCState.in2}")
}
// 电机D状态
Column {
Text("电机D")
Text("PWM: ${carController.carState.value.motorDState.pwm}")
Text("IN1: ${carController.carState.value.motorDState.in1}")
Text("IN2: ${carController.carState.value.motorDState.in2}")
}
// 设备选择按钮
Button(
onClick = { showDeviceList = true },
) {
Text(if (isConnected) "已连接" else "选择设备")
}
}
// 方向控制布局
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
// 左侧控制区
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.weight(1f)
.fillMaxHeight()
) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
ControlJoystick(
modifier = Modifier.padding(16.dp),
carController = carController,
joystickState = joystickState,
onJoystickStateChange = { newState ->
joystickState = newState
}
)
}
}
// 中间 Log 区
// 用于显示发出的蓝牙指令
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(4.dp)
.weight(1f),
reverseLayout = true // 反转布局顺序
) {
items(carController.logs.value.filter { it.direction == LogDirection.SEND }.reversed()) { log ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 2.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "[${
SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())
.format(Date(log.timestamp))}]",
style = MaterialTheme.typography.bodySmall
)
Text(
text = log.data,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.weight(1f)
)
}
}
}
// 右侧控制区
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.weight(1f)
) {
// 垂直滑杆
Slider(
value = sliderValue,
onValueChange = { newValue ->
// 在0附近添加吸附效果
val snapValue = when {
newValue > -0.1f && newValue < 0.1f -> 0f
else -> newValue
}
sliderValue = snapValue
val speed = (snapValue * 255).toInt()
if (speed > 0) {
carController.moveForward(speed)
} else if (speed < 0) {
carController.moveBackward(-speed)
} else {
carController.stop()
}
},
valueRange = -1f..1f,
modifier = Modifier
.height(10.dp)
.width(200.dp)
.padding(vertical = 16.dp)
.graphicsLayer(rotationZ = 270f)
)
}
}
}
// 设备列表对话框
if (showDeviceList) {
AlertDialog(
onDismissRequest = { showDeviceList = false },
title = { Text("选择设备") },
text = {
// 设备列表
LazyColumn {
items(deviceList) { device ->
TextButton(
onClick = {
onConnect(device)
showDeviceList = false
},
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start
) {
Text(device.device.name ?: "未知设备")
Text(
device.device.address,
style = MaterialTheme.typography.bodySmall
)
}
}
}
}
},
dismissButton = {
TextButton(onClick = { onStartScan() }) {
Text("扫描设备")
}
},
confirmButton = {
TextButton(onClick = { showDeviceList = false }) {
Text("取消")
}
}
)
}
}
}

View File

@ -0,0 +1,21 @@
package icu.fur93.esp32_car.page
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import icu.fur93.esp32_car.Route
import icu.fur93.esp32_car.ui.component.BottomNavigationBar
import icu.fur93.esp32_car.ui.layout.MainLayout
@Composable
fun SettingsPage(navController: NavHostController) {
MainLayout(
content = {
},
bottomBar = {
BottomNavigationBar(
currentRoute = Route.Settings.route,
navController = navController
)
}
)
}

View File

@ -0,0 +1,62 @@
package icu.fur93.esp32_car.ui.component
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.navigation.NavHostController
import androidx.navigation.NavOptions
import icu.fur93.esp32_car.Route
data class BottomNavigationItem(
val title: String,
val icon: ImageVector,
val selectedIcon: ImageVector,
val route: Route
)
@Composable
fun BottomNavigationBar(
currentRoute: String,
navController: NavHostController
) {
val navItems = listOf(
BottomNavigationItem(
title = "主页",
icon = Icons.Default.Home,
selectedIcon = Icons.Default.Home,
route = Route.Home
),
BottomNavigationItem(
title = "控制",
icon = Icons.AutoMirrored.Filled.Send,
selectedIcon = Icons.AutoMirrored.Filled.Send,
route = Route.Control
),
BottomNavigationItem(
title = "设置",
icon = Icons.Default.Settings,
selectedIcon = Icons.Default.Settings,
route = Route.Settings
)
)
NavigationBar() {
navItems.forEachIndexed{ _, item ->
NavigationBarItem(
icon = { Icon(if (currentRoute == item.route.route) item.selectedIcon else item.icon, item.title) },
label = { Text(item.title) },
selected = currentRoute == item.route.route,
onClick = {
navController.navigate(item.route.route)
}
)
}
}
}

View File

@ -3,6 +3,7 @@ package icu.fur93.esp32_car.ui.component
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -38,6 +39,9 @@ fun Joystick(
val radius = size / 2 val radius = size / 2
val innerRadius = radius * 0.4f val innerRadius = radius * 0.4f
val surfaceColor = MaterialTheme.colorScheme.surfaceDim
val primaryColor = MaterialTheme.colorScheme.primary
// 创建摇杆 // 创建摇杆
Canvas( Canvas(
modifier = modifier modifier = modifier
@ -77,10 +81,10 @@ fun Joystick(
} }
// 绘制底部圆形 // 绘制底部圆形
drawJoystickBase(centerPoint, radius) drawJoystickBase(centerPoint, radius, surfaceColor)
// 绘制摇杆 // 绘制摇杆
drawJoystickKnob(stickPosition, innerRadius) drawJoystickKnob(stickPosition, innerRadius, primaryColor)
} }
} }
@ -134,18 +138,18 @@ private fun calculateJoystickState(
} }
// 绘制底部圆形 // 绘制底部圆形
private fun DrawScope.drawJoystickBase(center: Offset, radius: Float) { private fun DrawScope.drawJoystickBase(center: Offset, radius: Float, color: Color) {
drawCircle( drawCircle(
color = Color.Gray.copy(alpha = 0.3f), color = color,
radius = radius, radius = radius,
center = center center = center
) )
} }
// 绘制摇杆球 // 绘制摇杆球
private fun DrawScope.drawJoystickKnob(position: Offset, radius: Float) { private fun DrawScope.drawJoystickKnob(position: Offset, radius: Float, color: Color) {
drawCircle( drawCircle(
color = Color.Blue.copy(alpha = 0.5f), color = color,
radius = radius, radius = radius,
center = position center = position
) )

View File

@ -0,0 +1,37 @@
package icu.fur93.esp32_car.ui.component
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import icu.fur93.esp32_car.ui.theme.PageTitleTextStyle
@Composable
fun PageTitle(
text: String,
@SuppressLint("ModifierParameter") modifier: Modifier? = null
) {
if (modifier != null) {
Text(
text = text,
style = PageTitleTextStyle,
modifier = modifier
)
} else {
Text(
text = text,
style = TextStyle(
fontSize = 32.sp,
fontWeight = FontWeight.Black
)
)
Spacer(Modifier.height(24.dp))
}
}

View File

@ -0,0 +1,68 @@
package icu.fur93.esp32_car.ui.component
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import icu.fur93.esp32_car.ui.theme.CardTypography
data class CardItem(
val title: String,
val content: @Composable () -> Unit
)
@Composable
fun StatusCard(
cardItems: List<CardItem>,
bottomControl: @Composable() (() -> Unit?)? = null
) {
Card (Modifier.fillMaxWidth()) {
Column(Modifier.fillMaxWidth().padding(20.dp)) {
cardItems.forEachIndexed { index, item ->
StatusCardTitle(item.title)
Spacer(Modifier.height(2.dp))
item.content()
if (index != cardItems.size - 1) {
Spacer(Modifier.height(16.dp))
}
}
if (bottomControl != null) {
Spacer(Modifier.height(20.dp))
Row (
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
bottomControl()
}
}
}
}
}
@Composable
fun StatusCardTitle (
text: String
) {
Text(
text = text,
style = CardTypography.TitleTextStyle
)
}
@Composable
fun StatusCardItemText (
text: String
) {
Text(
text = text,
style = CardTypography.ItemTextStyle
)
}

View File

@ -0,0 +1,21 @@
package icu.fur93.esp32_car.ui.layout
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun ControlModeLayout(
content: @Composable () -> Unit,
onBack: () -> Unit
) {
Column(
modifier = Modifier.fillMaxSize()
) {
Box(modifier = Modifier.weight(1f)) {
content()
}
}
}

View File

@ -0,0 +1,27 @@
package icu.fur93.esp32_car.ui.layout
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun MainLayout(
content: @Composable () -> Unit,
bottomBar: @Composable () -> Unit
) {
Column(
modifier = Modifier.fillMaxSize()
) {
Scaffold(
bottomBar = bottomBar
) { innerPadding ->
Column (Modifier.padding(innerPadding).padding(22.dp)) {
content()
}
}
}
}

View File

@ -14,7 +14,7 @@ val Typography = Typography(
fontSize = 16.sp, fontSize = 16.sp,
lineHeight = 24.sp, lineHeight = 24.sp,
letterSpacing = 0.5.sp letterSpacing = 0.5.sp
) ),
/* Other default text styles to override /* Other default text styles to override
titleLarge = TextStyle( titleLarge = TextStyle(
fontFamily = FontFamily.Default, fontFamily = FontFamily.Default,
@ -31,4 +31,20 @@ val Typography = Typography(
letterSpacing = 0.5.sp letterSpacing = 0.5.sp
) )
*/ */
)
object CardTypography {
val TitleTextStyle = TextStyle(
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold
)
val ItemTextStyle = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Medium
)
}
val PageTitleTextStyle = TextStyle(
fontSize = 32.sp,
fontWeight = FontWeight.Black
) )

View File

@ -0,0 +1,2 @@
package icu.fur93.esp32_car.viewmodel

View File

@ -9,6 +9,7 @@ lifecycleRuntimeKtx = "2.8.6"
activityCompose = "1.9.3" activityCompose = "1.9.3"
composeBom = "2024.04.01" composeBom = "2024.04.01"
roomKtx = "2.6.1" roomKtx = "2.6.1"
navigationCompose = "2.8.5"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -26,6 +27,7 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "roomKtx" } androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "roomKtx" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }