ピクチャー イン ピクチャー(PIP)を使って動画を追加する

Compose の方法を試す
Jetpack Compose は、Android で推奨される UI ツールキットです。Compose でピクチャー イン ピクチャーをサポートする方法について学習します。

Android 8.0(API レベル 26)以降では、アクティビティをピクチャー イン ピクチャー(PIP)モードで起動できます。PIP は特別なタイプのマルチウィンドウ モードで、主に動画の再生に使用されます。ユーザーは、メイン画面でアプリ間を移動したりコンテンツをブラウジングしたりしながら、画面の隅に固定された小さなウィンドウで動画を視聴し続けることができます。

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

PIP ウィンドウは、画面の最上位レイヤ(システムによって選択された隅)に表示されます。

PIP は、Android 14(API レベル 34)以降を搭載した互換性のある Android TV OS デバイスでもサポートされています。多くの類似点がありますが、テレビの PiP を使用する場合は考慮すべき点がいくつかあります。

ユーザーが PIP ウィンドウを操作する方法

ユーザーは、PIP ウィンドウを別の位置にドラッグできます。Android 12 以降では、次のことも行えます。

  • PIP ウィンドウをシングルタップすると、全画面表示への切り替え、閉じるボタン、設定ボタン、アプリが提供するカスタム アクション(再生コントロールなど)が表示されます。

  • ウィンドウをダブルタップして、現在の PIP サイズと、最大または最小の PIP サイズを切り替えます。たとえば、最大化したウィンドウをダブルタップすると最小化され、その逆も同様です。

  • ウィンドウを左端または右端にドラッグして退避状態にします。ウィンドウの退避を解除するには、退避中のウィンドウの表示されている部分をタップするか、ドラッグして引き出します。

  • ピンチ操作で PIP ウィンドウのサイズを変更できます。

現在のアクティビティが PIP モードに入るタイミングは、アプリで制御します。次に例を示します。

  • ユーザーがホームボタンをタップするか、画面を下から上にスワイプしてホーム画面に移動したら、アクティビティを PIP モードに切り替えます。Google マップは、ユーザーが別のアクティビティを同時に実行している間、この方法でルートを表示し続けます。

  • ユーザーが動画から他のコンテンツに移動してブラウジングを開始したら、動画を PIP モードに切り替えます。

  • ユーザーが見ている動画コンテンツがエピソードの終盤に差し掛かったら、動画を PIP モードに切り替えます。メイン画面には、シリーズの次のエピソードに関する宣伝情報やあらすじを表示します。

  • ユーザーが動画を見ながら他のコンテンツをキューに追加できるようにします。PIP モードで動画を再生し続けながら、メイン画面にコンテンツ選択アクティビティを表示します。

PiP のサポートを宣言する

デフォルトでは、システムはアプリによる PIP の使用を自動的にサポートすることはありません。アプリで PIP をサポートするには、android:supportsPictureInPicturetrue に設定することにより、動画アクティビティをマニフェストに登録します。また、アクティビティでレイアウト設定の変更を処理するように指定し、PiP モードへの移行中にレイアウト変更が発生した場合でもアクティビティが再起動しないようにします。

<activity android:name="VideoActivity"
    android:supportsPictureInPicture="true"
    android:configChanges=
        "screenSize|smallestScreenSize|screenLayout|orientation"
    ...

アクティビティを PiP に切り替える

Android 12 以降では、setAutoEnterEnabled フラグを true に設定することで、アクティビティを PiP モードに切り替えることができます。この設定では、onUserLeaveHintenterPictureInPictureMode() を明示的に呼び出す必要がなく、アクティビティは必要に応じて自動的に PIP モードに切り替わります。さらに 非常にスムーズな遷移を実現できるという 利点もあります詳しくは、ジェスチャー ナビゲーションでの PIP モードへの遷移をよりスムーズにするをご覧ください。

Android 11 以前をターゲットとしている場合、アクティビティは enterPictureInPictureMode() を呼び出して PiP モードに切り替える必要があります。たとえば、次のコードは、ユーザーがアプリ UI の専用ボタンをクリックすると、アクティビティを PIP モードに切り替えます。

Kotlin

override fun onActionClicked(action: Action) {
    if (action.id.toInt() == R.id.lb_control_picture_in_picture) {
        activity?.enterPictureInPictureMode()
        return
    }
}

Java

@Override
public void onActionClicked(Action action) {
    if (action.getId() == R.id.lb_control_picture_in_picture) {
        getActivity().enterPictureInPictureMode();
        return;
    }
    ...
}

アクティビティをバックグラウンドに移行する代わりに PIP モードに切り替えるロジックを含めることもできます。たとえば、Google マップは、ユーザーがホームボタンまたは最近ボタンを押して別のアプリに移動すると、PIP モードに切り替わります。このようなケースをキャッチするには、次のように onUserLeaveHint() をオーバーライドします。

Kotlin

override fun onUserLeaveHint() {
    if (iWantToBeInPipModeNow()) {
        enterPictureInPictureMode()
    }
}

Java

@Override
public void onUserLeaveHint () {
    if (iWantToBeInPipModeNow()) {
        enterPictureInPictureMode();
    }
}

推奨: 洗練された PIP 移行エクスペリエンスをユーザーに提供する

Android 12 では、全画面表示ウィンドウと PIP ウィンドウ間のアニメーション トランジションの外観が大幅に改善されました。該当する変更をすべて実装することを強くおすすめします。実装すると、追加の作業なしで、これらの変更は折りたたみ式デバイスやタブレットなどの大画面に自動的にスケーリングされます。

アプリに該当するアップデートが含まれていない場合、PIP 遷移は引き続き機能しますが、アニメーションの洗練度は低くなります。たとえば、全画面表示から PIP モードに移行すると、遷移中に PIP ウィンドウが消え、遷移が完了すると再表示されます。

これらの変更には、次のものが含まれます。

  • ジェスチャー ナビゲーションでの PIP モードへの遷移をよりスムーズにする
  • PIP モードの開始と終了のための適切な sourceRectHint の設定
  • 動画以外のコンテンツのシームレスなサイズ変更を無効にする

洗練された遷移エクスペリエンスを有効にするには、Android Kotlin PictureInPicture サンプルを参照してください。

ジェスチャー ナビゲーションでの PIP モードへの遷移をよりスムーズにする

Android 12 以降では、setAutoEnterEnabled フラグにより、ジェスチャー ナビゲーションを使用して PIP モードで動画コンテンツに遷移するアニメーションが大幅にスムーズになります(全画面表示から上にスワイプしてホーム画面に移動する場合など)。

この変更を行う手順は次のとおりです。参考として、こちらのサンプルもご覧ください。

  1. setAutoEnterEnabled を使用して PictureInPictureParams.Builder を作成します。

    Kotlin

    setPictureInPictureParams(PictureInPictureParams.Builder()
        .setAspectRatio(aspectRatio)
        .setSourceRectHint(sourceRectHint)
        .setAutoEnterEnabled(true)
        .build())
    

    Java

    setPictureInPictureParams(new PictureInPictureParams.Builder()
        .setAspectRatio(aspectRatio)
        .setSourceRectHint(sourceRectHint)
        .setAutoEnterEnabled(true)
        .build());
    
  2. 最新の PictureInPictureParams を使って、早い段階で setPictureInPictureParams を呼び出します。アプリは onUserLeaveHint コールバックを待たないようにします(Android 11 ではコールバックを待っていました)。

    たとえば、最初の再生と、アスペクト比が変更された場合は以降のすべての再生で、setPictureInPictureParams を呼び出す可能性があります。

  3. setAutoEnterEnabled(false) を呼び出すのは、必要な場合に限ります。たとえば、現在の再生が一時停止状態の場合、PIP に切り替える必要はありません。

PiP モードの開始と終了に適切な sourceRectHint を設定する

Android 8.0 で PiP が導入されて以降、setSourceRectHint は、ピクチャー イン ピクチャーへの遷移後に表示されるアクティビティの領域(動画プレーヤーの動画ビューの境界など)を示します。

Android 12 では、システムは sourceRectHint を使用して、PIP モードの開始と終了の両方でよりスムーズなアニメーションを実装します。

PIP モードの開始と終了について sourceRectHint を適切に設定するには:

  1. 適切な境界を sourceRectHint として使用して PictureInPictureParams を作成します。動画プレーヤーにレイアウト変更リスナーを接続することもおすすめします。

    Kotlin

    val mOnLayoutChangeListener =
    OnLayoutChangeListener { v: View?, oldLeft: Int,
            oldTop: Int, oldRight: Int, oldBottom: Int, newLeft: Int, newTop:
            Int, newRight: Int, newBottom: Int ->
        val sourceRectHint = Rect()
        mYourVideoView.getGlobalVisibleRect(sourceRectHint)
        val builder = PictureInPictureParams.Builder()
            .setSourceRectHint(sourceRectHint)
        setPictureInPictureParams(builder.build())
    }
    
    mYourVideoView.addOnLayoutChangeListener(mOnLayoutChangeListener)
    

    Java

    private final View.OnLayoutChangeListener mOnLayoutChangeListener =
            (v, oldLeft, oldTop, oldRight, oldBottom, newLeft, newTop, newRight,
            newBottom) -> {
        final Rect sourceRectHint = new Rect();
        mYourVideoView.getGlobalVisibleRect(sourceRectHint);
        final PictureInPictureParams.Builder builder =
            new PictureInPictureParams.Builder()
                .setSourceRectHint(sourceRectHint);
        setPictureInPictureParams(builder.build());
    };
    
    mYourVideoView.addOnLayoutChangeListener(mOnLayoutChangeListener);
    
  2. 必要に応じて、システムが終了遷移を開始する前に sourceRectHint を更新します。システムが PIP モードを終了しようとすると、アクティビティのビュー階層が遷移先の構成(全画面など)に配置されます。アプリはレイアウト変更リスナーをルートビューまたはターゲット ビュー(動画プレーヤー ビューなど)にアタッチしてイベントを検出し、アニメーションの開始前に sourceRectHint を更新できます。

    Kotlin

    // Listener is called immediately after the user exits PiP but before animating.
    playerView.addOnLayoutChangeListener { _, left, top, right, bottom,
                        oldLeft, oldTop, oldRight, oldBottom ->
        if (left != oldLeft
            || right != oldRight
            || top != oldTop
            || bottom != oldBottom) {
            // The playerView's bounds changed, update the source hint rect to
            // reflect its new bounds.
            val sourceRectHint = Rect()
            playerView.getGlobalVisibleRect(sourceRectHint)
            setPictureInPictureParams(
                PictureInPictureParams.Builder()
                    .setSourceRectHint(sourceRectHint)
                    .build()
            )
        }
    }
    
    

    Java

    // Listener is called right after the user exits PiP but before animating.
    playerView.addOnLayoutChangeListener((v, left, top, right, bottom,
                        oldLeft, oldTop, oldRight, oldBottom) -> {
        if (left != oldLeft
            || right != oldRight
            || top != oldTop
            || bottom != oldBottom) {
            // The playerView's bounds changed, update the source hint rect to
            // reflect its new bounds.
            final Rect sourceRectHint = new Rect();
            playerView.getGlobalVisibleRect(sourceRectHint);
            setPictureInPictureParams(
                new PictureInPictureParams.Builder()
                    .setSourceRectHint(sourceRectHint)
                    .build());
        }
    });
    
    

動画以外のコンテンツのシームレスなサイズ変更を無効にする

Android 12 では、setSeamlessResizeEnabled フラグが追加されました。このフラグにより、PIP ウィンドウで動画以外のコンテンツのサイズを変更する際に、よりスムーズなクロスフェード アニメーションを提供できるようになりました。以前は、PIP ウィンドウで動画以外のコンテンツのサイズを変更すると、視覚的なアーティファクトが表示されていました。

動画以外のコンテンツのシームレスなサイズ変更を無効にするには、次のようにします。

Kotlin

setPictureInPictureParams(PictureInPictureParams.Builder()
    .setSeamlessResizeEnabled(false)
    .build())

Java

setPictureInPictureParams(new PictureInPictureParams.Builder()
    .setSeamlessResizeEnabled(false)
    .build());

PIP 中の UI を処理する

アクティビティがピクチャー イン ピクチャー(PIP)モードを開始または終了すると、システムは Activity.onPictureInPictureModeChanged() または Fragment.onPictureInPictureModeChanged() を呼び出します。

Android 15 では、PIP モードへの移行をさらにスムーズにするための変更が導入されています。これは、メイン UI の上に UI 要素が重ねられているアプリ(PiP に移行するアプリ)に役立ちます。

デベロッパーは onPictureInPictureModeChanged() コールバックを使用して、重ねて表示される UI 要素の可視性を切り替えるロジックを定義します。このコールバックは、PiP の開始または終了アニメーションが完了するとトリガーされます。Android 15 以降、PictureInPictureUiState クラスに新しい状態が追加されました。

この新しい UI 状態により、Android 15 をターゲットとするアプリは、PiP アニメーションが開始されるとすぐに isTransitioningToPip()Activity#onPictureInPictureUiStateChanged() コールバックが呼び出されることを確認します。PIP モードのアプリに関係のない UI 要素が多数あります。たとえば、おすすめ、公開予定の動画、評価、タイトルなどの情報を含むビューやレイアウトなどです。アプリが PiP モードに入ると、onPictureInPictureUiStateChanged() コールバックを使用してこれらの UI 要素を非表示にします。アプリが PiP ウィンドウから全画面モードに移行する場合は、次の例に示すように、onPictureInPictureModeChanged() コールバックを使用してこれらの要素を非表示にします。

Kotlin

override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) {
        if (pipState.isTransitioningToPip()) {
          // Hide UI elements.
        }
    }

Java

@Override
public void onPictureInPictureUiStateChanged(PictureInPictureUiState pipState) {
        if (pipState.isTransitioningToPip()) {
          // Hide UI elements.
        }
    }

Kotlin

override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
        if (isInPictureInPictureMode) {
          // Unhide UI elements.
        }
    }

Java

@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
        if (isInPictureInPictureMode) {
          // Unhide UI elements.
        }
    }

無関係な UI 要素(PIP ウィンドウ用)のこのクイック ビジュアリティー切り替えにより、PIP の開始アニメーションをスムーズかつちらつきなく行うことができます。

これらのコールバックをオーバーライドして、アクティビティの UI 要素を再描画します。PIP モードでは、アクティビティが小さなウィンドウに表示されることに注意してください。アプリが PIP モードの場合、ユーザーはアプリの UI 要素を操作できず、小さな UI 要素の詳細が見えにくくなる可能性があります。動画再生アクティビティを最小限の UI で操作すると、ユーザー エクスペリエンスが向上します。

アプリで PIP 用のカスタム アクションを提供する必要がある場合は、このページのコントロールを追加するをご覧ください。他の UI 要素は、アクティビティを PIP に切り替える前に削除し、全画面表示に戻すときに復元するようにします。

コントロールを追加する

PIP ウィンドウでは、ユーザーが(モバイル デバイスでウィンドウをタップするか、テレビリモコンでメニューを選択することにより)ウィンドウのメニューを開いたときに、コントロールを表示できます。

アプリにアクティブなメディア セッションがある場合は、「再生」、「一時停止」、「次へ」、「前へ」の各コントロールが表示されます。

また、あらかじめ PictureInPictureParams.Builder.setActions()PictureInPictureParams を設定してカスタム アクションを明示的に指定しておき、PIP モードに入るときに enterPictureInPictureMode(android.app.PictureInPictureParams) または setPictureInPictureParams(android.app.PictureInPictureParams) を使用してそのパラメータを渡すこともできます。ここで注意が必要なのは、getMaxNumPictureInPictureActions() を超える数のアクションを追加しようとしても、最大数のアクションしか追加できないという点です。

PIP の状態で動画再生を続行する

アクティビティが PIP に切り替わるとき、システムはアクティビティを一時停止状態にして、アクティビティの onPause() メソッドを呼び出します。PIP モードへの移行中にアクティビティが一時停止された場合、動画の再生を一時停止せず、再生を続行する必要があります。

Android 7.0 以降では、システムがアクティビティの onStop() または onStart() を呼び出したとき、アプリで動画再生の一時停止または再開を行う必要があります。これにより、アプリが PIP モードで onPause() になっているかを確認したり、明示的に再生を継続したりする必要がなくなります。

setAutoEnterEnabled フラグを true に設定しておらず、onPause() の実装で再生を一時停止する必要がある場合は、isInPictureInPictureMode() を呼び出して PIP モードかどうかを確認し、再生を適切に処理します。例:

Kotlin

override fun onPause() {
    super.onPause()
    // If called while in PiP mode, do not pause playback.
    if (isInPictureInPictureMode) {
        // Continue playback.
    } else {
        // Use existing playback logic for paused activity behavior.
    }
}

Java

@Override
public void onPause() {
    // If called while in PiP mode, do not pause playback.
    if (isInPictureInPictureMode()) {
        // Continue playback.
        ...
    } else {
        // Use existing playback logic for paused activity behavior.
        ...
    }
}

アクティビティが PIP モードから全画面モードに戻ると、システムはアクティビティを再開して onResume() を呼び出します。

PIP に単一の再生アクティビティを使用する

動画再生アクティビティが PIP モードのときに、ユーザーがアプリのメイン画面でコンテンツをブラウジングしているときに新しい動画を選択することがあります。新しいアクティビティを起動するとユーザーが混乱する可能性があるため、既存の再生アクティビティを全画面モードにして新しい動画を再生します。

動画再生リクエストに対して必ず 1 つのアクティビティが使用され、必要に応じて PIP モードの開始と終了が切り替えられるようにするには、次のように、マニフェストでアクティビティの android:launchModesingleTask に設定します。

<activity android:name="VideoActivity"
    ...
    android:supportsPictureInPicture="true"
    android:launchMode="singleTask"
    ...

アクティビティでは、onNewIntent() をオーバーライドして新しい動画を処理し、必要に応じて既存の動画再生を停止します。

おすすめの方法

RAM が少ないデバイスでは、PIP が無効になっている場合があります。アプリで PIP を使用する前に、hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) を呼び出して PIP が使用可能であることを確認してください。

PIP は、フルスクリーン動画を再生するアクティビティを対象としています。アクティビティを PIP モードに切り替えるときは、動画以外のコンテンツを表示しないようにしてください。PIP 中の UI を処理するで説明されているように、アクティビティが PIP モードになったタイミングを追跡し、UI 要素を非表示にします。

アクティビティが PIP モードのとき、デフォルトでは入力フォーカスを取得しません。PIP モードのときに入力イベントを受信するには、MediaSession.setCallback() を使用します。setCallback() の使用方法について詳しくは、「この曲なに?」カードを表示するをご覧ください。

アプリが PIP モードの場合、PIP ウィンドウで動画を再生すると、音楽プレーヤー アプリや音声検索アプリなどの別のアプリとの音声干渉が発生する可能性があります。これを回避するには、音声フォーカスの管理で説明されているように、動画の再生を開始するときに音声フォーカスをリクエストし、音声フォーカスの変更通知を処理します。PIP モード中に音声フォーカスを失ったという通知を受け取った場合は、動画再生を一時停止または停止します。

アプリが PIP に入る際には、最上位のアクティビティのみがピクチャー イン ピクチャー モードに入ることに注意してください。ある種の状況では(マルチウィンドウ デバイスなど)、下位のアクティビティが表示されて PIP アクティビティとともにユーザーに再表示される可能性があります。このようなケース(onResume() コールバックまたは onPause() コールバックを取得する下位のアクティビティなど)は、状況に応じて適切に処理する必要があります。また、ユーザーがアクティビティを操作する可能性もあります。たとえば、動画リストのアクティビティを表示状態にして、動画再生のアクティビティを PIP モードにした場合は、ユーザーがリストから新しい動画を選択したら、それに応じて PIP アクティビティを更新する必要があります。

他のサンプルコード

Kotlin で記述されたサンプルアプリをダウンロードするには、Android PictureInPicture サンプル(Kotlin)をご覧ください。