Compose 界面入门

添加依赖项

Media3 库包含一个基于 Jetpack Compose 的 UI 模块。要使用它,请添加以下依赖项

Kotlin

implementation("androidx.media3:media3-ui-compose:1.7.1")

Groovy

implementation "androidx.media3:media3-ui-compose:1.7.1"

我们强烈建议您采用 Compose 优先的方式开发应用,或从使用视图迁移

全 Compose 演示应用

虽然 media3-ui-compose 库不包含开箱即用的 Composables(例如按钮、指示器、图片或对话框),但您可以找到一个完全用 Compose 编写的演示应用,该应用避免了任何互操作性解决方案,例如将 PlayerView 封装在 AndroidView 中。该演示应用利用 media3-ui-compose 模块中的 UI 状态持有者类,并使用了 Compose Material3 库。

UI 状态持有者

为了更好地了解如何利用 UI 状态持有者与可组合项的灵活性,请阅读 Compose 如何管理状态

按钮状态持有者

对于某些 UI 状态,我们假设它们很可能会被类似按钮的可组合项使用。

状态 remember*State 类型
PlayPauseButtonState rememberPlayPauseButtonState 2 切换
PreviousButtonState rememberPreviousButtonState 常量
NextButtonState rememberNextButtonState 常量
RepeatButtonState rememberRepeatButtonState 3 切换
ShuffleButtonState rememberShuffleButtonState 2 切换
PlaybackSpeedState rememberPlaybackSpeedState 菜单或 N 切换

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 转换为 UI 状态。

然后,您可以在您喜欢的布局中混合搭配这些按钮

Row(
  modifier = modifier.fillMaxWidth(),
  horizontalArrangement = Arrangement.SpaceEvenly,
  verticalAlignment = Alignment.CenterVertically,
) {
  PreviousButton(player)
  PlayPauseButton(player)
  NextButton(player)
}

视觉输出状态持有者

PresentationState 包含何时可以在 PlayerSurface 中显示视频输出或应被占位符 UI 元素覆盖的信息。

val presentationState = rememberPresentationState(player)
val scaledModifier = Modifier.resize(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 对象来收集不断变化的 UI 数据。例如,您可能正在寻找一个 Player.isPlaying flow,您可以通过生命周期感知的方式 collect 它。或者类似 Player.eventsFlow 的东西,它为您提供一个 Flow<Player.Events>,您可以按照自己想要的方式 filter

然而,将 flow 用于 Player UI 状态存在一些缺点。主要问题之一是数据传输的异步性质。我们希望确保 Player.Event 及其在 UI 端的消费之间的延迟尽可能小,避免显示与 Player 不同步的 UI 元素。

其他要点包括

  • 包含所有 Player.Events 的 flow 不会遵循单一职责原则,每个消费者都必须过滤掉相关事件。
  • 为每个 Player.Event 创建一个 flow 将要求您为每个 UI 元素(使用 combine)组合它们。Player.Event 和 UI 元素更改之间存在多对多映射。必须使用 combine 可能会导致 UI 进入潜在的非法状态。

创建自定义 UI 状态

如果现有 UI 状态不能满足您的需求,您可以添加自定义 UI 状态。查看现有状态的源代码以复制模式。典型的 UI 状态持有者类执行以下操作:

  1. 接收一个 Player
  2. 使用协程订阅 Player。有关详细信息,请参阅 Player.listen
  3. 通过更新其内部状态来响应特定的 Player.Events
  4. 接受将转换为适当 Player 更新的业务逻辑命令。
  5. 可以在 UI 树的多个位置创建,并且始终保持对播放器状态的一致视图。
  6. 公开 Compose State 字段,可由 Composable 使用以动态响应更改。
  7. 附带一个 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 来捕获它们,这是一个 suspend fun,可让您进入协程世界并无限期地监听 Player.Events。Media3 对各种 UI 状态的实现有助于最终开发者无需关注了解 Player.Events