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 的彈性,請參閱 Compose 管理狀態的方式。

按鈕狀態容器

對於某些 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 包含影片輸出內容何時可顯示,或應由預留位置 UI 元素遮蓋的資訊。PlayerSurfaceContentFrame 可組合項會結合處理比例,並負責在尚未準備就緒的介面上顯示快門。

@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 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.videoSizeDp 將 Surface 縮放至所選的長寬比 (如需更多類型,請參閱 ContentScale 文件),並使用 presentationState.coverSurface 判斷顯示 Surface 的時機是否不適當。在這種情況下,您可以在表面上放置不透明的快門,表面準備就緒時,快門就會消失。ContentFrame 可讓您將快門自訂為結尾的 lambda,但預設會是填滿父項容器大小的黑色 @Composable Box

Flows 在哪裡?

許多 Android 開發人員都熟悉使用 Kotlin Flow 物件收集不斷變動的 UI 資料。舉例來說,您可能會尋找可Player.isPlaying以生命週期感知方式collect的流程。或是類似 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(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 實作各種 UI 狀態,可協助開發人員不必擔心學習 Player.Events