ピクチャー イン ピクチャー(PiP)Jetpack ライブラリは、Android アプリ デベロッパーが PiP 機能を実装するための、効率的で堅牢なソリューションを提供します。特に、メディア再生、動画通信、ナビゲーション アプリに適しています。このライブラリは、統合された API を提供することで、ボイラープレート コードや一般的なアプリ内バグを排除し、PiP ユーザー エクスペリエンスの全体的な品質を向上させます。
PiP Jetpack ライブラリは、Android エコシステム全体におけるいくつかの重要な課題と不整合に対処することで、既存の PiP API を容易にします。
- OS の断片化: このライブラリは、さまざまな Android バージョンにおける PiP API 呼び出しの違いを自動的に処理します。たとえば、Android 12 より前では
enterPictureInPictureModeを使用し、それ以降ではisAutoEnterEnabledを使用します。そのため、デベロッパーはバージョンの違いを管理する必要がありません。 - 不適切な PiP パラメータ: PiP パラメータ(
setSourceRectHintなど)を正しく設定するための統合ソリューションを提供し、メディア再生中にスムーズで高品質なアニメーションを作成します。 - 統合された PiP 状態コールバック:
onPictureInPictureModeChangedとonPictureInPictureUiStateChangedを 1 つの統合コールバック インターフェース(PictureInPictureDelegate.OnPictureInPictureEventListener)に統合し、状態と UI の管理を簡素化します。 - ボイラープレート コードの削減: このライブラリは、再生コントロールやビデオ通話アクションなどの一般的なユースケース向けに事前定義された
RemoteActionsのセットを提供することで、繰り返しのボイラープレート コードの量を削減します。 - 将来への対応: Jetpack ライブラリを通じて追加の PiP 機能が提供されるため、導入者は最小限の労力で追加機能にアクセスできます。
Migration Workflow
アプリのユースケース カテゴリと以前の PiP ロジックを特定します。
カテゴリ: 動画再生、ナビゲーション、ビデオ通話。
従来の PiP ロジックによる識別:
onUserLeaveHintsetAutoEnterEnabledonPictureInPictureModeChangedonPictureInPictureUiStateChangedsetPictureInPictureParams。
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. テンプレートの選択と初期化
アプリのユースケースに最適な実装テンプレートを選択します。
- Navigation とビデオ通話:
BasicPictureInPicture。通常、シームレスなサイズ変更はサポートされておらず、ソース矩形ヒントは必要ありません。 - 動画再生:
VideoPlaybackPictureInPicture。ソースの長方形ヒントのプレーヤー ビュー境界を自動的に追跡し、デフォルトでシームレスなサイズ変更を可能にします。
Jetpack ライブラリを導入するには、既存のカスタム PiP 実装を Jetpack ライブラリ API に置き換えます。導入の複雑さと費用は、アプリの現在の実装によって異なります。
以下のセクションでは、ピクチャー イン ピクチャーの一般的なユースケースと、必要な実装手順について説明します。
ナビゲーション
アプリは、ナビゲーションのアクティブ状態または非アクティブ状態をライブラリに通知し、アスペクト比を設定します。残りの処理は Jetpack ライブラリが行います。
主な違い:
- アプリ側で自動入力と従来の入力を区別する必要はありません。
- コールバック インターフェースを統合しました。
- 下位互換性のための新しい
PictureInPictureParamsビルダー。
ビデオ通話
アプリは、通話がアクティブか非アクティブかをライブラリに通知し、アスペクト比を設定します。
主な違い:
- アプリ側で自動入力と従来の入力を区別する必要はありません。
- コールバック インターフェースを統合しました。
- 下位互換性のための新しい
PictureInPictureParamsビルダー。 - ビデオ通話の標準化されたアクション アイコン。
5. コードの移行
- エントリ ロジック: API 固有のロジック(Android 12 以降の場合は
setAutoEnterEnabled、Android 11 以前の場合はonUserLeaveHint)をsetEnabledに置き換えます。PiP の利用資格ステータスが変更されるたびに、これをトリガーします。 - コールバック:
onPictureInPictureModeChanged(レイアウトの切り替え)とonPictureInPictureUiStateChanged(アニメーション/状態)を、統合されたイベントベースのコールバックonPictureInPictureEventに統合します。 - アクションとパラメータ: テンプレート インスタンスの
setActionsとsetAspectRatioを使用して、パラメータが変更されるたびに更新します。
リファレンス実装パターン
実装の例。
ナビゲーションとビデオ通話
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() } } } }