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:
@@ -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 |
Reference in New Issue
Block a user