From b2adf46957b92512fa2c5b9c39afc36e2e121be9 Mon Sep 17 00:00:00 2001 From: CaoWangrenbo Date: Sat, 18 Jul 2026 01:58:11 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=8A=9F=E8=83=BD=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E5=85=A8=E9=9D=A2=E5=B8=83=E5=B1=80=E4=BC=98=E5=8C=96?= =?UTF-8?q?=20=E2=80=94=20=E5=9D=87=E5=B8=83=E5=9B=9B=E5=91=A8+=E5=B9=B3?= =?UTF-8?q?=E8=A1=8C=E5=9B=9B=E8=BE=B9=E5=BD=A2=E6=A0=B7=E5=BC=8F+?= =?UTF-8?q?=E4=BA=92=E6=96=A5=E5=90=88=E5=B9=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 起立/趴下合并为切换按钮,移至左上角 - 模式选择器(3个互斥无缝隙)移至左上角最上方 - 步态切换(4个互斥无缝隙)移至模式选择器下方,仅普通模式可点击 - 照明/休眠/充电移至右上角,紧凑排列 - 所有功能按钮使用平行四边形形状(左侧向右倾斜,右侧向左倾斜) - 底部控制区移除更多展开区域,仅保留摇杆 - 所有控制按钮未连接时灰色不可点击 Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- AGENTS.md | 9 +- .../com/example/m20_gamepad/ui/MainScreen.kt | 492 +++++++++++------- 2 files changed, 307 insertions(+), 194 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a825616..02f7831 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -200,8 +200,15 @@ gradlew installDebug - [x] UI 布局:连接按钮合并到顶部 StatusBar 的 ConnectionIndicator,删除底部独立连接按钮 - [x] UI 布局:ConnectionIndicator 改为按钮形态(Surface + 圆角边框),仅指示器区域可点击切换连接 - [x] UI 布局:隐藏系统状态栏(WindowInsetsControllerCompat),压缩自定义 StatusBar 高度,扩展可视区域 +- [x] UI 布局:功能按钮均布在四周——左上角模式选择+步态切换+起立/趴下,右上角灯光+休眠+充电,充分利用屏幕空间,优化单手操作体验 +- [x] UI 布局:起立/趴下合并为切换按钮(琥珀色=站立时显示"趴下",暗色=非站立时显示"起立"),移至左上角 StatusBar 下方 +- [x] UI 布局:模式选择器(常规/导航/辅助,3个互斥按钮,无缝隙,底色显示当前模式)移至左上角,位于起立/趴上方 +- [x] UI 布局:步态切换(基础/楼梯/平地敏捷/楼梯敏捷,4个互斥按钮,无缝隙)移至模式选择器下方,始终显示,仅普通模式+RL控制时可点击 +- [x] UI 布局:照明/休眠/充电移至右上角,紧凑排列(前灯后灯无缝隙一行,休眠/唤醒切换,充电/充电中切换) +- [x] UI 布局:底部控制区移除"更多"展开区域,仅保留左右摇杆 +- [x] UI 布局:所有功能按钮统一使用平行四边形形状(左侧按钮斜边左下→右上,右侧按钮斜边左上→右下) +- [x] UI 布局:所有控制按钮在未连接时灰色不可点击 ### 待办 -- [ ] UI 布局:功能按钮均布在四周,充分利用屏幕空间,优化单手操作体验 - [ ] 视频进一步延迟优化:探索更激进的低延迟策略(如降低帧率、缩小分辨率、调整 ExoPlayer 缓冲策略等) diff --git a/app/src/main/java/com/example/m20_gamepad/ui/MainScreen.kt b/app/src/main/java/com/example/m20_gamepad/ui/MainScreen.kt index 84f7f42..1c52044 100644 --- a/app/src/main/java/com/example/m20_gamepad/ui/MainScreen.kt +++ b/app/src/main/java/com/example/m20_gamepad/ui/MainScreen.kt @@ -17,10 +17,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Settings @@ -40,12 +38,19 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel @@ -66,6 +71,38 @@ import com.example.m20_gamepad.video.RtspVideoPlayer private val OverlayBg = Color(0x88000000) private val OverlayText = Color.White +/** 平行四边形形状,用于功能按钮。skewLeft=true → 左下-右上斜边(左侧按钮); + * skewLeft=false → 左上-右下斜边(右侧按钮)。 */ +private class ParallelogramShape( + private val skewDp: Dp = 10.dp, + private val skewLeft: Boolean = true +) : Shape { + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + val skewPx = with(density) { skewDp.toPx() } + val path = Path().apply { + if (skewLeft) { + // 左下 → 右上 斜边(平行四边形向右倾斜) + moveTo(skewPx, 0f) + lineTo(size.width, 0f) + lineTo(size.width - skewPx, size.height) + lineTo(0f, size.height) + } else { + // 左上 → 右下 斜边(平行四边形向左倾斜) + moveTo(0f, 0f) + lineTo(size.width - skewPx, 0f) + lineTo(size.width, size.height) + lineTo(skewPx, size.height) + } + close() + } + return Outline.Generic(path) + } +} + /** * 主界面:RTSP 视频背景 + 双摇杆覆盖层 + 状态栏 + 控制按钮。 * @@ -144,12 +181,89 @@ fun MainScreen( modifier = Modifier.align(Alignment.TopCenter) ) + // ---- 左上角控制区:模式选择 + 步态切换 + 起立/趴下 ---- + Column( + modifier = Modifier + .align(Alignment.TopStart) + .padding(top = 56.dp, start = 12.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + ModeSelector( + currentMode = status?.basicStatus?.controlUsageMode ?: 0, + connected = connectionState == ProtocolClient.State.CONNECTED, + onSelectMode = { mode -> viewModel.sendSwitchMode(mode) } + ) + // 步态切换——始终显示,仅普通模式 + RL 控制时可点击 + GaitSelector( + currentGait = status?.basicStatus?.gait ?: 0, + enabled = connectionState == ProtocolClient.State.CONNECTED && + motionState == ControlCommands.MOTION_RL_CONTROL && + status?.basicStatus?.controlUsageMode == ControlCommands.MODE_NORMAL, + onSelectGait = { gait -> viewModel.sendSwitchGait(gait) } + ) + StandToggleButton( + motionState = motionState, + connected = connectionState == ProtocolClient.State.CONNECTED, + onToggle = { + if (motionState == ControlCommands.MOTION_IDLE || + motionState == ControlCommands.MOTION_LIE_DOWN + ) { + viewModel.sendMotionControl(ControlCommands.MOTION_STAND) + } else { + viewModel.sendMotionControl(ControlCommands.MOTION_LIE_DOWN) + } + } + ) + } + + // ---- 右上角控制区:照明 + 休眠 + 充电 ---- + Column( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 56.dp, end = 12.dp), + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + // 照明:前灯+后灯,无缝隙 + val frontOn = ledStatus?.front == 1 + val backOn = ledStatus?.back == 1 + Row(horizontalArrangement = Arrangement.spacedBy(0.dp)) { + LightToggleButton( + text = "前灯", + isOn = frontOn, + enabled = connectionState == ProtocolClient.State.CONNECTED + ) { + viewModel.sendLightControl(if (frontOn) 0 else 1, ledStatus?.back ?: 0) + } + LightToggleButton( + text = "后灯", + isOn = backOn, + enabled = connectionState == ProtocolClient.State.CONNECTED + ) { + viewModel.sendLightControl(ledStatus?.front ?: 0, if (backOn) 0 else 1) + } + } + // 休眠/唤醒 + val isSleeping = status?.basicStatus?.sleep == 1 + SleepToggleButton( + isSleeping = isSleeping, + connected = connectionState == ProtocolClient.State.CONNECTED, + onToggle = { viewModel.sendSleepSettings(!isSleeping) } + ) + // 充电/结束充电 + val isCharging = status?.basicStatus?.charge == 1 + ChargeToggleButton( + isCharging = isCharging, + connected = connectionState == ProtocolClient.State.CONNECTED, + onToggle = { viewModel.sendChargeControl(if (isCharging) 0 else 1) } + ) + } + // ---- 底部控制区 ---- BottomControls( viewModel = viewModel, connectionState = connectionState, motionState = motionState, - ledStatus = ledStatus, modifier = Modifier .align(Alignment.BottomCenter) .windowInsetsPadding(WindowInsets.navigationBars) @@ -336,7 +450,6 @@ private fun BottomControls( viewModel: MainViewModel, connectionState: ProtocolClient.State, motionState: Int, - ledStatus: LedStatus?, modifier: Modifier = Modifier ) { Row( @@ -357,14 +470,6 @@ private fun BottomControls( } ) - // 中间控制按钮组 - ControlButtons( - viewModel = viewModel, - connectionState = connectionState, - motionState = motionState, - ledStatus = ledStatus - ) - // 右摇杆(Yaw 旋转) Joystick( modifier = Modifier.size(180.dp), @@ -378,170 +483,6 @@ private fun BottomControls( } } -@Composable -private fun ControlButtons( - viewModel: MainViewModel, - connectionState: ProtocolClient.State, - motionState: Int, - ledStatus: LedStatus? -) { - val connected = connectionState == ProtocolClient.State.CONNECTED - val isRlControl = motionState == ControlCommands.MOTION_RL_CONTROL - var showMore by remember { mutableStateOf(false) } - val scrollState = rememberScrollState() - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(6.dp), - modifier = Modifier - .verticalScroll(scrollState) - .padding(top = 4.dp, bottom = 4.dp) - ) { - // 运动状态转换 - ControlButton( - "起立", - enabled = connected && (motionState == ControlCommands.MOTION_IDLE || - motionState == ControlCommands.MOTION_LIE_DOWN) - ) { - viewModel.sendMotionControl(ControlCommands.MOTION_STAND) - } - ControlButton( - "RL控制", - enabled = connected && motionState == ControlCommands.MOTION_STAND - ) { - viewModel.sendMotionControl(ControlCommands.MOTION_RL_CONTROL) - } - ControlButton( - "趴下", - enabled = connected && motionState != ControlCommands.MOTION_LIE_DOWN - ) { - viewModel.sendMotionControl(ControlCommands.MOTION_LIE_DOWN) - } - - // 步态切换——仅 RL 控制 - if (isRlControl) { - Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { - MiniButton("基础", enabled = isRlControl) { - viewModel.sendSwitchGait(ControlCommands.GAIT_BASIC) - } - MiniButton("楼梯", enabled = isRlControl) { - viewModel.sendSwitchGait(ControlCommands.GAIT_STAIRS) - } - MiniButton("平地敏捷", enabled = isRlControl) { - viewModel.sendSwitchGait(ControlCommands.GAIT_FLAT_AGILE) - } - } - MiniButton("楼梯敏捷", enabled = isRlControl) { - viewModel.sendSwitchGait(ControlCommands.GAIT_STAIRS_AGILE) - } - } - - // 更多/收起切换 - if (connected) { - ControlButton(if (showMore) "收起" else "更多", enabled = connected) { - showMore = !showMore - } - } - - // ---- 更多控制(可展开) ---- - if (showMore) { - Separator() - - // 使用模式 - SectionLabel("使用模式") - Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { - MiniButton("常规") { - viewModel.sendSwitchMode(ControlCommands.MODE_NORMAL) - } - MiniButton("导航") { - viewModel.sendSwitchMode(ControlCommands.MODE_NAVIGATION) - } - MiniButton("辅助") { - viewModel.sendSwitchMode(ControlCommands.MODE_ASSIST) - } - } - Spacer(Modifier.height(4.dp)) - - // 照明 - SectionLabel("照明") - val frontOn = ledStatus?.front == 1 - val backOn = ledStatus?.back == 1 - Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { - LightToggleButton( - text = "前灯", - isOn = frontOn, - enabled = connected - ) { - viewModel.sendLightControl(if (frontOn) 0 else 1, ledStatus?.back ?: 0) - } - LightToggleButton( - text = "后灯", - isOn = backOn, - enabled = connected - ) { - viewModel.sendLightControl(ledStatus?.front ?: 0, if (backOn) 0 else 1) - } - } - Spacer(Modifier.height(4.dp)) - - // 充电 - SectionLabel("充电") - Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { - MiniButton("开始充电") { viewModel.sendChargeControl(1) } - MiniButton("结束充电") { viewModel.sendChargeControl(0) } - } - Spacer(Modifier.height(4.dp)) - - // 休眠 - SectionLabel("休眠") - Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { - MiniButton("进入休眠") { viewModel.sendSleepSettings(true) } - MiniButton("唤醒") { viewModel.sendSleepSettings(false) } - } - } - } -} - -@Composable -private fun ControlButton( - text: String, - enabled: Boolean = true, - onClick: () -> Unit -) { - Button( - onClick = onClick, - enabled = enabled, - colors = ButtonDefaults.buttonColors( - containerColor = Color(0x66333333), - disabledContainerColor = Color(0x33222222) - ), - modifier = Modifier.width(110.dp) - ) { - Text(text, color = if (enabled) OverlayText else Color(0x88FFFFFF), fontWeight = FontWeight.Medium) - } -} - -@Composable -private fun MiniButton(text: String, enabled: Boolean = true, onClick: () -> Unit) { - Button( - onClick = onClick, - enabled = enabled, - colors = ButtonDefaults.buttonColors( - containerColor = Color(0x66333333), - disabledContainerColor = Color(0x33222222) - ), - contentPadding = ButtonDefaults.TextButtonContentPadding, - modifier = Modifier.height(32.dp) - ) { - Text( - text, - color = if (enabled) OverlayText else Color(0x88FFFFFF), - fontSize = 12.sp, - fontWeight = FontWeight.Normal - ) - } -} - @Composable private fun LightToggleButton( text: String, @@ -552,40 +493,205 @@ private fun LightToggleButton( Button( onClick = onClick, enabled = enabled, + shape = ParallelogramShape(skewLeft = false), colors = ButtonDefaults.buttonColors( containerColor = if (isOn) Color(0xCCFFC107) else Color(0x66333333), disabledContainerColor = Color(0x33222222) ), - modifier = Modifier.width(110.dp) + contentPadding = ButtonDefaults.TextButtonContentPadding, + modifier = Modifier.height(32.dp) ) { Text( "$text ${if (isOn) "开" else "关"}", color = if (enabled) OverlayText else Color(0x88FFFFFF), + fontSize = 12.sp, fontWeight = FontWeight.Medium ) } } @Composable -private fun SectionLabel(text: String) { - Text( - text, - color = Color(0xFFB0B0B0), - fontSize = 11.sp, - modifier = Modifier.fillMaxWidth() - ) +private fun StandToggleButton( + motionState: Int, + connected: Boolean, + onToggle: () -> Unit, + modifier: Modifier = Modifier +) { + val isStanding = motionState == ControlCommands.MOTION_STAND || + motionState == ControlCommands.MOTION_RL_CONTROL + Button( + onClick = onToggle, + enabled = connected, + shape = ParallelogramShape(skewLeft = true), + colors = ButtonDefaults.buttonColors( + containerColor = if (isStanding) Color(0xCCFFC107) else Color(0x66333333), + disabledContainerColor = Color(0x33222222) + ), + contentPadding = ButtonDefaults.TextButtonContentPadding, + modifier = modifier.height(32.dp) + ) { + Text( + text = if (isStanding) "趴下" else "起立", + color = if (connected) OverlayText else Color(0x88FFFFFF), + fontSize = 12.sp, + fontWeight = FontWeight.Medium + ) + } +} + +// ==================== 模式选择器(3个互斥无缝隙) ==================== + +@Composable +private fun ModeSelector( + currentMode: Int, + connected: Boolean, + onSelectMode: (Int) -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + ModeButton("常规", ControlCommands.MODE_NORMAL, currentMode, connected, onSelectMode) + ModeButton("导航", ControlCommands.MODE_NAVIGATION, currentMode, connected, onSelectMode) + ModeButton("辅助", ControlCommands.MODE_ASSIST, currentMode, connected, onSelectMode) + } } @Composable -private fun Separator() { - Spacer(Modifier.height(2.dp)) - Box( - modifier = Modifier - .width(80.dp) - .height(1.dp) - .background(Color(0x44FFFFFF)) - ) - Spacer(Modifier.height(2.dp)) +private fun ModeButton( + text: String, + mode: Int, + currentMode: Int, + connected: Boolean, + onClick: (Int) -> Unit +) { + val isActive = currentMode == mode + Button( + onClick = { onClick(mode) }, + enabled = connected, + shape = ParallelogramShape(skewLeft = true), + colors = ButtonDefaults.buttonColors( + containerColor = if (isActive) Color(0xCCFFC107) else Color(0x66333333), + disabledContainerColor = Color(0x33222222) + ), + contentPadding = ButtonDefaults.TextButtonContentPadding, + modifier = Modifier.height(32.dp) + ) { + Text( + text, + color = if (connected) OverlayText else Color(0x88FFFFFF), + fontSize = 12.sp, + fontWeight = if (isActive) FontWeight.Bold else FontWeight.Normal + ) + } +} + +// ==================== 步态切换(4个互斥无缝隙,仅普通模式可用) ==================== + +@Composable +private fun GaitSelector( + currentGait: Int, + enabled: Boolean, + onSelectGait: (Int) -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + GaitButton("基础", ControlCommands.GAIT_BASIC, currentGait, enabled, onSelectGait) + GaitButton("楼梯", ControlCommands.GAIT_STAIRS, currentGait, enabled, onSelectGait) + GaitButton("平地敏捷", ControlCommands.GAIT_FLAT_AGILE, currentGait, enabled, onSelectGait) + GaitButton("楼梯敏捷", ControlCommands.GAIT_STAIRS_AGILE, currentGait, enabled, onSelectGait) + } +} + +@Composable +private fun GaitButton( + text: String, + gait: Int, + currentGait: Int, + enabled: Boolean, + onClick: (Int) -> Unit +) { + val isActive = currentGait == gait + Button( + onClick = { onClick(gait) }, + enabled = enabled, + shape = ParallelogramShape(skewLeft = true), + colors = ButtonDefaults.buttonColors( + containerColor = if (isActive) Color(0xCCFFC107) else Color(0x66333333), + disabledContainerColor = Color(0x33222222) + ), + contentPadding = ButtonDefaults.TextButtonContentPadding, + modifier = Modifier.height(32.dp) + ) { + Text( + text, + color = if (enabled) OverlayText else Color(0x88FFFFFF), + fontSize = 12.sp, + fontWeight = if (isActive) FontWeight.Bold else FontWeight.Normal + ) + } +} + +// ==================== 休眠/唤醒切换 ==================== + +@Composable +private fun SleepToggleButton( + isSleeping: Boolean, + connected: Boolean, + onToggle: () -> Unit, + modifier: Modifier = Modifier +) { + Button( + onClick = onToggle, + enabled = connected, + shape = ParallelogramShape(skewLeft = false), + colors = ButtonDefaults.buttonColors( + containerColor = if (isSleeping) Color(0xCCFFC107) else Color(0x66333333), + disabledContainerColor = Color(0x33222222) + ), + contentPadding = ButtonDefaults.TextButtonContentPadding, + modifier = modifier.height(32.dp) + ) { + Text( + text = if (isSleeping) "唤醒" else "休眠", + color = if (connected) OverlayText else Color(0x88FFFFFF), + fontSize = 12.sp, + fontWeight = FontWeight.Medium + ) + } +} + +// ==================== 充电/结束充电切换 ==================== + +@Composable +private fun ChargeToggleButton( + isCharging: Boolean, + connected: Boolean, + onToggle: () -> Unit, + modifier: Modifier = Modifier +) { + Button( + onClick = onToggle, + enabled = connected, + shape = ParallelogramShape(skewLeft = false), + colors = ButtonDefaults.buttonColors( + containerColor = if (isCharging) Color(0xCCFFC107) else Color(0x66333333), + disabledContainerColor = Color(0x33222222) + ), + contentPadding = ButtonDefaults.TextButtonContentPadding, + modifier = modifier.height(32.dp) + ) { + Text( + text = if (isCharging) "充电中" else "充电", + color = if (connected) OverlayText else Color(0x88FFFFFF), + fontSize = 12.sp, + fontWeight = FontWeight.Medium + ) + } } // ==================== 辅助函数 ====================