文章マジック

media3-ui-compose ライブラリは、Jetpack Compose でメディア UI を構築するための基盤となるコンポーネントを提供します。これは、media3-ui-compose-material3 ライブラリで提供されるものよりも詳細なカスタマイズが必要なデベロッパー向けに設計されています。このページでは、コア コンポーネントと状態ホルダーを使用してカスタム メディア プレーヤー UI を作成する方法について説明します。

Material3 とカスタムの Compose コンポーネントを組み合わせる

media3-ui-compose-material3 ライブラリは柔軟性を重視して設計されています。UI のほとんどには事前構築済みのコンポーネントを使用できますが、より詳細な制御が必要な場合は、1 つのコンポーネントをカスタム実装に置き換えることができます。ここで 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 AndroidViewSurfaceViewTextureView をラップする未加工のサーフェス。

Player コンポーザブルをカスタマイズする

media3-ui-compose-material3 ライブラリを使用する場合、Player コンポーザブルは完全なメディア再生エクスペリエンスを提供します。コンテンツ スロットに独自のコンポーザブルを指定してレイアウトをカスタマイズすることも、PlayerDefaults オブジェクトを利用して TopControlsCenterControlsBottomControlsErrorOverlay などの UI の特定の部分をカスタマイズすることもできます。

たとえば、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 状態では、ボタンのようなコンポーザブルで消費される可能性が高いと想定されます。

状態 remember*State タイプ
PlayPauseButtonState rememberPlayPauseButtonState 2-Toggle
PreviousButtonState rememberPreviousButtonState 定数
NextButtonState rememberNextButtonState 定数
RepeatButtonState rememberRepeatButtonState 3-Toggle
ShuffleButtonState rememberShuffleButtonState 2-Toggle
PlaybackSpeedState rememberPlaybackSpeedState メニューまたは N-Toggle

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 を使用してサーフェスを選択したアスペクト比にスケーリングし(その他のタイプについてはコンテンツのスケールを参照)、presentationState.coverSurface を使用してサーフェスを表示するタイミングが適切でない場合を把握できます。この場合、サーフェスの準備が整うと消える不透明なシャッターをサーフェスの最前面に配置できます。ContentFrame を使用すると、シャッターを後続のラムダとしてカスタマイズできますが、デフォルトでは、親コンテナのサイズを埋める黒い @Composable Box になります。

Flow はどこにありますか?

多くの Android デベロッパーは、Kotlin の Flow オブジェクトを使用して常に変化する UI データを収集することに慣れています。たとえば、ライフサイクル対応の方法で collect できる Player.isPlaying フローを探しているとします。または、Player.eventsFlow のようなものを使用して、filter を自由に Flow<Player.Events> できます。

ただし、Player UI 状態にフローを使用することには、いくつかの欠点があります。主な懸念事項の 1 つは、データ転送の非同期性です。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 ツリーの複数の場所で作成でき、常にプレーヤーの状態の一貫したビューを維持します。
  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 を使用してキャッチします。これは、コルーチンの世界に入り、Player.Events を無期限にリッスンできる suspend fun です。Media3 のさまざまな UI 状態の実装により、エンド デベロッパーは Player.Events について学習する必要がなくなります。