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

自訂 Player 可組合函式

使用 media3-ui-compose-material3 程式庫時,Player 可組合函式可提供完整的媒體播放體驗。您可以將自己的可組合函式提供給內容插槽,藉此自訂版面配置,也可以運用 PlayerDefaults 物件自訂 UI 的特定部分,例如 TopControlsCenterControlsBottomControlsErrorOverlay

舉例來說,您可以使用 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
  )
}

UI 狀態容器

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

如要進一步瞭解如何運用 UI 狀態持有者和可組合函式的彈性,請參閱「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),
  )
}

視覺輸出狀態容器

CurrentMediaItemState 提供目前播放的 MediaItem 相關資訊,而 PlaylistState 則會公開播放器上設定的 MediaItems 相關資訊。這些屬性可用於在自訂 UI 中顯示中繼資料資訊。

ErrorState 提供播放器的目前錯誤狀態相關資訊,可用於顯示錯誤疊加層。

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.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 的時機是否不適當。在這種情況下,您可以在表面上放置不透明的快門,表面準備就緒時,快門就會消失。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