Compose 核心

media3-ui-compose 程式庫提供基礎元件,可在 Jetpack Compose 中建構媒體 UI。這項功能專為需要比 media3-ui-compose-material3 程式庫提供的自訂項目更多的開發人員設計。本頁說明如何使用核心元件和狀態持有者,建立自訂媒體播放器 UI。

混合使用 Material 3 和自訂 Compose 元件

media3-ui-compose-material3 程式庫的設計宗旨是提供彈性,您可以將預先建構的元件用於大部分的 UI,但如果需要更多控制權,可以將單一元件換成自訂實作項目。這時 media3-ui-compose 程式庫就派上用場。

舉例來說,假設您想使用 Material3 程式庫中的標準 PreviousButtonNextButton,但需要完全自訂的 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 用於顯示媒體內容的介面,可處理顯示比例管理、大小調整和快門
PlayerSurface 原始介面,會在 AndroidView 中包裝 SurfaceViewTextureView

UI 狀態持有物件

如果沒有任何架構元件符合需求,您也可以直接使用狀態物件。一般來說,建議使用對應的 remember 方法,在重組之間保留 UI 外觀。

如要進一步瞭解如何運用 UI 狀態持有者與 Composables 的彈性,請參閱「管理狀態」一文。

按鈕狀態容器

對於某些 UI 狀態,程式庫會假設這些狀態最有可能由類似按鈕的可組合函式取用。

狀態 記住*狀態 類型
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),
  )
}

視覺輸出狀態容器

PresentationState 包含影片輸出內容在 PlayerSurface 中顯示或應由預留位置 UI 元素遮蓋的相關資訊。ContentFrame 可組合項會結合處理比例,並負責在尚未準備好的介面上顯示快門。

@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.videoSizeDp)

  // 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, scaledModifier, surfaceType)

  if (presentationState.coverSurface) {
    // Cover the surface that is being prepared with a shutter
    shutter()
  }
}

在這裡,我們可以使用 presentationState.videoSizeDp 將 Surface 縮放至所選的長寬比 (如需更多類型,請參閱 ContentScale 說明文件),並使用 presentationState.coverSurface 瞭解何時不適合顯示 Surface。在這種情況下,您可以在表面上放置不透明的快門,表面準備就緒時,快門就會消失。ContentFrame 可讓您將快門自訂為尾端 lambda,但根據預設,快門會是填滿父項容器大小的黑色 @Composable Box

Flows 在哪裡?

許多 Android 開發人員都熟悉使用 Kotlin Flow 物件收集不斷變動的 UI 資料。舉例來說,您可能會尋找可collect以生命週期感知方式Player.isPlaying使用的流程。或是Player.eventsFlow,為您提供可Flow<Player.Events>filter

不過,使用 Flow 管理 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