feat: 底图bg1.png+开关灯合并按键+默认IP更新
- 添加 bg1.png 作为底层背景图,视频未播放时透出 - PlayerView 仅在视频播放中时渲染,避免黑色 SurfaceView 遮挡底图 - 开关灯合并为前灯/后灯两个按键,依据 LED 回报状态显示琥珀色/暗色 - 默认 IP 改为 10.21.41.1,RTSP 地址改为 rtsp://10.21.41.1:8554/video1 Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
@@ -28,9 +28,9 @@ data class AppSettings(
|
||||
val videoResizeMode: VideoResizeMode = VideoResizeMode.ZOOM
|
||||
) {
|
||||
companion object {
|
||||
const val DEFAULT_ROBOT_HOST = "10.21.31.103"
|
||||
const val DEFAULT_ROBOT_HOST = "10.21.41.1"
|
||||
const val DEFAULT_ROBOT_PORT = 30000
|
||||
const val DEFAULT_RTSP_URL = "rtsp://10.21.31.103:554/stream"
|
||||
const val DEFAULT_RTSP_URL = "rtsp://10.21.41.1:8554/video1"
|
||||
|
||||
/** RTSP 端口范围校验 */
|
||||
fun isValidPort(port: Int): Boolean = port in 1..65535
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.example.m20_gamepad.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -40,6 +41,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -49,9 +52,11 @@ import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import com.erz.joysticklibrary.Joystick
|
||||
import com.erz.joysticklibrary.JoystickType
|
||||
import com.example.m20_gamepad.R
|
||||
import com.example.m20_gamepad.data.SettingsRepository
|
||||
import com.example.m20_gamepad.network.ProtocolClient
|
||||
import com.example.m20_gamepad.network.models.ControlCommands
|
||||
import com.example.m20_gamepad.network.models.LedStatus
|
||||
import com.example.m20_gamepad.ui.components.StatusPanel
|
||||
import com.example.m20_gamepad.video.RtspVideoPlayer
|
||||
|
||||
@@ -83,6 +88,7 @@ fun MainScreen(
|
||||
val motionStatus by viewModel.motionStatus.collectAsStateWithLifecycle()
|
||||
val deviceTemperature by viewModel.deviceTemperature.collectAsStateWithLifecycle()
|
||||
val batteryStatus by viewModel.batteryStatus.collectAsStateWithLifecycle()
|
||||
val ledStatus by viewModel.ledStatus.collectAsStateWithLifecycle()
|
||||
val lastUpdate by viewModel.lastUpdate.collectAsStateWithLifecycle()
|
||||
val actionFeedback by viewModel.actionFeedback.collectAsStateWithLifecycle()
|
||||
|
||||
@@ -99,6 +105,14 @@ fun MainScreen(
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
// ---- 底图层(无视频流时显示) ----
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.bg1),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillBounds,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
// ---- 视频背景层 ----
|
||||
RtspVideoPlayer(
|
||||
url = settings.rtspUrl,
|
||||
@@ -123,6 +137,7 @@ fun MainScreen(
|
||||
viewModel = viewModel,
|
||||
connectionState = connectionState,
|
||||
motionState = motionState,
|
||||
ledStatus = ledStatus,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.windowInsetsPadding(WindowInsets.navigationBars)
|
||||
@@ -245,6 +260,7 @@ private fun BottomControls(
|
||||
viewModel: MainViewModel,
|
||||
connectionState: ProtocolClient.State,
|
||||
motionState: Int,
|
||||
ledStatus: LedStatus?,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
@@ -269,7 +285,8 @@ private fun BottomControls(
|
||||
ControlButtons(
|
||||
viewModel = viewModel,
|
||||
connectionState = connectionState,
|
||||
motionState = motionState
|
||||
motionState = motionState,
|
||||
ledStatus = ledStatus
|
||||
)
|
||||
|
||||
// 右摇杆(Yaw 旋转)
|
||||
@@ -289,7 +306,8 @@ private fun BottomControls(
|
||||
private fun ControlButtons(
|
||||
viewModel: MainViewModel,
|
||||
connectionState: ProtocolClient.State,
|
||||
motionState: Int
|
||||
motionState: Int,
|
||||
ledStatus: LedStatus?
|
||||
) {
|
||||
val connected = connectionState == ProtocolClient.State.CONNECTED
|
||||
val isRlControl = motionState == ControlCommands.MOTION_RL_CONTROL
|
||||
@@ -383,13 +401,23 @@ private fun ControlButtons(
|
||||
|
||||
// 照明
|
||||
SectionLabel("照明")
|
||||
val frontOn = ledStatus?.front == 1
|
||||
val backOn = ledStatus?.back == 1
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
MiniButton("前灯开") { viewModel.sendLightControl(1, 0) }
|
||||
MiniButton("前灯关") { viewModel.sendLightControl(0, 0) }
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
MiniButton("后灯开") { viewModel.sendLightControl(0, 1) }
|
||||
MiniButton("后灯关") { viewModel.sendLightControl(0, 0) }
|
||||
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))
|
||||
|
||||
@@ -451,6 +479,30 @@ private fun MiniButton(text: String, enabled: Boolean = true, onClick: () -> Uni
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LightToggleButton(
|
||||
text: String,
|
||||
isOn: Boolean,
|
||||
enabled: Boolean = true,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (isOn) Color(0xCCFFC107) else Color(0x66333333),
|
||||
disabledContainerColor = Color(0x33222222)
|
||||
),
|
||||
modifier = Modifier.width(110.dp)
|
||||
) {
|
||||
Text(
|
||||
"$text ${if (isOn) "开" else "关"}",
|
||||
color = if (enabled) OverlayText else Color(0x88FFFFFF),
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SectionLabel(text: String) {
|
||||
Text(
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.example.m20_gamepad.network.models.BatteryStatus
|
||||
import com.example.m20_gamepad.network.models.ControlCommands
|
||||
import com.example.m20_gamepad.network.models.DeviceTemperature
|
||||
import com.example.m20_gamepad.network.models.ErrorInfo
|
||||
import com.example.m20_gamepad.network.models.LedStatus
|
||||
import com.example.m20_gamepad.network.models.MotionStatus
|
||||
import com.example.m20_gamepad.network.models.StatusReport
|
||||
import com.example.m20_gamepad.service.JoystickController
|
||||
@@ -70,6 +71,9 @@ class MainViewModel(
|
||||
private val _batteryStatus = MutableStateFlow<BatteryStatus?>(null)
|
||||
val batteryStatus: StateFlow<BatteryStatus?> = _batteryStatus.asStateFlow()
|
||||
|
||||
private val _ledStatus = MutableStateFlow<LedStatus?>(null)
|
||||
val ledStatus: StateFlow<LedStatus?> = _ledStatus.asStateFlow()
|
||||
|
||||
private val _lastUpdate = MutableStateFlow<String?>(null)
|
||||
val lastUpdate: StateFlow<String?> = _lastUpdate.asStateFlow()
|
||||
|
||||
@@ -153,6 +157,7 @@ class MainViewModel(
|
||||
_motionStatus.value = null
|
||||
_deviceTemperature.value = null
|
||||
_batteryStatus.value = null
|
||||
_ledStatus.value = null
|
||||
_lastUpdate.value = null
|
||||
}
|
||||
|
||||
@@ -165,6 +170,7 @@ class MainViewModel(
|
||||
report.motionStatus?.let { _motionStatus.value = it }
|
||||
report.deviceTemperature?.let { _deviceTemperature.value = it }
|
||||
report.batteryStatus?.let { _batteryStatus.value = it }
|
||||
report.led?.let { _ledStatus.value = it }
|
||||
// 通用响应错误提示
|
||||
report.errorCode?.let { code ->
|
||||
if (code != 0) {
|
||||
|
||||
@@ -131,22 +131,24 @@ fun RtspVideoPlayer(
|
||||
}
|
||||
|
||||
Box(modifier = modifier) {
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
factory = { ctx ->
|
||||
PlayerView(ctx).apply {
|
||||
useController = false
|
||||
this.resizeMode = resizeMode.toMedia3ResizeMode()
|
||||
this.player = player
|
||||
setShutterBackgroundColor(android.graphics.Color.BLACK)
|
||||
// 仅在视频播放中时渲染 PlayerView,否则透出底层背景
|
||||
if (state is RtspState.Playing) {
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
factory = { ctx ->
|
||||
PlayerView(ctx).apply {
|
||||
useController = false
|
||||
this.resizeMode = resizeMode.toMedia3ResizeMode()
|
||||
this.player = player
|
||||
setShutterBackgroundColor(android.graphics.Color.TRANSPARENT)
|
||||
}
|
||||
},
|
||||
update = { view ->
|
||||
view.player = player
|
||||
view.resizeMode = resizeMode.toMedia3ResizeMode()
|
||||
}
|
||||
},
|
||||
// player 引用变化时(codec 重建)更新 PlayerView
|
||||
update = { view ->
|
||||
view.player = player
|
||||
view.resizeMode = resizeMode.toMedia3ResizeMode()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// 连接中 / 错误时的提示覆盖层
|
||||
when (val s = state) {
|
||||
|
||||
Reference in New Issue
Block a user