依存関係を追加する
Media3 ライブラリには、Jetpack Compose ベースの UI モジュールが含まれています。使用するには、次の依存関係を追加します。
Kotlin
implementation("androidx.media3:media3-ui-compose:1.6.0")
Groovy
implementation "androidx.media3:media3-ui-compose:1.6.0"
Compose を優先してアプリを開発するか、ビューから移行することを強くおすすめします。
完全に Compose で記述されたデモアプリ
media3-ui-compose
ライブラリには、ボタン、インジケーター、画像、ダイアログなどのすぐに使えるコンポーザブルは含まれていませんが、AndroidView
で PlayerView
をラップするなどの相互運用性ソリューションを回避する、Compose で完全に記述されたデモアプリがあります。デモアプリは、media3-ui-compose
モジュールの UI 状態ホルダー クラスを使用し、Compose Material3 ライブラリを使用します。
UI 状態ホルダー
UI 状態ホルダーとコンポーザブルの柔軟性をどのように使用できるかについて詳しくは、Compose で状態を管理する方法をご覧ください。
ボタンの状態ホルダー
一部の UI 状態は、ボタンのようなコンポーザブルによって使用される可能性が高いと想定されます。
状態 | remember*State | タイプ |
---|---|---|
PlayPauseButtonState |
rememberPlayPauseButtonState |
2 つの切り替えボタン |
PreviousButtonState |
rememberPreviousButtonState |
定数 |
NextButtonState |
rememberNextButtonState |
定数 |
RepeatButtonState |
rememberRepeatButtonState |
3 トグル |
ShuffleButtonState |
rememberShuffleButtonState |
2 つの切り替えボタン |
PlaybackSpeedState |
rememberPlaybackSpeedState |
メニューまたは N-Toggle |
PlayPauseButtonState
の使用例:
@Composable
fun PlayPauseButton(player: Player, modifier: Modifier = Modifier) {
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),
)
}
}
state
には、再生や一時停止に使用するアイコンなどのテーマ設定情報がないことに注意してください。Player
を UI 状態に変換することのみがその責任です。
次に、好みのレイアウトにボタンを組み合わせます。
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
) {
PreviousButton(player)
PlayPauseButton(player)
NextButton(player)
}
ビジュアル出力の状態ホルダー
PresentationState
には、PlayerSurface
の動画出力を表示できる場合や、プレースホルダ UI 要素で覆う必要がある場合の情報が保持されます。
val presentationState = rememberPresentationState(player)
val scaledModifier = Modifier.resize(ContentScale.Fit, presentationState.videoSizeDp)
Box(modifier) {
// 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 = player,
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
modifier = scaledModifier,
)
if (presentationState.coverSurface) {
// Cover the surface that is being prepared with a shutter
Box(Modifier.background(Color.Black))
}
ここでは、presentationState.videoSizeDp
を使用してサーフェスを目的のアスペクト比にスケーリングし(その他のタイプについては ContentScale のドキュメントをご覧ください)、presentationState.coverSurface
を使用してサーフェスを表示するタイミングが適切でないかどうかを確認できます。この場合、サーフェスの上に不透明なシャッターを配置し、サーフェスの準備が整うと消えるようにできます。
フローとは
多くの 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(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
を使用して Player.Events
をキャッチします。これは、コルーチンの世界に入り、Player.Events
を無期限にリッスンできる suspend fun
です。さまざまな UI 状態の Media3 実装により、エンドデベロッパーは Player.Events
の学習に煩わされることがなくなります。