refactor: 功能按钮全面布局优化 — 均布四周+平行四边形样式+互斥合并

- 起立/趴下合并为切换按钮,移至左上角
- 模式选择器(3个互斥无缝隙)移至左上角最上方
- 步态切换(4个互斥无缝隙)移至模式选择器下方,仅普通模式可点击
- 照明/休眠/充电移至右上角,紧凑排列
- 所有功能按钮使用平行四边形形状(左侧向右倾斜,右侧向左倾斜)
- 底部控制区移除更多展开区域,仅保留摇杆
- 所有控制按钮未连接时灰色不可点击

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
2026-07-18 01:58:11 +08:00
parent df35ff3b05
commit b2adf46957
2 changed files with 307 additions and 194 deletions
@@ -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
)
}
}
// ==================== 辅助函数 ====================