media3-ui-compose ライブラリは、Jetpack Compose でメディア UI を構築するための基盤となるコンポーネントを提供します。これは、media3-ui-compose-material3 ライブラリで提供されるよりも多くのカスタマイズを必要とするデベロッパー向けに設計されています。このページでは、コア コンポーネントと状態ホルダーを使用してカスタム メディア プレーヤー UI を作成する方法について説明します。
Material3 とカスタム Compose コンポーネントを組み合わせる
media3-ui-compose-material3 ライブラリは柔軟性を重視して設計されています。UI のほとんどに事前構築済みのコンポーネントを使用できますが、より細かく制御する必要がある場合は、1 つのコンポーネントをカスタム実装に置き換えることができます。ここで media3-ui-compose ライブラリの出番となります。
たとえば、Material3 ライブラリの標準の PreviousButton と NextButton を使用したいが、完全にカスタムの 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 の SurfaceView と TextureView をラップする未加工のサーフェス。 |
Player コンポーザブルをカスタマイズする
media3-ui-compose-material3 ライブラリを使用する場合、Player コンポーザブルは完全なメディア再生エクスペリエンスを提供します。コンテンツ スロットに独自のコンポーザブルを指定してレイアウトをカスタマイズすることも、PlayerDefaults オブジェクトを利用して UI の特定の部分(TopControls、CenterControls、BottomControls、ErrorOverlay など)をカスタマイズすることもできます。
図 1 は、Player コンポーザブルがスロットを配置する場所を示しています。図 2 に、レイヤが描画される順序を示します。
Player コンポーザブルのスロット。
中央と下部のコントロールには、PlayerDefaults.CenterControls と PlayerDefaults.BottomControls のスロットが表示されます。topControls、above、below スロットはデフォルトで空です。Player コンポーザブルのレイヤ。描画される順に番号が付けられています。たとえば、PlayerDefaults.CenterControls を使用すると、中央の再生/一時停止ボタンのみをオーバーライドし、中央のコントロールの残りの部分はそのままにできます。また、topControls などのパラメータに完全にカスタムのコンポーザブルを渡し、bottomControls などの他のパラメータを完全に省略してデフォルトのままにすることもできます。
@Composable fun CustomPlayerSlots(player: Player, showControls: Boolean, modifier: Modifier = Modifier) { Player( player = player, modifier = modifier, showControls = showControls, topControls = { // Fully custom top controls Text("My custom title") }, centerControls = { // Use default CenterControls but override the central button PlayerDefaults.CenterControls( player = player, central = { // A custom play/pause button Material3PlayPauseButton(player, 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 を使用して Surface を選択したアスペクト比にスケーリングし(その他のタイプについてはコンテンツ スケールをご覧ください)、presentationState.coverSurface を使用して Surface を表示するタイミングが適切でないことを把握できます。この場合、サーフェスの上に不透明なシャッターを配置できます。このシャッターは、サーフェスの準備が整うと消えます。@Composable BoxContentFrame では、シャッターを末尾のラムダとしてカスタマイズできますが、デフォルトでは、親コンテナのサイズを埋める黒い @Composable Box になります。
Flows はどこにありますか?
多くの 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 状態ホルダークラスは、次の処理を行います。
Playerオブジェクトを受け取ります。- コルーチンを使用して
Playerに登録します。詳しくは、Player.listenをご覧ください。 - 内部状態を更新することで、特定の
Player.Eventsに応答します。 - 適切な
Player更新に変換されるビジネス ロジック コマンドを受け入れます。 - UI ツリーの複数の場所で作成でき、プレーヤーの状態の一貫したビューを常に維持します。
- コンポーザブルで使用できる Compose
Stateフィールドを公開して、変更に動的に対応できるようにします。 - コンポジション間でインスタンスを記憶するための
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 です。さまざまな UI 状態の Media3 実装により、エンド デベロッパーは Player.Events について学習する必要がなくなります。