開始使用以 Compose 為基礎的 UI

新增依附元件

Media3 程式庫包含以 Jetpack Compose 為基礎的 UI 模組。如要使用它,請新增下列依附元件:

Kotlin

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

Groovy

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

我們強烈建議您以 Compose 優先的方式開發應用程式,或從使用 View 遷移

完整的 Compose 示範應用程式

雖然 media3-ui-compose 程式庫不包含現成的可組合項 (例如按鈕、指標、圖片或對話方塊),但您可以找到完全以 Compose 編寫的示範應用程式,避免任何互通性解決方案,例如在 AndroidView 中包裝 PlayerView。這個示範應用程式會使用 media3-ui-compose 模組的 UI 狀態容器類別,並使用 Compose Material3 程式庫。

UI 狀態容器

如要進一步瞭解如何使用 UI 狀態持有者與可組合函式的彈性,請參閱 Compose 管理狀態的方式。

按鈕狀態容器

對於某些 UI 狀態,我們假設這些狀態最有可能由類似按鈕的 Composable 使用。

狀態 remember*State 類型
PlayPauseButtonState rememberPlayPauseButtonState 2 個切換鈕
PreviousButtonState rememberPreviousButtonState 常數
NextButtonState rememberNextButtonState 常數
RepeatButtonState rememberRepeatButtonState 3-Toggle
ShuffleButtonState rememberShuffleButtonState 2 個切換鈕
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 轉換為 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。在這種情況下,您可以在表面上方放置不透明的快門,當表面準備就緒時,快門就會消失。

流程在哪裡?

許多 Android 開發人員都熟悉使用 Kotlin Flow 物件收集不斷變化的 UI 資料。舉例來說,您可能會尋找可以以生命週期感知方式 collectPlayer.isPlaying 流程。或者,您也可以使用 Player.eventsFlow 之類的函式,為您提供 Flow<Player.Events>,讓您可以按照自己的方式進行 filter

不過,使用流程來處理 Player UI 狀態有一些缺點。其中一個主要問題是資料傳輸的非同步性質。我們希望確保 Player.Event 與 UI 端的使用情形之間的延遲時間盡可能短,避免顯示與 Player 不同步的 UI 元素。

其他重點包括:

  • 包含所有 Player.Events 的流程不會遵循單一責任原則,每個使用者都必須篩除相關事件。
  • 為每個 Player.Event 建立流程時,您必須為每個 UI 元素合併 (使用 combine)。Player.Event 和 UI 元素變更之間的對應關係為多對多。必須使用 combine 可能會導致 UI 進入非法狀態。

建立自訂 UI 狀態

如果現有的 UI 狀態無法滿足您的需求,您可以新增自訂 UI 狀態。查看現有狀態的原始碼,複製模式。典型的 UI 狀態容器類別會執行以下操作:

  1. 接收 Player
  2. 使用協同程式訂閱 Player。詳情請參閱 Player.listen
  3. 透過更新內部狀態,回應特定 Player.Events
  4. 接受業務邏輯指令,並轉換為適當的 Player 更新。
  5. 可在 UI 樹狀結構的多個位置建立,且會一律維持 Player 狀態的一致檢視畫面。
  6. 公開 Compose State 欄位,可供可組合函式使用,以便動態回應變更。
  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