ピクチャー イン ピクチャー(PIP)は、主に動画の再生に使用される特別なタイプのマルチウィンドウ モードです。ユーザーは、画面の隅に固定された小さなウィンドウで動画を視聴しながら、メイン画面でアプリ間を移動したり、コンテンツをブラウジングしたりできます。
PIP は、Android 7.0 で使用可能になったマルチウィンドウ API を利用して、固定された動画オーバーレイ ウィンドウを提供します。PIP をアプリに追加するには、アクティビティを登録し、必要に応じてアクティビティを PIP モードに切り替えて、アクティビティが PIP モードのときに UI 要素が非表示になり、動画の再生が続行されるようにする必要があります。
このガイドでは、Compose の動画実装を使用して、Compose の PIP をアプリに追加する方法について説明します。これらのベスト プラクティスの実例については、Socialite アプリをご覧ください。
PIP 用にアプリを設定する
AndroidManifest.xml
ファイルのアクティビティ タグで、次の操作を行います。
- アプリで PIP を使用することを宣言するには、
supportsPictureInPicture
を追加してtrue
に設定します。 configChanges
を追加してorientation|screenLayout|screenSize|smallestScreenSize
に設定すると、アクティビティがレイアウト構成の変更を処理するように指定できます。こうすることで、PIP モードの遷移中にレイアウト変更が発生しても、アクティビティが再起動されなくなります。<activity android:name=".SnippetsActivity" android:exported="true" android:supportsPictureInPicture="true" android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize" android:theme="@style/Theme.Snippets">
Compose コードで、次の操作を行います。
- この拡張機能を
Context
に追加してください。アクティビティにアクセスするには、ガイド全体を通してこの拡張機能を複数回使用します。internal fun Context.findActivity(): ComponentActivity { var context = this while (context is ContextWrapper) { if (context is ComponentActivity) return context context = context.baseContext } throw IllegalStateException("Picture in picture should be called in the context of an Activity") }
Android 12 より前のバージョンで休暇アプリに PIP を追加する
Android 12 より前のバージョン用に PIP を追加するには、addOnUserLeaveHintProvider
を使用します。Android 12 より前のバージョンで PIP を追加する手順は次のとおりです。
- バージョン ゲートを追加して、このコードに R までバージョン O でのみアクセスできるようにします。
Context
をキーとしてDisposableEffect
を使用します。DisposableEffect
内で、ラムダを使用してonUserLeaveHintProvider
がトリガーされたときの動作を定義します。ラムダで、findActivity()
に対してenterPictureInPictureMode()
を呼び出し、PictureInPictureParams.Builder().build()
を渡します。findActivity()
を使用してaddOnUserLeaveHintListener
を追加し、ラムダを渡します。onDispose
で、findActivity()
を使用してremoveOnUserLeaveHintListener
を追加し、ラムダを渡します。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Build.VERSION.SDK_INT < Build.VERSION_CODES.S ) { val context = LocalContext.current DisposableEffect(context) { val onUserLeaveBehavior: () -> Unit = { context.findActivity() .enterPictureInPictureMode(PictureInPictureParams.Builder().build()) } context.findActivity().addOnUserLeaveHintListener( onUserLeaveBehavior ) onDispose { context.findActivity().removeOnUserLeaveHintListener( onUserLeaveBehavior ) } } } else { Log.i(PIP_TAG, "API does not support PiP") }
Android 12 以降で休暇アプリに PIP を追加する
Android 12 以降、PictureInPictureParams.Builder
は、アプリの動画プレーヤーに渡される修飾子を介して追加されます。
modifier
を作成し、それに対してonGloballyPositioned
を呼び出します。レイアウト座標は後のステップで使用します。PictureInPictureParams.Builder()
の変数を作成します。- SDK が S 以降かどうかを確認する
if
ステートメントを追加します。その場合は、setAutoEnterEnabled
をビルダーに追加してtrue
に設定し、スワイプで PIP モードを開始します。これにより、enterPictureInPictureMode
を実行するよりもスムーズなアニメーションになります。 findActivity()
を使用してsetPictureInPictureParams()
を呼び出します。builder
に対してbuild()
を呼び出し、それを渡します。
val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> val builder = PictureInPictureParams.Builder() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setAutoEnterEnabled(true) } context.findActivity().setPictureInPictureParams(builder.build()) } VideoPlayer(pipModifier)
ボタンで PIP を追加する
ボタンを押して PIP モードに切り替えるには、findActivity()
で enterPictureInPictureMode()
を呼び出します。
パラメータは以前の PictureInPictureParams.Builder
の呼び出しで設定済みであるため、ビルダーに新しいパラメータを設定する必要はありません。ただし、ボタンのクリック時にパラメータを変更する場合は、ここで設定できます。
val context = LocalContext.current Button(onClick = { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.findActivity().enterPictureInPictureMode( PictureInPictureParams.Builder().build() ) } else { Log.i(PIP_TAG, "API does not support PiP") } }) { Text(text = "Enter PiP mode!") }
PIP モードで UI を処理する
PIP モードに入ると、UI を PIP モードで、どのように見えるかを指定しない限り、アプリの UI 全体が PIP ウィンドウに入ります。
まず、アプリが PIP モードになっているかどうかを把握する必要があります。そのためには、OnPictureInPictureModeChangedProvider
を使用します。次のコードは、アプリが PIP モードかどうかを示しています。
@Composable fun rememberIsInPipMode(): Boolean { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val activity = LocalContext.current.findActivity() var pipMode by remember { mutableStateOf(activity.isInPictureInPictureMode) } DisposableEffect(activity) { val observer = Consumer<PictureInPictureModeChangedInfo> { info -> pipMode = info.isInPictureInPictureMode } activity.addOnPictureInPictureModeChangedListener( observer ) onDispose { activity.removeOnPictureInPictureModeChangedListener(observer) } } return pipMode } else { return false } }
これで、rememberIsInPipMode()
を使用して、アプリが PIP モードになったときに表示する UI 要素を切り替えられるようになりました。
val inPipMode = rememberIsInPipMode() Column(modifier = modifier) { // This text will only show up when the app is not in PiP mode if (!inPipMode) { Text( text = "Picture in Picture", ) } VideoPlayer() }
アプリが適切なタイミングで PIP モードに入るようにしてください
次のような場合は、アプリを PIP モードにしないでください。
- 動画が停止または一時停止した場合。
- アプリのページと動画プレーヤーが異なる。
アプリが PIP モードに入るタイミングを制御するには、mutableStateOf
を使用して、動画プレーヤーの状態を追跡する変数を追加します。
動画が再生されているかどうかに応じて状態を切り替える
動画プレーヤーが再生されているかどうかに基づいて状態を切り替えるには、動画プレーヤーにリスナーを追加します。プレーヤーが再生しているかどうかに基づいて、状態変数の状態を切り替えます。
player.addListener(object : Player.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) { shouldEnterPipMode = isPlaying } })
プレーヤーが離されたかどうかに基づいて状態を切り替える
プレーヤーが離されたら、状態変数を false
に設定します。
fun releasePlayer() { shouldEnterPipMode = false }
状態を使用して PIP モードに入るかどうかを定義する(Android 12 より前)
- PIP 12 より前のバージョンを追加する場合、
DisposableEffect
を使用するため、newValue
を状態変数として、rememberUpdatedState
によって新しい変数を作成する必要があります。これにより、更新されたバージョンがDisposableEffect
内で使用されます。 OnUserLeaveHintListener
がトリガーされたときの動作を定義するラムダで、enterPictureInPictureMode()
の呼び出しの周りに状態変数を含むif
ステートメントを追加します。val currentShouldEnterPipMode by rememberUpdatedState(newValue = shouldEnterPipMode) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Build.VERSION.SDK_INT < Build.VERSION_CODES.S ) { val context = LocalContext.current DisposableEffect(context) { val onUserLeaveBehavior: () -> Unit = { if (currentShouldEnterPipMode) { context.findActivity() .enterPictureInPictureMode(PictureInPictureParams.Builder().build()) } } context.findActivity().addOnUserLeaveHintListener( onUserLeaveBehavior ) onDispose { context.findActivity().removeOnUserLeaveHintListener( onUserLeaveBehavior ) } } } else { Log.i(PIP_TAG, "API does not support PiP") }
状態を使用して PIP モードに入るかどうかを定義する(Android 12 以降)
状態変数を setAutoEnterEnabled
に渡して、適切なタイミングでアプリが PIP モードに入るようにします。
val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> val builder = PictureInPictureParams.Builder() // Add autoEnterEnabled for versions S and up if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setAutoEnterEnabled(shouldEnterPipMode) } context.findActivity().setPictureInPictureParams(builder.build()) } VideoPlayer(pipModifier)
setSourceRectHint
を使用してスムーズなアニメーションを実装する
setSourceRectHint
API は、PIP モードに入る際のアニメーションをより滑らかにします。Android 12 以降では、PIP モードを終了するときに、よりスムーズなアニメーションも作成されます。この API を PiP ビルダーに追加して、PIP への移行後に表示されるアクティビティの領域を指定します。
setSourceRectHint()
をbuilder
に追加するのは、アプリが PIP モードに入る必要があることを状態で定義されている場合のみです。これにより、アプリが PIP に入る必要がないときにsourceRect
が計算されなくなります。sourceRect
値を設定するには、修飾子のonGloballyPositioned
関数から渡されたlayoutCoordinates
を使用します。builder
に対してsetSourceRectHint()
を呼び出し、sourceRect
変数を渡します。
val context = LocalContext.current val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> val builder = PictureInPictureParams.Builder() if (shouldEnterPipMode) { val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect() builder.setSourceRectHint(sourceRect) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setAutoEnterEnabled(shouldEnterPipMode) } context.findActivity().setPictureInPictureParams(builder.build()) } VideoPlayer(pipModifier)
setAspectRatio
を使用して PIP ウィンドウのアスペクト比を設定する
PIP ウィンドウのアスペクト比を設定するには、特定のアスペクト比を選択するか、プレーヤーの動画サイズの幅と高さを使用します。media3 プレーヤーを使用している場合は、アスペクト比を設定する前に、プレーヤーが null でないことと、プレーヤーの動画サイズが VideoSize.UNKNOWN
と等しくないことを確認してください。
val context = LocalContext.current val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> val builder = PictureInPictureParams.Builder() if (shouldEnterPipMode && player != null && player.videoSize != VideoSize.UNKNOWN) { val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect() builder.setSourceRectHint(sourceRect) builder.setAspectRatio( Rational(player.videoSize.width, player.videoSize.height) ) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setAutoEnterEnabled(shouldEnterPipMode) } context.findActivity().setPictureInPictureParams(builder.build()) } VideoPlayer(pipModifier)
カスタム プレーヤーを使用している場合は、プレーヤーに固有の構文を使用して、プレーヤーの高さと幅のアスペクト比を設定します。初期化中にプレーヤーのサイズ変更を行ったときに、そのアスペクト比が有効なアスペクト比の範囲を外れると、アプリがクラッシュすることに注意してください。media3 プレーヤーの場合と同様に、アスペクト比を計算できるタイミングに関するチェックの追加が必要になる場合があります。
リモート アクションを追加する
PIP ウィンドウにコントロール(再生、一時停止など)を追加する場合は、追加するコントロールごとに RemoteAction
を作成します。
- ブロードキャスト コントロール用の定数を追加します。
// Constant for broadcast receiver const val ACTION_BROADCAST_CONTROL = "broadcast_control" // Intent extras for broadcast controls from Picture-in-Picture mode. const val EXTRA_CONTROL_TYPE = "control_type" const val EXTRA_CONTROL_PLAY = 1 const val EXTRA_CONTROL_PAUSE = 2
- PIP ウィンドウのコントロール用に
RemoteActions
のリストを作成します。 - 次に、
BroadcastReceiver
を追加し、onReceive()
をオーバーライドして、各ボタンのアクションを設定します。DisposableEffect
を使用してレシーバとリモート アクションを登録します。プレーヤーが不要になったら、レシーバーの登録を解除します。@RequiresApi(Build.VERSION_CODES.O) @Composable fun PlayerBroadcastReceiver(player: Player?) { val isInPipMode = rememberIsInPipMode() if (!isInPipMode || player == null) { // Broadcast receiver is only used if app is in PiP mode and player is non null return } val context = LocalContext.current DisposableEffect(player) { val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if ((intent == null) || (intent.action != ACTION_BROADCAST_CONTROL)) { return } when (intent.getIntExtra(EXTRA_CONTROL_TYPE, 0)) { EXTRA_CONTROL_PAUSE -> player.pause() EXTRA_CONTROL_PLAY -> player.play() } } } ContextCompat.registerReceiver( context, broadcastReceiver, IntentFilter(ACTION_BROADCAST_CONTROL), ContextCompat.RECEIVER_NOT_EXPORTED ) onDispose { context.unregisterReceiver(broadcastReceiver) } } }
- リモート アクションのリストを
PictureInPictureParams.Builder
に渡します。val context = LocalContext.current val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> val builder = PictureInPictureParams.Builder() builder.setActions( listOfRemoteActions() ) if (shouldEnterPipMode && player != null && player.videoSize != VideoSize.UNKNOWN) { val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect() builder.setSourceRectHint(sourceRect) builder.setAspectRatio( Rational(player.videoSize.width, player.videoSize.height) ) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setAutoEnterEnabled(shouldEnterPipMode) } context.findActivity().setPictureInPictureParams(builder.build()) } VideoPlayer(modifier = pipModifier)
次のステップ
このガイドでは、Android 12 以前と Android 12 以降の両方で Compose に PIP を追加するベスト プラクティスを学習しました。
- Compose PIP の実際のベスト プラクティスについては、Socialite アプリをご覧ください。
- 詳しくは、PIP 設計ガイダンスをご覧ください。