文案神器

media3-ui-compose 库提供了在 Jetpack Compose 中构建媒体界面的基础组件。它专为需要比 media3-ui-compose-material3 库提供的自定义功能更多的开发者而设计。本页介绍了如何使用核心组件和状态容器创建自定义媒体播放器界面。

混合使用 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 的原始 Surface。

自定义 Player 可组合项

使用 media3-ui-compose-material3 库时,Player 可组合项可提供完整的媒体播放体验。您可以通过向其内容槽提供您自己的可组合项来自定义其布局,也可以利用 PlayerDefaults 对象自定义界面的特定部分,例如 TopControls、CenterControls、BottomControls 和 ErrorOverlay。

图 1 显示了 Player 可组合项放置其槽的位置。图 2 显示了其绘制图层的顺序。

一个视频播放器,其插槽用虚线轮廓标记。从上到下:沿顶部边缘的 topControls、中心上方的 errorOverlay、中间的 centerControls(包含 backSecondary、back、central、forward 和 forwardSecondary 按钮)、靠近底部的 subtitles,以及沿底部边缘的 bottomControls(包含 above、left、progressSlider、right 和 below 槽位)。ContentFrame 会填充其背后的区域。
图 1. Player 可组合项的槽。 中间和底部的控件显示 PlayerDefaults.CenterControls 和 PlayerDefaults.BottomControls 的槽位。topControls、above 和 below 槽默认情况下为空。
六个堆叠层的分解视图。从下到上:1. PlayerSurface;2. shutter;3. artwork;4. subtitles;5. errorOverlay;6. topControls、centerControls 和 bottomControls。第 1 层到第 4 层构成 ContentFrame,所有 6 层构成 Player。
图 2. Player 可组合项的层,按绘制顺序编号。

例如,您可以使用 PlayerDefaults.CenterControls 仅替换中央播放/暂停按钮,而让其余中央控件保持不变。 您还可以为 topControls 等参数传递完全自定义的可组合项,并完全省略 bottomControls 等其他参数,以将其保留为默认值。

@Composable
fun CustomPlayerSlots(player: Player, showControls: Boolean, modifier: Modifier = Modifier) {
  Player(
    player = player,
    modifier = modifier,
    showControls = showControls,
    topControls = {
      // Fully custom top controls
      AnimatedVisibility(showControls) { Text("My custom title") }
    },
    centerControls = {
      // Use default CenterControls but override the central button
      PlayerDefaults.CenterControls(
        player = player,
        visible = showControls,
        central = {
          // A custom play/pause button
          Material3PlayPauseButton(player, 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-Toggle
ShuffleButtonState rememberShuffleButtonState 2-Toggle
PlaybackSpeedState rememberPlaybackSpeedState 菜单或 N 切换

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。

Flows 在哪里?

许多 Android 开发者都熟悉如何使用 Kotlin Flow 对象来收集不断变化的界面数据。例如,您可能正在寻找可以collect的 Player.isPlaying 流,并希望以生命周期感知的方式进行。或者使用 Player.eventsFlow 之类的工具,为您提供一个 Flow<Player.Events>,您可以按照自己想要的方式filter。

不过,使用 flow 来处理 Player 界面状态有一些缺点。主要问题之一是数据传输的异步性质。我们希望尽可能缩短 Player.Event 与其在界面端的使用之间的延迟时间,避免显示与 Player 不同步的界面元素。

其他要点包括:

  • 包含所有 Player.Events 的数据流不符合单一职责原则,每个使用方都必须过滤掉相关事件。
  • 为每个 Player.Event 创建一个 flow 需要您为每个界面元素组合这些 flow(使用 combine)。Player.Event 与界面元素更改之间存在多对多映射。必须使用 combine 可能会导致界面处于可能不合法的状态。

创建自定义界面状态

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

  1. 接受 Player 对象。
  2. 使用协程订阅 Player。如需了解详情,请参阅 Player.listen。
  3. 通过更新其内部状态来响应特定 Player.Events。
  4. 接受将转换为适当 Player 更新的业务逻辑命令。
  5. 可在界面树中的多个位置创建,并且始终保持 Player 状态的一致视图。
  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 捕获它们,这是一个 suspend fun,可让您进入协程世界并无限期监听 Player.Events。Media3 实现了各种界面状态,这有助于最终开发者无需费心了解 Player.Events。