feat: basic control
This commit is contained in:
parent
d0fe216097
commit
4ab4487197
|
@ -18,6 +18,12 @@ android {
|
|||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
|
||||
buildConfigField(
|
||||
type = "String",
|
||||
name = "BUILD_TIME",
|
||||
value = "\"${System.currentTimeMillis()}\""
|
||||
)
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
@ -38,6 +44,7 @@ android {
|
|||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.1"
|
||||
|
|
|
@ -16,12 +16,20 @@ import androidx.activity.viewModels
|
|||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
import androidx.compose.material.icons.automirrored.outlined.Send
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.outlined.Home
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
@ -30,6 +38,7 @@ import androidx.lifecycle.lifecycleScope
|
|||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navigation
|
||||
import icu.fur93.esp32_car.data.PreferencesDataStore
|
||||
import icu.fur93.esp32_car.page.ControlGamepadModePage
|
||||
import icu.fur93.esp32_car.page.ControlPage
|
||||
|
@ -37,8 +46,10 @@ import icu.fur93.esp32_car.page.ControlPathfinderModePage
|
|||
import icu.fur93.esp32_car.page.ControlSingleJoystickModePage
|
||||
import icu.fur93.esp32_car.page.HomePage
|
||||
import icu.fur93.esp32_car.page.SettingsPage
|
||||
import icu.fur93.esp32_car.provider.LocalNavigation
|
||||
import icu.fur93.esp32_car.repository.BluetoothRepositoryImpl
|
||||
import icu.fur93.esp32_car.ui.component.BottomNavigationBar
|
||||
import icu.fur93.esp32_car.ui.component.BottomNavigationItem
|
||||
import icu.fur93.esp32_car.ui.theme.Esp32carTheme
|
||||
import icu.fur93.esp32_car.viewmodel.CarControlUseCase
|
||||
import icu.fur93.esp32_car.viewmodel.CarViewModel
|
||||
|
@ -165,32 +176,65 @@ fun App(viewModel: CarViewModel) {
|
|||
onDispose {}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
BottomNavigationBar(navController)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
) { innerPadding ->
|
||||
Box(Modifier.padding(innerPadding)) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Route.Home.route
|
||||
) {
|
||||
composable(Route.Home.route) { HomePage(viewModel) }
|
||||
composable(Route.Control.route) { ControlPage(viewModel) }
|
||||
composable(Route.Settings.route) { SettingsPage(viewModel) }
|
||||
composable(Route.ControlPathfinderMode.route) {
|
||||
ControlPathfinderModePage(viewModel)
|
||||
CompositionLocalProvider(
|
||||
LocalNavigation provides navController,
|
||||
) {
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
BottomNavigationBar(
|
||||
navController = navController,
|
||||
navItems = listOf(
|
||||
BottomNavigationItem(
|
||||
icon = Icons.Outlined.Home,
|
||||
selectedIcon = Icons.Default.Home,
|
||||
route = Route.Home
|
||||
),
|
||||
BottomNavigationItem(
|
||||
icon = Icons.AutoMirrored.Outlined.Send,
|
||||
selectedIcon = Icons.AutoMirrored.Filled.Send,
|
||||
route = Route.Control
|
||||
),
|
||||
BottomNavigationItem(
|
||||
icon = Icons.Outlined.Settings,
|
||||
selectedIcon = Icons.Default.Settings,
|
||||
route = Route.Settings
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
) { innerPadding ->
|
||||
Box(Modifier.padding(innerPadding)) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Route.Home.route
|
||||
) {
|
||||
// 主导航路由
|
||||
composable(Route.Home.route) { HomePage(viewModel) }
|
||||
composable(Route.Settings.route) { SettingsPage(viewModel) }
|
||||
|
||||
// Control 相关的导航图
|
||||
navigation(
|
||||
startDestination = Route.Control.route,
|
||||
route = "control_graph"
|
||||
) {
|
||||
composable(Route.Control.route) { ControlPage(viewModel) }
|
||||
|
||||
// Control 的子页面
|
||||
composable(Route.ControlPathfinderMode.route) {
|
||||
ControlPathfinderModePage(viewModel)
|
||||
}
|
||||
composable(Route.ControlSingleJoystickMode.route) {
|
||||
ControlSingleJoystickModePage(viewModel)
|
||||
}
|
||||
composable(Route.ControlGamepadMode.route) {
|
||||
ControlGamepadModePage(viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
composable(Route.ControlSingleJoystickMode.route) {
|
||||
ControlSingleJoystickModePage(viewModel)
|
||||
}
|
||||
composable(Route.ControlGamepadMode.route) { ControlGamepadModePage(viewModel) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,10 +1,33 @@
|
|||
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")
|
||||
sealed class Route(
|
||||
val route: String,
|
||||
val title: String = route
|
||||
) {
|
||||
object Home : Route(
|
||||
route = "home",
|
||||
title = "首页"
|
||||
)
|
||||
object Settings : Route(
|
||||
route = "settings",
|
||||
title = "设置"
|
||||
)
|
||||
|
||||
// Control 相关路由
|
||||
object Control : Route(
|
||||
route = "control",
|
||||
title = "控制"
|
||||
)
|
||||
object ControlPathfinderMode : Route(
|
||||
route = "control/pathfinder",
|
||||
title = "循迹模式"
|
||||
)
|
||||
object ControlSingleJoystickMode : Route(
|
||||
route = "control/single_joystick",
|
||||
title = "单摇杆模式"
|
||||
)
|
||||
object ControlGamepadMode : Route(
|
||||
route = "control/gamepad",
|
||||
title = "单向按键模式"
|
||||
)
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package icu.fur93.esp32_car.entity.control
|
||||
|
||||
data class JoystickState(
|
||||
val x: Float = 0f, // -1 到 1
|
||||
val y: Float = 0f, // -1 到 1
|
||||
val angle: Float = 0f, // 角度(弧度)
|
||||
val distance: Float = 0f // 0 到 1
|
||||
)
|
|
@ -13,6 +13,8 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import icu.fur93.esp32_car.R
|
||||
import icu.fur93.esp32_car.Route
|
||||
import icu.fur93.esp32_car.provider.LocalNavigation
|
||||
import icu.fur93.esp32_car.repository.ConnectionState
|
||||
import icu.fur93.esp32_car.ui.card.UnconnectedStatusCard
|
||||
import icu.fur93.esp32_car.ui.component.CardButtonGroup
|
||||
|
@ -26,7 +28,7 @@ fun ControlPage(viewModel: CarViewModel) {
|
|||
val connectionState by viewModel.connectionState.collectAsState()
|
||||
|
||||
Column(LayoutContentModifier) {
|
||||
PageTitle("控制")
|
||||
PageTitle(Route.Control.title)
|
||||
if (connectionState == ConnectionState.CONNECTED) {
|
||||
ControlPageStatusCard()
|
||||
} else {
|
||||
|
@ -49,7 +51,7 @@ fun ControlPageAutoControl() {
|
|||
title = "自动控制",
|
||||
buttons = listOf(
|
||||
CardButtonItem(
|
||||
text = "循迹模式",
|
||||
text = Route.ControlPathfinderMode.title,
|
||||
icon = ImageVector.vectorResource(R.drawable.outline_route_24),
|
||||
onClick = {
|
||||
Toast.makeText(context, "暂未实现", Toast.LENGTH_SHORT).show()
|
||||
|
@ -62,16 +64,23 @@ fun ControlPageAutoControl() {
|
|||
@Composable
|
||||
fun ControlPageManualControl() {
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavigation.current
|
||||
CardButtonGroup(
|
||||
title = "手动控制",
|
||||
buttons = listOf(
|
||||
CardButtonItem(
|
||||
text = "单摇杆模式",
|
||||
icon = ImageVector.vectorResource(R.drawable.outline_joystick_24)
|
||||
text = Route.ControlSingleJoystickMode.title,
|
||||
icon = ImageVector.vectorResource(R.drawable.outline_joystick_24),
|
||||
onClick = {
|
||||
navController.navigate(Route.ControlSingleJoystickMode.route)
|
||||
}
|
||||
),
|
||||
CardButtonItem(
|
||||
text = "单向按键模式",
|
||||
icon = ImageVector.vectorResource(R.drawable.outline_gamepad_24)
|
||||
text = Route.ControlGamepadMode.title,
|
||||
icon = ImageVector.vectorResource(R.drawable.outline_gamepad_24),
|
||||
onClick = {
|
||||
navController.navigate(Route.ControlGamepadMode.route)
|
||||
}
|
||||
),
|
||||
CardButtonItem(
|
||||
text = "陀螺仪模式",
|
||||
|
|
|
@ -1,8 +1,188 @@
|
|||
package icu.fur93.esp32_car.page
|
||||
|
||||
import android.view.MotionEvent
|
||||
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.FilledIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.pointer.pointerInteropFilter
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import icu.fur93.esp32_car.R
|
||||
import icu.fur93.esp32_car.Route
|
||||
import icu.fur93.esp32_car.entity.control.JoystickState
|
||||
import icu.fur93.esp32_car.ui.carditem.StatusCardInfo
|
||||
import icu.fur93.esp32_car.ui.carditem.StatusCardJoystickStatus
|
||||
import icu.fur93.esp32_car.ui.carditem.StatusCardMotorStatus
|
||||
import icu.fur93.esp32_car.ui.component.Joystick
|
||||
import icu.fur93.esp32_car.ui.component.PageTitle
|
||||
import icu.fur93.esp32_car.ui.component.StatusCard
|
||||
import icu.fur93.esp32_car.ui.theme.LayoutContentModifier
|
||||
import icu.fur93.esp32_car.viewmodel.CarViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ControlSingleJoystickModePage(viewModel: CarViewModel) {
|
||||
var joystickState by remember { mutableStateOf(JoystickState()) }
|
||||
|
||||
Column(LayoutContentModifier) {
|
||||
PageTitle(Route.ControlSingleJoystickMode.title)
|
||||
ControlSingleJoystickModeStatusCard(viewModel, joystickState)
|
||||
ControlSingleJoystickModeControlPanel(viewModel) {
|
||||
joystickState = it
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ControlSingleJoystickModeStatusCard(viewModel: CarViewModel, joystickState: JoystickState) {
|
||||
val deviceInfo by viewModel.connectionInfoState.collectAsState()
|
||||
val carState by viewModel.carState.collectAsState()
|
||||
|
||||
StatusCard(
|
||||
cardItems = listOf(
|
||||
StatusCardInfo(deviceInfo),
|
||||
StatusCardMotorStatus(carState),
|
||||
StatusCardJoystickStatus(joystickState)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun ControlSingleJoystickModeControlPanel(
|
||||
viewModel: CarViewModel,
|
||||
onJoystickStateChange: (JoystickState) -> Unit
|
||||
) {
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
FilledIconButton(
|
||||
onClick = {},
|
||||
modifier = Modifier.pointerInteropFilter { event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
viewModel.sendXYR(0, 0, 50)
|
||||
true
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
viewModel.stop()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(ImageVector.vectorResource(R.drawable.rotate_left_24), "逆时针旋转")
|
||||
}
|
||||
|
||||
ControlJoystick(
|
||||
viewModel = viewModel,
|
||||
onJoystickStateChange = onJoystickStateChange
|
||||
)
|
||||
|
||||
FilledIconButton(
|
||||
onClick = {},
|
||||
modifier = Modifier.pointerInteropFilter { event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
viewModel.sendXYR(0, 0, -50)
|
||||
true
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
viewModel.stop()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(ImageVector.vectorResource(R.drawable.rotate_right_24), "顺时针旋转")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ControlJoystick(
|
||||
viewModel: CarViewModel,
|
||||
onJoystickStateChange: (JoystickState) -> Unit
|
||||
) {
|
||||
// 记住上一次发送的状态和时间
|
||||
val lastSentState = remember { mutableStateOf(JoystickState()) }
|
||||
val lastSentTime = remember { mutableStateOf(0L) }
|
||||
|
||||
// 用于停止命令重发的计数器
|
||||
val stopCommandCount = remember { mutableStateOf(0) }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
|
||||
|
||||
Joystick(
|
||||
size = 175.dp,
|
||||
knobScale = 0.25f
|
||||
) { state ->
|
||||
// 更新状态
|
||||
onJoystickStateChange(state)
|
||||
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
// 检查是否是停止状态(摇杆回正)
|
||||
val isStopCommand = state.x == 0f && state.y == 0f
|
||||
|
||||
// 如果是停止命令,启动重发机制
|
||||
// 否则检查时间间隔和状态变化
|
||||
if (isStopCommand) {
|
||||
if (stopCommandCount.value == 0) {
|
||||
// 第一次发送停止命令
|
||||
viewModel.stop()
|
||||
stopCommandCount.value = 1
|
||||
|
||||
// 启动延时重发
|
||||
coroutineScope.launch {
|
||||
delay(50) // 延时50ms
|
||||
viewModel.stop() // 再次发送停止命令
|
||||
stopCommandCount.value = 2
|
||||
|
||||
delay(50) // 再次延时50ms
|
||||
viewModel.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()
|
||||
viewModel.sendXYR(x, y, 0)
|
||||
|
||||
// 更新上次发送的状态和时间
|
||||
lastSentState.value = state
|
||||
lastSentTime.value = currentTime
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,11 +19,13 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.navigation.NavHostController
|
||||
import icu.fur93.esp32_car.BuildConfig
|
||||
import icu.fur93.esp32_car.R
|
||||
import icu.fur93.esp32_car.Route
|
||||
import icu.fur93.esp32_car.repository.BluetoothRepositoryImpl
|
||||
import icu.fur93.esp32_car.repository.ConnectionState
|
||||
import icu.fur93.esp32_car.ui.component.PageTitle
|
||||
import icu.fur93.esp32_car.ui.component.TipText
|
||||
import icu.fur93.esp32_car.ui.dialog.BleDeviceScanDialog
|
||||
import icu.fur93.esp32_car.ui.theme.LayoutContentPadding
|
||||
import icu.fur93.esp32_car.ui.theme.LayoutTopPadding
|
||||
|
@ -39,9 +41,16 @@ fun SettingsPage(viewModel: CarViewModel) {
|
|||
end = LayoutContentPadding
|
||||
)
|
||||
) {
|
||||
PageTitle("设置")
|
||||
PageTitle(Route.Settings.title)
|
||||
}
|
||||
SettingsList(viewModel)
|
||||
TipText("GitHub: colour93/esp32-car-android")
|
||||
TipText("Build: ${
|
||||
android.text.format.DateFormat.format(
|
||||
"yyyy/MM/dd HH:mm:ss",
|
||||
BuildConfig.BUILD_TIME.toLong()
|
||||
)
|
||||
}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package icu.fur93.esp32_car.provider
|
||||
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.navigation.NavHostController
|
||||
|
||||
val LocalNavigation = staticCompositionLocalOf<NavHostController> { error("Not provided") }
|
|
@ -7,12 +7,14 @@ import android.bluetooth.BluetoothDevice.TRANSPORT_LE
|
|||
import android.bluetooth.BluetoothGatt
|
||||
import android.bluetooth.BluetoothGattCallback
|
||||
import android.bluetooth.BluetoothGattCharacteristic
|
||||
import android.bluetooth.BluetoothGattDescriptor
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.bluetooth.BluetoothProfile
|
||||
import android.bluetooth.le.ScanCallback
|
||||
import android.bluetooth.le.ScanResult
|
||||
import android.bluetooth.le.ScanSettings
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import icu.fur93.esp32_car.const.CarCommands
|
||||
import icu.fur93.esp32_car.entity.BleDevice
|
||||
|
@ -189,15 +191,21 @@ class BluetoothRepositoryImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
||||
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
|
||||
super.onServicesDiscovered(gatt, status)
|
||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||
// 找到特定的服务和特征
|
||||
gatt.services?.forEach { service ->
|
||||
service.characteristics?.forEach { characteristic ->
|
||||
// 这里需要根据你的设备具体的UUID来匹配
|
||||
if (characteristic.uuid == UUID.fromString(rxCharUUID)) {
|
||||
rxCharacteristic = characteristic
|
||||
}
|
||||
gatt?.getService(UUID.fromString(serviceUUID))?.let { service ->
|
||||
bluetoothGatt = gatt
|
||||
rxCharacteristic =
|
||||
service.getCharacteristic(UUID.fromString(rxCharUUID))
|
||||
|
||||
// 注册通知监听器
|
||||
val txCharacteristic =
|
||||
service.getCharacteristic(UUID.fromString(txCharUUID))
|
||||
gatt.setCharacteristicNotification(txCharacteristic, true)
|
||||
txCharacteristic.descriptors.forEach { descriptor ->
|
||||
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
||||
gatt.writeDescriptor(descriptor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -208,6 +216,7 @@ class BluetoothRepositoryImpl(
|
|||
characteristic: BluetoothGattCharacteristic,
|
||||
value: ByteArray
|
||||
) {
|
||||
super.onCharacteristicChanged(gatt, characteristic, value)
|
||||
if (characteristic.uuid == UUID.fromString(txCharUUID)) {
|
||||
onReceivePacket(value)
|
||||
}
|
||||
|
|
|
@ -2,17 +2,19 @@ package icu.fur93.esp32_car.ui.carditem
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.runtime.Composable
|
||||
import icu.fur93.esp32_car.entity.control.JoystickState
|
||||
import icu.fur93.esp32_car.ui.component.CardItem
|
||||
import icu.fur93.esp32_car.ui.component.StatusCardItemText
|
||||
import icu.fur93.esp32_car.ui.component.format
|
||||
|
||||
@SuppressLint("ComposableNaming")
|
||||
@Composable
|
||||
fun StatusCardJoystickStatus () : CardItem {
|
||||
fun StatusCardJoystickStatus (joystickState: JoystickState) : CardItem {
|
||||
return CardItem(
|
||||
title = "摇杆状态",
|
||||
content = {
|
||||
StatusCardItemText("X 0.00")
|
||||
StatusCardItemText("Y 0.00")
|
||||
StatusCardItemText("X ${joystickState.x.format(2)}")
|
||||
StatusCardItemText("Y ${joystickState.y.format(2)}")
|
||||
}
|
||||
)
|
||||
}
|
|
@ -17,11 +17,8 @@ import androidx.compose.runtime.collectAsState
|
|||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.navigation.NavHostController
|
||||
import icu.fur93.esp32_car.Route
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
data class BottomNavigationItem(
|
||||
val title: String,
|
||||
val icon: ImageVector,
|
||||
val selectedIcon: ImageVector,
|
||||
val route: Route
|
||||
|
@ -29,29 +26,9 @@ data class BottomNavigationItem(
|
|||
|
||||
@Composable
|
||||
fun BottomNavigationBar(
|
||||
navController: NavHostController
|
||||
navController: NavHostController,
|
||||
navItems: List<BottomNavigationItem>
|
||||
) {
|
||||
val navItems = listOf(
|
||||
BottomNavigationItem(
|
||||
title = "主页",
|
||||
icon = Icons.Outlined.Home,
|
||||
selectedIcon = Icons.Default.Home,
|
||||
route = Route.Home
|
||||
),
|
||||
BottomNavigationItem(
|
||||
title = "控制",
|
||||
icon = Icons.AutoMirrored.Outlined.Send,
|
||||
selectedIcon = Icons.AutoMirrored.Filled.Send,
|
||||
route = Route.Control
|
||||
),
|
||||
BottomNavigationItem(
|
||||
title = "设置",
|
||||
icon = Icons.Outlined.Settings,
|
||||
selectedIcon = Icons.Default.Settings,
|
||||
route = Route.Settings
|
||||
)
|
||||
)
|
||||
|
||||
val currentRoute = navController
|
||||
.currentBackStackEntryFlow
|
||||
.collectAsState(initial = navController.currentBackStackEntry)
|
||||
|
@ -66,13 +43,21 @@ fun BottomNavigationBar(
|
|||
Icon(
|
||||
if ((currentRoute
|
||||
?: "").startsWith(item.route.route)
|
||||
) item.selectedIcon else item.icon, item.title
|
||||
) item.selectedIcon else item.icon, item.route.title
|
||||
)
|
||||
},
|
||||
label = { Text(item.title) },
|
||||
label = { Text(item.route.title) },
|
||||
selected = (currentRoute?:"").startsWith(item.route.route),
|
||||
onClick = {
|
||||
navController.navigate(item.route.route)
|
||||
navController.navigate(item.route.route) {
|
||||
// 弹出到导航图的起始位置
|
||||
popUpTo(item.route.route) { inclusive = true }
|
||||
launchSingleTop = true
|
||||
// 避免创建同一目标的多个副本
|
||||
launchSingleTop = true
|
||||
// 恢复之前的状态
|
||||
restoreState = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,30 +4,24 @@ import androidx.compose.foundation.Canvas
|
|||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import icu.fur93.esp32_car.entity.control.JoystickState
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.hypot
|
||||
import kotlin.math.PI
|
||||
|
||||
// 摇杆数据类
|
||||
data class JoystickState(
|
||||
val x: Float = 0f, // -1 到 1
|
||||
val y: Float = 0f, // -1 到 1
|
||||
val angle: Float = 0f, // 角度(弧度)
|
||||
val distance: Float = 0f // 0 到 1
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun Joystick(
|
||||
modifier: Modifier = Modifier,
|
||||
size: Float = 200f,
|
||||
size: Dp = 200.dp,
|
||||
knobScale: Float = 0.4f,
|
||||
onJoystickMove: (JoystickState) -> Unit
|
||||
) {
|
||||
// 状态管理
|
||||
|
@ -35,23 +29,23 @@ fun Joystick(
|
|||
var stickPosition by remember { mutableStateOf(Offset.Zero) }
|
||||
var isDragging by remember { mutableStateOf(false) }
|
||||
|
||||
// 计算实际尺寸
|
||||
val radius = size / 2
|
||||
val innerRadius = radius * 0.4f
|
||||
|
||||
val surfaceColor = MaterialTheme.colorScheme.surfaceDim
|
||||
val primaryColor = MaterialTheme.colorScheme.primary
|
||||
|
||||
// 创建摇杆
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.size(size.dp)
|
||||
.padding(8.dp)
|
||||
.size(size)
|
||||
.pointerInput(Unit) {
|
||||
// 将 size 转换为像素
|
||||
val sizePx = size.toPx()
|
||||
val radius = sizePx / 2
|
||||
val innerRadius = radius * knobScale
|
||||
|
||||
detectDragGestures(
|
||||
onDragStart = { offset ->
|
||||
isDragging = true
|
||||
centerPoint = Offset(size / 2, size / 2)
|
||||
centerPoint = Offset(sizePx / 2, sizePx / 2)
|
||||
updateStickPosition(offset, centerPoint, radius, innerRadius) { newPos ->
|
||||
stickPosition = newPos
|
||||
val state = calculateJoystickState(centerPoint, newPos, radius)
|
||||
|
@ -74,9 +68,14 @@ fun Joystick(
|
|||
)
|
||||
}
|
||||
) {
|
||||
// 获取实际像素尺寸
|
||||
val sizePx = size.toPx()
|
||||
val radius = sizePx / 2
|
||||
val innerRadius = radius * knobScale
|
||||
|
||||
// 初始化中心点
|
||||
if (centerPoint == Offset.Zero) {
|
||||
centerPoint = Offset(size / 2, size / 2)
|
||||
centerPoint = Offset(sizePx / 2, sizePx / 2)
|
||||
stickPosition = centerPoint
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package icu.fur93.esp32_car.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.unit.sp
|
||||
|
||||
@Composable()
|
||||
fun TipText(
|
||||
text: String
|
||||
) {
|
||||
Row (
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = TextStyle(
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.tertiary
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package icu.fur93.esp32_car.viewmodel
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import icu.fur93.esp32_car.const.CarCommands
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="@android:color/white" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M7.11,8.53L5.7,7.11C4.8,8.27 4.24,9.61 4.07,11h2.02c0.14,-0.87 0.49,-1.72 1.02,-2.47zM6.09,13L4.07,13c0.17,1.39 0.72,2.73 1.62,3.89l1.41,-1.42c-0.52,-0.75 -0.87,-1.59 -1.01,-2.47zM7.1,18.32c1.16,0.9 2.51,1.44 3.9,1.61L11,17.9c-0.87,-0.15 -1.71,-0.49 -2.46,-1.03L7.1,18.32zM13,4.07L13,1L8.45,5.55 13,10L13,6.09c2.84,0.48 5,2.94 5,5.91s-2.16,5.43 -5,5.91v2.02c3.95,-0.49 7,-3.85 7,-7.93s-3.05,-7.44 -7,-7.93z"/>
|
||||
|
||||
</vector>
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="@android:color/white" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M15.55,5.55L11,1v3.07C7.06,4.56 4,7.92 4,12s3.05,7.44 7,7.93v-2.02c-2.84,-0.48 -5,-2.94 -5,-5.91s2.16,-5.43 5,-5.91L11,10l4.55,-4.45zM19.93,11c-0.17,-1.39 -0.72,-2.73 -1.62,-3.89l-1.42,1.42c0.54,0.75 0.88,1.6 1.02,2.47h2.02zM13,17.9v2.02c1.39,-0.17 2.74,-0.71 3.9,-1.61l-1.44,-1.44c-0.75,0.54 -1.59,0.89 -2.46,1.03zM16.89,15.48l1.42,1.41c0.9,-1.16 1.45,-2.5 1.62,-3.89h-2.02c-0.14,0.87 -0.48,1.72 -1.02,2.48z"/>
|
||||
|
||||
</vector>
|
Loading…
Reference in New Issue