Jetpack ピクチャー イン ピクチャー ライブラリを使用する

ピクチャー イン ピクチャー(PiP)Jetpack ライブラリは、Android アプリ デベロッパーが PiP 機能を実装するための合理化された 堅牢なソリューションを提供します。 特に、メディア再生、ビデオ通話、ナビゲーション アプリに適しています。統合された API を提供することで、このライブラリはボイラープレート コードやアプリ内の一般的なバグを排除し、PiP のユーザー エクスペリエンス全体の品質を向上させます。

PiP Jetpack ライブラリは、Android エコシステム全体でいくつかの重要な課題と不整合に対処することで、既存の PiP API を容易にします。

  • OS の断片化: このライブラリは、Android のバージョンによる PiP API 呼び出しの違いを自動的に処理します。たとえば、Android 12 より前では enterPictureInPictureMode を使用し、それ以降では isAutoEnterEnabled を使用します。そのため、デベロッパーがバージョンの違いを管理する必要はありません。
  • PiP パラメータの誤り: PiP パラメータ(setSourceRectHint など)を正しく設定するための統合ソリューションを提供し、メディア再生中にスムーズで高品質なアニメーションを作成できるようにします。
  • 統合された PiP 状態コールバック: onPictureInPictureModeChangedonPictureInPictureUiStateChanged を単一の統合コールバック インターフェース(PictureInPictureDelegate.OnPictureInPictureEventListener)に統合し、状態と UI の管理を簡素化します。
  • ボイラープレート コードの削減: このライブラリは、再生コントロールやビデオ通話アクションなど、一般的なユースケース向けに事前定義された RemoteActions のセットを提供することで、繰り返し使用される ボイラープレート コードの量を削減します。
  • 将来を見据えた設計: PiP のその他の機能は Jetpack ライブラリを通じて提供されるため、採用者は最小限の労力で追加機能に アクセスできます。

移行ワークフロー

アプリのユースケース カテゴリと従来の PiP ロジックを特定します。

カテゴリ: 動画再生、ナビゲーション、ビデオ通話。

特定する従来の PiP ロジック:

  • onUserLeaveHint
  • setAutoEnterEnabled
  • onPictureInPictureModeChanged
  • onPictureInPictureUiStateChanged
  • setPictureInPictureParams

2. AndroidManifest の構成

PiP を開始するアクティビティが、不要な再起動を防ぐために必要な configChanges を使用して AndroidManifest.xml でサポートを宣言していることを確認します。

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

3. 環境設定

必要な依存関係を build.gradle に追加します。

dependencies {
implementation("androidx.core:core:1.18.0")
implementation("androidx.activity:activity:1.13.0")
implementation("androidx.core:core-pip:1.0.0-alpha02") }

依存関係には最新の AndroidX ライブラリを使用し、その情報については リリースページをご覧ください。

4. テンプレートの選択と初期化

アプリのユースケースに最も適した実装テンプレートを選択します。

  • ナビゲーションとビデオ通話: BasicPictureInPicture; 通常、シームレスなサイズ変更はサポートされておらず、ソース rect ヒントは必要ありません。
  • 動画再生: VideoPlaybackPictureInPicture; ソース rect ヒントのプレーヤー ビューの境界を自動的にトラッキングし、デフォルトでシームレスなサイズ変更を有効にします。

Jetpack ライブラリを採用するには、既存のカスタム PiP 実装を Jetpack ライブラリ API に置き換えます。採用の複雑さとコストは、アプリの現在の実装によって異なります。

以降のセクションでは、PiP の一般的なユースケースと必要な実装手順について説明します。

アプリは、ナビゲーションのアクティブまたは非アクティブの状態をライブラリに通知し、アスペクト比を設定します。残りの処理は Jetpack ライブラリが行います。

主な違い:

  1. アプリ側で自動入力と従来の入力とを区別する必要はありません。
  2. 統合されたコールバック インターフェース。
  3. 下位互換性を実現する新しい PictureInPictureParams ビルダー。

ビデオ通話

アプリは、通話のアクティブまたは非アクティブの状態をライブラリに通知し、アスペクト比を設定します。

主な違い:

  1. アプリ側で自動入力と従来の入力とを区別する必要はありません。
  2. 統合されたコールバック インターフェース。
  3. 下位互換性を実現する新しい PictureInPictureParams ビルダー。
  4. ビデオ通話のアクション アイコンの標準化。

5. コードの移行

  • エントリ ロジック: Android 12 以降の場合は setAutoEnterEnabled 、Android 11 以前の場合は onUserLeaveHint など、API 固有のロジックを setEnabled に置き換えます。PiP の利用資格ステータスが変更されるたびに、これをトリガーします。
  • コールバック: onPictureInPictureModeChanged(レイアウトの切り替え)と onPictureInPictureUiStateChanged(アニメーション/状態)を統合して、イベントベースの統合コールバック onPictureInPictureEvent にします。
  • アクションとパラメータ: パラメータが変更されたら、テンプレート インスタンスで setActionssetAspectRatio を使用してパラメータを更新します。
  • 動画の特別な処理: 動画アプリの場合は、setPlayerView を使用してソース rect ヒントの更新を自動化し、スムーズな移行を実現します。 ` ### 6. クリーンアップ

VideoPlaybackPictureInPicture の場合は、closeonDispose または onDestroy で呼び出して、ビュー トラッカーなどのリソースを解放します。

リファレンス実装パターン

実装例。

ナビゲーションとビデオ通話

class NavOrVideoCallJpipActivity : ComponentActivity(), PictureInPictureDelegate.OnPictureInPictureEventListener {
    private lateinit var pictureInPictureImpl: BasicPictureInPicture
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        pictureInPictureImpl = BasicPictureInPicture(this)
        // BasicPictureInPicture is ideal for Navigation and Video call use cases.
        pictureInPictureImpl.addOnPictureInPictureEventListener(
            ContextCompat.getMainExecutor(this),
            this
        )
        setContent {
        }
    }
    override fun onPictureInPictureEvent(
        event: PictureInPictureDelegate.Event,
        config: Configuration?
    ) {
        when (event) {
            PictureInPictureDelegate.Event.ENTERED -> { /* Toggle to PiP layout */ }
            PictureInPictureDelegate.Event.EXITED -> { /* Toggle to Full-screen layout */ }
            PictureInPictureDelegate.Event.STASHED -> { /* Optional: PiP is stashed */ }
            PictureInPictureDelegate.Event.UNSTASHED -> { /* Optional: PiP is unstashed */ }
        }
    }
}

動画再生

class VideoPlaybackJpipActivity : ComponentActivity(), PictureInPictureDelegate.OnPictureInPictureEventListener {
    private lateinit var pictureInPictureImpl: VideoPlaybackPictureInPicture
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        pictureInPictureImpl = VideoPlaybackPictureInPicture(this)
        pictureInPictureImpl.addOnPictureInPictureEventListener(
            ContextCompat.getMainExecutor(this),
            this
        )
        setContent {
            ContentScreen(pictureInPictureImpl)
        }
    }
    override fun onPictureInPictureEvent(
        event: PictureInPictureDelegate.Event,
        config: Configuration?
    ) {
        when (event) {
            PictureInPictureDelegate.Event.ENTER_ANIMATION_START -> { /* Hide overlays */ }
            PictureInPictureDelegate.Event.ENTER_ANIMATION_END -> { /* Animation finished */ }
            PictureInPictureDelegate.Event.ENTERED -> { /* Switch to PiP layout */ }
            PictureInPictureDelegate.Event.STASHED -> { /* PiP stashed */ }
            PictureInPictureDelegate.Event.UNSTASHED -> { /* PiP unstashed */ }
            PictureInPictureDelegate.Event.EXITED -> { /* Return to full-screen */ }
        }
    }

    @Composable
    fun ContentScreen(pipController: VideoPlaybackPictureInPicture) {
        DisposableEffect(pipController) {
            onDispose {
                pipController.close()
            }
        }
    }
}