添加依赖项
Media3 库包含一个基于 Jetpack Compose 的界面模块。如需使用该库,请添加以下依赖项:
Kotlin
implementation("androidx.media3:media3-ui-compose:1.8.0")
Groovy
implementation "androidx.media3:media3-ui-compose:1.8.0"
我们强烈建议您以“Compose 优先”的方式开发应用,或从使用 View 进行迁移。
完全采用 Compose 的演示应用
虽然 media3-ui-compose 库不包含开箱即用的可组合项(例如按钮、指示器、图片或对话框),但您可以找到一个完全使用 Compose 编写的演示应用,该应用避免了任何互操作性解决方案,例如将 PlayerView 封装在 AndroidView 中。此演示应用利用 media3-ui-compose 模块中的界面状态持有者类,并使用 Compose Material3 库。
界面状态容器
如需更好地了解如何灵活使用界面状态容器与可组合项,请阅读有关 Compose 如何管理状态的文章。
按钮状态容器
对于某些界面状态,我们假设它们很可能会被类似按钮的可组合函数使用。
| 状态 | remember*State | 类型 | 
|---|---|---|
| PlayPauseButtonState | rememberPlayPauseButtonState | 2-Toggle | 
| PreviousButtonState | rememberPreviousButtonState | 常量 | 
| NextButtonState | rememberNextButtonState | 常量 | 
| RepeatButtonState | rememberRepeatButtonState | 3-Toggle | 
| ShuffleButtonState | rememberShuffleButtonState | 2-Toggle | 
| PlaybackSpeedState | rememberPlaybackSpeedState | 菜单或 N-Toggle | 
PlayPauseButtonState 的用法示例:
@Composable
fun PlayPauseButton(player: Player, modifier: Modifier = Modifier) {
  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),
    )
  }
}
请注意,state 不包含主题信息,例如用于播放或暂停的图标。它的唯一职责是将 Player 转换为界面状态。
然后,您可以根据自己的偏好在布局中混合搭配按钮:
Row(
  modifier = modifier.fillMaxWidth(),
  horizontalArrangement = Arrangement.SpaceEvenly,
  verticalAlignment = Alignment.CenterVertically,
) {
  PreviousButton(player)
  PlayPauseButton(player)
  NextButton(player)
}
可视化输出状态容器
PresentationState 用于保存以下信息:PlayerSurface 中的视频输出何时可以显示,或者何时应被占位界面元素覆盖。
val presentationState = rememberPresentationState(player)
val scaledModifier = Modifier.resizeWithContentScale(ContentScale.Fit, presentationState.videoSizeDp)
Box(modifier) {
  // Always leave PlayerSurface to be part of the Compose tree because it will be initialised 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 = player,
    surfaceType = SURFACE_TYPE_SURFACE_VIEW,
    modifier = scaledModifier,
  )
  if (presentationState.coverSurface) {
    // Cover the surface that is being prepared with a shutter
    Box(Modifier.background(Color.Black))
  }
在此示例中,我们可以使用 presentationState.videoSizeDp 将 Surface 缩放到所需的宽高比(如需了解更多类型,请参阅 ContentScale 文档),并使用 presentationState.coverSurface 来了解何时不适合显示 Surface。在这种情况下,您可以在 Surface 上放置一个不透明的快门,当 Surface 准备就绪时,该快门就会消失。
Flows 在哪里?
许多 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(Player.COMMAND_ACTION_A))
    private set
  var someField by mutableStateOf(someFieldDefault)
    private set
  fun onClick() {
    player.actionA()
  }
  suspend fun observe() =
    player.listen { events ->
      if (
        events.containsAny(
          Player.EVENT_B_CHANGED,
          Player.EVENT_C_CHANGED,
          Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
        )
      ) {
        someField = this.someField
        isEnabled = this.isCommandAvailable(Player.COMMAND_ACTION_A)
      }
    }
}
如需对自己的 Player.Events 做出反应,您可以使用 Player.listen 捕获它们,Player.listen 是一个 suspend fun,可让您进入协程世界并无限期地监听 Player.Events。Media3 对各种界面状态的实现有助于最终开发者不必担心学习 Player.Events。
