From 4ab44871971048e7c0114af87e4b8d5c0f04125d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=96=E5=8F=81?= Date: Tue, 24 Dec 2024 18:59:12 +0800 Subject: [PATCH] feat: basic control --- app/build.gradle.kts | 7 + .../java/icu/fur93/esp32_car/MainActivity.kt | 92 ++++++--- .../main/java/icu/fur93/esp32_car/Route.kt | 37 +++- .../esp32_car/entity/control/JoystickState.kt | 8 + .../icu/fur93/esp32_car/page/ControlPage.kt | 21 +- .../page/ControlSingleJoystickMode.kt | 180 ++++++++++++++++++ .../icu/fur93/esp32_car/page/SettingsPage.kt | 13 +- .../esp32_car/provider/LocalNavigation.kt | 6 + .../repository/BluetoothRepository.kt | 25 ++- .../ui/carditem/StatusCardJoystickStatus.kt | 8 +- .../ui/component/BottomNavigationBar.kt | 41 ++-- .../fur93/esp32_car/ui/component/Joystick.kt | 35 ++-- .../fur93/esp32_car/ui/component/TipText.kt | 29 +++ .../fur93/esp32_car/viewmodel/CarViewModel.kt | 1 + app/src/main/res/drawable/rotate_left_24.xml | 5 + app/src/main/res/drawable/rotate_right_24.xml | 5 + 16 files changed, 417 insertions(+), 96 deletions(-) create mode 100644 app/src/main/java/icu/fur93/esp32_car/entity/control/JoystickState.kt create mode 100644 app/src/main/java/icu/fur93/esp32_car/provider/LocalNavigation.kt create mode 100644 app/src/main/java/icu/fur93/esp32_car/ui/component/TipText.kt create mode 100644 app/src/main/res/drawable/rotate_left_24.xml create mode 100644 app/src/main/res/drawable/rotate_right_24.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index abc9233..fbcdceb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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" diff --git a/app/src/main/java/icu/fur93/esp32_car/MainActivity.kt b/app/src/main/java/icu/fur93/esp32_car/MainActivity.kt index ae2e600..3a31255 100644 --- a/app/src/main/java/icu/fur93/esp32_car/MainActivity.kt +++ b/app/src/main/java/icu/fur93/esp32_car/MainActivity.kt @@ -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) } } } } - - } \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/Route.kt b/app/src/main/java/icu/fur93/esp32_car/Route.kt index ddc0baf..eb6793e 100644 --- a/app/src/main/java/icu/fur93/esp32_car/Route.kt +++ b/app/src/main/java/icu/fur93/esp32_car/Route.kt @@ -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 = "单向按键模式" + ) } \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/entity/control/JoystickState.kt b/app/src/main/java/icu/fur93/esp32_car/entity/control/JoystickState.kt new file mode 100644 index 0000000..278c5c2 --- /dev/null +++ b/app/src/main/java/icu/fur93/esp32_car/entity/control/JoystickState.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/page/ControlPage.kt b/app/src/main/java/icu/fur93/esp32_car/page/ControlPage.kt index daec293..661a1ff 100644 --- a/app/src/main/java/icu/fur93/esp32_car/page/ControlPage.kt +++ b/app/src/main/java/icu/fur93/esp32_car/page/ControlPage.kt @@ -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 = "陀螺仪模式", diff --git a/app/src/main/java/icu/fur93/esp32_car/page/ControlSingleJoystickMode.kt b/app/src/main/java/icu/fur93/esp32_car/page/ControlSingleJoystickMode.kt index be0d0ad..c42ffdc 100644 --- a/app/src/main/java/icu/fur93/esp32_car/page/ControlSingleJoystickMode.kt +++ b/app/src/main/java/icu/fur93/esp32_car/page/ControlSingleJoystickMode.kt @@ -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 + } + } } \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/page/SettingsPage.kt b/app/src/main/java/icu/fur93/esp32_car/page/SettingsPage.kt index 0d17188..ba99b81 100644 --- a/app/src/main/java/icu/fur93/esp32_car/page/SettingsPage.kt +++ b/app/src/main/java/icu/fur93/esp32_car/page/SettingsPage.kt @@ -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() + ) + }") } } diff --git a/app/src/main/java/icu/fur93/esp32_car/provider/LocalNavigation.kt b/app/src/main/java/icu/fur93/esp32_car/provider/LocalNavigation.kt new file mode 100644 index 0000000..55df21c --- /dev/null +++ b/app/src/main/java/icu/fur93/esp32_car/provider/LocalNavigation.kt @@ -0,0 +1,6 @@ +package icu.fur93.esp32_car.provider + +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.navigation.NavHostController + +val LocalNavigation = staticCompositionLocalOf { error("Not provided") } \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/repository/BluetoothRepository.kt b/app/src/main/java/icu/fur93/esp32_car/repository/BluetoothRepository.kt index 643dfca..eae34b0 100644 --- a/app/src/main/java/icu/fur93/esp32_car/repository/BluetoothRepository.kt +++ b/app/src/main/java/icu/fur93/esp32_car/repository/BluetoothRepository.kt @@ -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) } diff --git a/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StatusCardJoystickStatus.kt b/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StatusCardJoystickStatus.kt index ee87b5e..01e8c42 100644 --- a/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StatusCardJoystickStatus.kt +++ b/app/src/main/java/icu/fur93/esp32_car/ui/carditem/StatusCardJoystickStatus.kt @@ -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)}") } ) } \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/ui/component/BottomNavigationBar.kt b/app/src/main/java/icu/fur93/esp32_car/ui/component/BottomNavigationBar.kt index c6944cb..d8c2419 100644 --- a/app/src/main/java/icu/fur93/esp32_car/ui/component/BottomNavigationBar.kt +++ b/app/src/main/java/icu/fur93/esp32_car/ui/component/BottomNavigationBar.kt @@ -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 ) { - 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 + } } ) } diff --git a/app/src/main/java/icu/fur93/esp32_car/ui/component/Joystick.kt b/app/src/main/java/icu/fur93/esp32_car/ui/component/Joystick.kt index db55b28..8e30449 100644 --- a/app/src/main/java/icu/fur93/esp32_car/ui/component/Joystick.kt +++ b/app/src/main/java/icu/fur93/esp32_car/ui/component/Joystick.kt @@ -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 } diff --git a/app/src/main/java/icu/fur93/esp32_car/ui/component/TipText.kt b/app/src/main/java/icu/fur93/esp32_car/ui/component/TipText.kt new file mode 100644 index 0000000..cb980ad --- /dev/null +++ b/app/src/main/java/icu/fur93/esp32_car/ui/component/TipText.kt @@ -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 + ), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/icu/fur93/esp32_car/viewmodel/CarViewModel.kt b/app/src/main/java/icu/fur93/esp32_car/viewmodel/CarViewModel.kt index 2db6a55..5579f9f 100644 --- a/app/src/main/java/icu/fur93/esp32_car/viewmodel/CarViewModel.kt +++ b/app/src/main/java/icu/fur93/esp32_car/viewmodel/CarViewModel.kt @@ -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 diff --git a/app/src/main/res/drawable/rotate_left_24.xml b/app/src/main/res/drawable/rotate_left_24.xml new file mode 100644 index 0000000..dd651ad --- /dev/null +++ b/app/src/main/res/drawable/rotate_left_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rotate_right_24.xml b/app/src/main/res/drawable/rotate_right_24.xml new file mode 100644 index 0000000..5c8fd52 --- /dev/null +++ b/app/src/main/res/drawable/rotate_right_24.xml @@ -0,0 +1,5 @@ + + + + +