UI: 增大摇杆尺寸至180dp;视频全屏延展(ZOOM模式)并加入缩放模式设置项

- 左右摇杆 Modifier.size 140dp -> 180dp (MainScreen.kt)
- RtspVideoPlayer 新增 resizeMode: VideoResizeMode 参数,默认 ZOOM
- 新增 VideoResizeMode 枚举 (FIT/ZOOM/FILL) 及 Media3 常量映射
- 设置界面新增"视频缩放模式"下拉选项,持久化到 DataStore
- AGENTS.md 更新已完成项和配置项表格

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
2026-07-17 20:54:03 +08:00
parent 6d7231b064
commit d563585cd5
9 changed files with 115 additions and 10 deletions
@@ -2,6 +2,18 @@ package com.example.m20_gamepad.data
import com.example.m20_gamepad.video.VideoCodec
/**
* 视频缩放模式。
*/
enum class VideoResizeMode {
/** 等比缩放,完整显示(留黑边) */
FIT,
/** 等比缩放,填充全屏(裁切多余部分) */
ZOOM,
/** 拉伸填满全屏 */
FILL
}
/**
* 应用配置数据类。
*
@@ -12,7 +24,8 @@ data class AppSettings(
val robotHost: String = DEFAULT_ROBOT_HOST,
val robotPort: Int = DEFAULT_ROBOT_PORT,
val rtspUrl: String = DEFAULT_RTSP_URL,
val videoCodec: VideoCodec = VideoCodec.AUTO
val videoCodec: VideoCodec = VideoCodec.AUTO,
val videoResizeMode: VideoResizeMode = VideoResizeMode.ZOOM
) {
companion object {
const val DEFAULT_ROBOT_HOST = "10.21.31.103"
@@ -34,7 +34,8 @@ class SettingsRepository(context: Context) {
robotHost = prefs[KEY_HOST] ?: AppSettings.DEFAULT_ROBOT_HOST,
robotPort = prefs[KEY_PORT] ?: AppSettings.DEFAULT_ROBOT_PORT,
rtspUrl = prefs[KEY_RTSP_URL] ?: AppSettings.DEFAULT_RTSP_URL,
videoCodec = VideoCodec.entries.getOrElse(prefs[KEY_CODEC] ?: 0) { VideoCodec.AUTO }
videoCodec = VideoCodec.entries.getOrElse(prefs[KEY_CODEC] ?: 0) { VideoCodec.AUTO },
videoResizeMode = VideoResizeMode.entries.getOrElse(prefs[KEY_RESIZE_MODE] ?: 1) { VideoResizeMode.ZOOM }
)
}
@@ -61,6 +62,7 @@ class SettingsRepository(context: Context) {
prefs[KEY_PORT] = settings.robotPort.coerceIn(1, 65535)
prefs[KEY_RTSP_URL] = settings.rtspUrl.trim()
prefs[KEY_CODEC] = settings.videoCodec.ordinal
prefs[KEY_RESIZE_MODE] = settings.videoResizeMode.ordinal
}
}
@@ -69,5 +71,6 @@ class SettingsRepository(context: Context) {
private val KEY_PORT = intPreferencesKey("robot_port")
private val KEY_RTSP_URL = stringPreferencesKey("rtsp_url")
private val KEY_CODEC = intPreferencesKey("video_codec")
private val KEY_RESIZE_MODE = intPreferencesKey("video_resize_mode")
}
}
@@ -103,6 +103,7 @@ fun MainScreen(
RtspVideoPlayer(
url = settings.rtspUrl,
codec = settings.videoCodec,
resizeMode = settings.videoResizeMode,
modifier = Modifier.fillMaxSize()
)
@@ -255,7 +256,7 @@ private fun BottomControls(
) {
// 左摇杆(X/Y 移动)
Joystick(
modifier = Modifier.size(140.dp),
modifier = Modifier.size(180.dp),
type = JoystickType.EIGHT_AXIS,
padColor = Color(0x55808080),
buttonColor = Color(0xCCFFFFFF),
@@ -273,7 +274,7 @@ private fun BottomControls(
// 右摇杆(Yaw 旋转)
Joystick(
modifier = Modifier.size(140.dp),
modifier = Modifier.size(180.dp),
type = JoystickType.EIGHT_AXIS,
padColor = Color(0x55808080),
buttonColor = Color(0xCCFFFFFF),
@@ -37,6 +37,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.example.m20_gamepad.data.AppSettings
import com.example.m20_gamepad.data.SettingsRepository
import com.example.m20_gamepad.data.VideoResizeMode
import com.example.m20_gamepad.video.VideoCodec
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@@ -65,6 +66,7 @@ fun SettingsScreen(
var portText by remember { mutableStateOf(AppSettings.DEFAULT_ROBOT_PORT.toString()) }
var rtspUrl by remember { mutableStateOf(AppSettings.DEFAULT_RTSP_URL) }
var codec by remember { mutableStateOf(VideoCodec.AUTO) }
var resizeMode by remember { mutableStateOf(VideoResizeMode.ZOOM) }
var hostError by remember { mutableStateOf<String?>(null) }
var portError by remember { mutableStateOf<String?>(null) }
var initialized by remember { mutableStateOf(false) }
@@ -76,6 +78,7 @@ fun SettingsScreen(
portText = s.robotPort.toString()
rtspUrl = s.rtspUrl
codec = s.videoCodec
resizeMode = s.videoResizeMode
initialized = true
}
}
@@ -161,6 +164,12 @@ fun SettingsScreen(
modifier = Modifier.fillMaxWidth()
)
ResizeModeDropdown(
selected = resizeMode,
onSelect = { resizeMode = it },
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.padding(top = 8.dp))
// ---- 操作按钮 ----
@@ -183,7 +192,8 @@ fun SettingsScreen(
robotHost = host,
robotPort = port,
rtspUrl = rtspUrl,
videoCodec = codec
videoCodec = codec,
videoResizeMode = resizeMode
)
)
onSaved()
@@ -264,3 +274,59 @@ private fun CodecDropdown(
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ResizeModeDropdown(
selected: VideoResizeMode,
onSelect: (VideoResizeMode) -> Unit,
modifier: Modifier = Modifier
) {
var expanded by remember { mutableStateOf(false) }
val options = VideoResizeMode.entries
val labels = mapOf(
VideoResizeMode.FIT to "等比缩放(留黑边)",
VideoResizeMode.ZOOM to "等比缩放(填充全屏)",
VideoResizeMode.FILL to "拉伸填充"
)
Column(modifier = modifier) {
Text(
text = "视频缩放模式",
style = androidx.compose.material3.MaterialTheme.typography.titleSmall,
color = androidx.compose.material3.MaterialTheme.colorScheme.primary
)
Spacer(Modifier.padding(top = 4.dp))
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = it }
) {
OutlinedTextField(
value = labels[selected] ?: selected.name,
onValueChange = {},
readOnly = true,
singleLine = true,
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded)
},
modifier = Modifier
.fillMaxWidth()
.menuAnchor()
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
options.forEach { m ->
DropdownMenuItem(
text = { Text(labels[m] ?: m.name) },
onClick = {
onSelect(m)
expanded = false
}
)
}
}
}
}
}
@@ -23,7 +23,9 @@ import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.mediacodec.MediaCodecInfo
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView
import com.example.m20_gamepad.data.VideoResizeMode
/**
* 视频解码器选择策略。
@@ -58,12 +60,14 @@ sealed class RtspState {
*
* @param url RTSP 流地址,如 `rtsp://10.21.31.103:554/stream`
* @param codec 解码器选择策略
* @param resizeMode 视频缩放模式,默认 ZOOM(等比填充全屏)
* @param modifier 布局修饰符
*/
@Composable
fun RtspVideoPlayer(
url: String,
codec: VideoCodec = VideoCodec.AUTO,
resizeMode: VideoResizeMode = VideoResizeMode.ZOOM,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
@@ -132,6 +136,7 @@ fun RtspVideoPlayer(
factory = { ctx ->
PlayerView(ctx).apply {
useController = false
this.resizeMode = resizeMode.toMedia3ResizeMode()
this.player = player
setShutterBackgroundColor(android.graphics.Color.BLACK)
}
@@ -139,6 +144,7 @@ fun RtspVideoPlayer(
// player 引用变化时(codec 重建)更新 PlayerView
update = { view ->
view.player = player
view.resizeMode = resizeMode.toMedia3ResizeMode()
}
)
@@ -222,4 +228,13 @@ private fun forceHardware(): MediaCodecSelector = object : MediaCodecSelector {
mimeType, requiresSecureDecoder, requiresTunnelingDecoder
).filter { it.hardwareAccelerated }
}
}
// ==================== 缩放模式映射 ====================
/** 将 [VideoResizeMode] 映射为 [AspectRatioFrameLayout] 的 resizeMode 常量。 */
private fun VideoResizeMode.toMedia3ResizeMode(): Int = when (this) {
VideoResizeMode.FIT -> AspectRatioFrameLayout.RESIZE_MODE_FIT
VideoResizeMode.ZOOM -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
VideoResizeMode.FILL -> AspectRatioFrameLayout.RESIZE_MODE_FILL
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB