Compose 動画プレーヤーを使用してアプリにピクチャー イン ピクチャー(PIP)を追加する

ピクチャー イン ピクチャー(PIP)は特別なタイプのマルチウィンドウ モードであり、主に 動画再生などがあります。ユーザーは、固定された小さなウィンドウで動画を視聴できます。 アプリ間の移動やコンテンツのブラウジング中に アクセスできます。

PIP は、Android 7.0 で使用可能になったマルチウィンドウ API を利用して、固定された動画オーバーレイ ウィンドウを提供します。アプリに PIP を追加するには、 必要に応じてアクティビティを PIP モードに切り替えて アクティビティが PIP モードのときは、動画の再生が継続されます。

このガイドでは、Compose の動画を使用して Compose の PIP をアプリに追加する方法について説明します。 説明します。Socialite アプリでおすすめのアプリをチェック 実践していきます。

PIP 用にアプリを設定する

AndroidManifest.xml ファイルのアクティビティ タグで、次の操作を行います。

  1. supportsPictureInPicture を追加して true に設定すると、 アプリの PIP を使用します。
  2. 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 コードで、次の操作を行います。

  1. この拡張機能を 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 を追加する手順は次のとおりです。

  1. バージョン ゲートを追加して、このコードにアクセスできるのはバージョン O~R のみにします。
  2. Context をキーとして DisposableEffect を使用します。
  3. DisposableEffect 内で、コマンドが呼び出されたときの動作を onUserLeaveHintProvider はラムダを使用してトリガーされます。ラムダで、 findActivity()enterPictureInPictureMode() を指定し、渡す PictureInPictureParams.Builder().build()
  4. findActivity() を使用して addOnUserLeaveHintListener を追加し、ラムダを渡します。
  5. 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 info", "API does not support PiP")
}

Android 12 以降で休暇時の PIP アプリを追加

Android 12 以降では、PictureInPictureParams.Builder は アプリの動画プレーヤーに渡される修飾子です。

  1. modifier を作成し、それに対して onGloballyPositioned を呼び出します。レイアウト 座標は後のステップで使用します。
  2. PictureInPictureParams.Builder() の変数を作成します。
  3. SDK が S 以降かどうかを確認する if ステートメントを追加します。スワイプ操作で PiP モードに入るようにするには、ビルダーに setAutoEnterEnabled を追加して true に設定します。この方法では、1 行 1 行を enterPictureInPictureMode
  4. findActivity() を使用して setPictureInPictureParams() を呼び出します。build() に発信: builder を作成して渡します。

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 ウィンドウに入ります。 PIP モードでの UI の表示方法を指定します。

まず、アプリが 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() を使用して、表示する UI 要素を切り替えることができます。 アプリが PIP モードに入ったとき:

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 より前)

  1. 12 より前の PIP を追加するには DisposableEffect を使用するため、作成する必要があります。 newValue を設定して rememberUpdatedState で新しい変数を作成します。 使用します。これにより、更新されたバージョンが DisposableEffect
  2. OnUserLeaveHintListener が呼び出されたときの動作を定義するラムダで、 の呼び出しを囲む状態変数を指定した if ステートメントを enterPictureInPictureMode():

    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 info", "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 への移行後に確認できます

  1. setSourceRectHint()builder に追加するのは、その状態が「 アプリが PIP モードになります。これにより、アプリの実行時に sourceRect が計算されなくなる PIP を入力する必要はありません
  2. sourceRect 値を設定するには、指定された layoutCoordinates を使用します。 修飾子の onGloballyPositioned 関数から取得できます。
  3. buildersetSourceRectHint() を呼び出し、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 Player を使用している場合、Player が null でないこと、および Player の アスペクト比を設定する前の動画サイズが 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

  1. ブロードキャスト コントロールの定数を追加します。
    // 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
  2. PIP ウィンドウのコントロールの RemoteActions のリストを作成します。
  3. 次に、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)
            }
        }
    }
  4. リモート アクションのリストを 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 を追加する際のベスト プラクティスについて学習しました。