文案神器

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 可能会导致界面处于潜在的非法状态。

创建自定义界面状态

如果现有界面状态无法满足您的需求,您可以添加自定义界面状态。 查看现有状态的源代码,以复制该模式。典型的界面状态容器类会执行以下操作:

  1. 接受 Player 对象。
  2. 使用协程订阅 Player。如需了解详情,请参阅 Player.listen。
  3. 通过更新其内部状态来响应特定的 Player.Events。
  4. 接受将转换为相应 Player 更新的业务逻辑命令。
  5. 可以在界面树中的多个位置创建,并且始终保持对播放器状态的一致视图。
  6. 公开可由可组合项使用的 Compose State 字段,以动态响应更改。
  7. 附带一个 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。