media3-ui-compose 库为在 Jetpack Compose 中构建媒体界面提供了基础组件。它专为需要比 media3-ui-compose-material3 库提供的更多自定义功能的开发者而设计。本页介绍了如何使用核心组件和状态容器 (state holder) 创建自定义媒体播放器界面。
混合使用 Material3 和自定义 Compose 组件
media3-ui-compose-material3 库在设计上具有灵活性。您可以将预构建的组件用于大多数界面,但如果需要更多控制权,可以换成自定义实现。这时,media3-ui-compose 库便可发挥作用。
例如,假设您想使用 Material3 库中的标准 PreviousButton 和 NextButton,但需要完全自定义的 PlayPauseButton。为此,您可以使用核心 media3-ui-compose 库中的 PlayPauseButton,并将其与预构建的组件放在一起。
Row { // Use prebuilt component from the Media3 UI Compose Material3 library PreviousButton(player) // Use the scaffold component from Media3 UI Compose library PlayPauseButton(player) { // `this` is PlayPauseButtonState FilledTonalButton( onClick = { Log.d("PlayPauseButton", "Clicking on play-pause button") this.onClick() }, enabled = this.isEnabled, ) { Icon( imageVector = if (showPlay) Icons.Default.PlayArrow else Icons.Default.Pause, contentDescription = if (showPlay) "Play" else "Pause", ) } } // Use prebuilt component from the Media3 UI Compose Material3 library NextButton(player) }
可用组件
media3-ui-compose 库提供了一组预构建的可组合项,用于实现常见的播放器控件。以下是一些可以直接在应用中使用的组件:
| 组件 | 说明 |
|---|---|
PlayPauseButton |
一种用于在播放和暂停之间切换的按钮的状态容器。 |
SeekBackButton |
用于按定义的增量向后搜索的按钮的状态容器。 |
SeekForwardButton |
一种状态容器,用于以定义的增量向前搜索的按钮。 |
NextButton |
用于查找下一个媒体项的按钮的状态容器。 |
PreviousButton |
用于跳转到上一个媒体项的按钮的状态容器。 |
RepeatButton |
用于循环切换重复模式的按钮的状态容器。 |
ShuffleButton |
用于切换随机播放模式的按钮的状态容器。 |
MuteButton |
用于存放静音和取消静音按钮的状态容器。 |
TimeText |
用于显示玩家进度的可组合项的状态容器。 |
ContentFrame |
用于显示媒体内容的 Surface,可处理宽高比管理、调整大小和快门 |
PlayerSurface |
在 AndroidView 中封装 SurfaceView 和 TextureView 的原始界面。 |
自定义 Player 可组合项
使用 media3-ui-compose-material3 库时,Player 可组合项可提供完整的媒体播放体验。您可以通过向其内容槽位提供自己的可组合项来自定义其布局,也可以利用 PlayerDefaults 对象来自定义界面的特定部分,例如 TopControls、CenterControls、BottomControls 和 ErrorOverlay。
例如,您可以使用 PlayerDefaults.CenterControls 仅替换中央播放/暂停按钮,同时保持其余的中央控件不变。您还可以为 topControls 等参数传递完全自定义的可组合项,并完全省略 bottomControls 等其他参数,使其保持默认值。
@Composable fun CustomPlayerSlots(player: Player, modifier: Modifier = Modifier) { Player( player = player, modifier = modifier, topControls = { p, visible -> // Fully custom top controls AnimatedVisibility(visible) { Text("My custom title") } }, centerControls = { p, visible -> // Use default CenterControls but override the central button PlayerDefaults.CenterControls( player = p, visible = visible, central = { // A custom play/pause button Material3PlayPauseButton(it, modifier = Modifier.size(64.dp)) }, ) }, // bottomControls are left as default ) }
界面状态容器
如果没有任何脚手架组件符合您的需求,您也可以直接使用状态对象。一般建议使用相应的 remember 方法,以在重组之间保持界面外观。
如需更好地了解如何灵活使用界面状态容器与可组合项,请阅读有关 Compose 如何管理状态的文章。
按钮状态容器
对于某些界面状态,该库会假设它们很可能会被类似按钮的可组合项使用。
| 状态 | remember*State | 类型 |
|---|---|---|
PlayPauseButtonState |
rememberPlayPauseButtonState |
2-Toggle |
PreviousButtonState |
rememberPreviousButtonState |
常量 |
NextButtonState |
rememberNextButtonState |
常量 |
RepeatButtonState |
rememberRepeatButtonState |
3-切换 |
ShuffleButtonState |
rememberShuffleButtonState |
2-Toggle |
PlaybackSpeedState |
rememberPlaybackSpeedState |
菜单或 N-Toggle |
PlayPauseButtonState 的用法示例:
val state = rememberPlayPauseButtonState(player) IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) { Icon( imageVector = if (state.showPlay) Icons.Default.PlayArrow else Icons.Default.Pause, contentDescription = if (state.showPlay) stringResource(R.string.playpause_button_play) else stringResource(R.string.playpause_button_pause), ) }
可视化输出状态容器
CurrentMediaItemState 提供有关当前播放的 MediaItem 的信息,而 PlaylistState 则公开有关播放器上设置的 MediaItems 的信息。这些方法有助于在自定义界面中显示元数据信息。
ErrorState 提供有关播放器当前错误状态的信息,可用于显示错误叠加层。
PresentationState 用于保存 PlayerSurface 中视频输出何时可以显示或何时应被占位界面元素覆盖的信息。ContentFrame 可组合项将宽高比处理与在尚未准备就绪的 Surface 上显示快门相结合。
@Composable fun ContentFrame( player: Player?, modifier: Modifier = Modifier, surfaceType: @SurfaceType Int = SURFACE_TYPE_SURFACE_VIEW, contentScale: ContentScale = ContentScale.Fit, keepContentOnReset: Boolean = false, shutter: @Composable () -> Unit = { Box(Modifier.fillMaxSize().background(Color.Black)) }, ) { val presentationState = rememberPresentationState(player, keepContentOnReset) val scaledModifier = modifier.resizeWithContentScale(contentScale, presentationState.videoAspectRatio) // Always leave PlayerSurface to be part of the Compose tree because it will be initialized in // the process. If this composable is guarded by some condition, it might never become visible // because the Player won't emit the relevant event, e.g. the first frame being ready. PlayerSurface(player, scaledModifier, surfaceType) if (presentationState.coverSurface) { // Cover the surface that is being prepared with a shutter shutter() } }
在此示例中,我们可以使用 presentationState.videoAspectRatio 将 Surface 缩放到所选的宽高比(如需了解更多类型,请参阅内容缩放),并使用 presentationState.coverSurface 来了解何时不适合显示 Surface。在这种情况下,您可以在 Surface 上放置一个不透明的快门,当 Surface 准备就绪时,该快门会消失。
ContentFrame 可让您将快门自定义为尾随 lambda,但默认情况下,它将是填充父容器大小的黑色 @Composable Box。
Flow 在哪里?
许多 Android 开发者都熟悉如何使用 Kotlin Flow 对象来收集不断变化的界面数据。例如,您可能正在寻找可以以生命周期感知方式进行 collect 的 Player.isPlaying 流。或者,您也可以使用 Player.eventsFlow 之类的代码为您提供 Flow<Player.Events>,以便您按照自己想要的方式filter。
不过,使用 flow 处理 Player 界面状态有一些缺点。主要问题之一是数据传输的异步性。我们希望尽可能缩短 Player.Event 与界面端使用之间的延迟时间,避免显示与 Player 不同步的界面元素。
其他要点包括:
- 如果一个流程包含所有
Player.Events,则不符合单一责任原则,每个消费者都必须过滤掉相关事件。 - 为每个
Player.Event创建一个 flow 需要您将它们(使用combine)组合在一起,以用于每个界面元素。Player.Event 与界面元素更改之间存在多对多映射关系。必须使用combine可能会导致界面处于潜在的非法状态。
创建自定义界面状态
如果现有界面状态无法满足您的需求,您可以添加自定义界面状态。 查看现有状态的源代码,以复制该模式。典型的界面状态容器类会执行以下操作:
- 接受
Player对象。 - 使用协程订阅
Player。如需了解详情,请参阅Player.listen。 - 通过更新其内部状态来响应特定的
Player.Events。 - 接受将转换为相应
Player更新的业务逻辑命令。 - 可以在界面树中的多个位置创建,并且始终保持对播放器状态的一致视图。
- 公开可由可组合项使用的 Compose
State字段,以动态响应更改。 - 附带一个
remember*State函数,用于在组合之间记住实例。
幕后会发生什么:
class SomeButtonState(private val player: Player) { var isEnabled by mutableStateOf(player.isCommandAvailable(COMMAND_ACTION_A)) private set var someFieldValue by mutableStateOf(someFieldDefault) private set fun onClick() { player.actionA() } suspend fun observe(): Nothing = player.listen { events -> if (events.containsAny(EVENT_B_CHANGED, EVENT_C_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) { someFieldValue = this.someField isEnabled = this.isCommandAvailable(COMMAND_ACTION_A) } } }
如需对自己的 Player.Events 做出反应,您可以使用 Player.listen 捕获它们,Player.listen 是一个 suspend fun,可让您进入协程世界并无限期地监听 Player.Events。Media3 对各种界面状态的实现有助于最终开发者不必担心学习 Player.Events。