目次

目次

【Android】Picture in Pictureの実装をしてみた

chiaki.kyui
chiaki.kyui
最終更新日2024/08/09 投稿日2024/08/09

はじめに

こんにちは、Androidアプリ開発グループの休井です。
私が担当しているアプリで、MV再生機能とピクチャーインピクチャーの機能を追加することになりました。
動画再生については弊社のいくつかのアプリで実装されていますが、ピクチャーインピクチャーについては今回が初の導入となります。
そこで、今回はピクチャーインピクチャーの実装方法やカスタマイズ方法についてご紹介します。

ピクチャーインピクチャーの概要

Picture in Picture(PiP)モードは、ユーザーが他のアプリを操作しながらビデオを小さなウィンドウで再生できる機能です。
主にビデオ再生アプリで使用されることが多く、ユーザーエクスペリエンスを向上させるための重要な機能です。

前提

以下の依存関係がプロジェクトに追加されていること。

実装方法

  1. 動画を再生する
    まずは、動画を再生する部分の実装をします。
    今回はPiPの実装が目的なので、ローカルに持つMP4ファイルを再生するだけのシンプルな実装にします。
    // MainActivity.kt
    
    private lateinit var exoPlayer: ExoPlayer
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        initializePlayer()
        setContent {
            MaterialTheme {
                VideoPlayer(exoPlayer)
            }
        }
    }
    
    private fun initializePlayer() {
        // ExoPlayerの初期化
        exoPlayer = ExoPlayer.Builder(this).build().apply {
            // MediaItemの作成
            val mediaItem = MediaItem.fromUri("android.resource://${packageName}/${R.raw.video}")
            setMediaItem(mediaItem)
            // 再生準備開始
            prepare()
        }
    }
    
    @Composable
    fun VideoPlayer(player: ExoPlayer) {
        AndroidView(factory = {
            // PlayerViewに作成したExoPlayerをセット
            PlayerView(it).apply {
                this.player = player
            }
        })
    }
    
    こちらを実行すると以下のようになり、動画が再生できるようになります。
    PiP_movie_1.gif
  2. AndroidManifest.xmlの設定
    次に、アプリがPiPモードをサポートできるようにします。
    AndroidManifest.xmlファイルで、動画再生を行うActivityに対して android:supportsPictureInPicture="true"の宣言を追加します。

    <activity>
        android:name=".MainActivity"
        android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
        android:supportsPictureInPicture="true" >
            ...
    </activity>
    
  3. PiPモードの切り替え
    onUserLeaveHint()メソッドをオーバーライドして、ユーザーがアクティビティを離れるときにPiPモードに切り替わるようにします。
    override fun onUserLeaveHint() {
        enterPictureInPictureMode(
            PictureInPictureParams.Builder()
                .setAspectRatio(Rational(16, 9))
                .build()
        )
        super.onUserLeaveHint()
    }
    
    enterPictureInPictureMode()がPiPモードに切り替わるためのメソッドです。
    ここでは、PiPウィンドウのアスペクト比が16:9で表示されるよう設定しました。
    上記処理を追加することで、端末のホームを表示しようとしたときにPiPモードに切り替わるようになりました。
    PiP_movie_2.gif
  4. UIの調整
    PiPモードに切り替わるようになりましたが、PiPで表示したときにUIの崩れが起きてしまいました。
    PiPに切り替わった時に、PiPのウィンドウいっぱいに動画が表示されるよう調整します。
    まず、デフォルトで表示されているアクションバーを非表示にしたいので theme.xmlを以下のようにします。

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <style name="Theme.PictureInPictureSample" parent="android:Theme.Material.Light.NoActionBar" />
    </resources>
    
    これを AndroidManifest.xmlのActivityの宣言に追加します。
    <activity>
        android:name=".MainActivity"
        android:theme="@style/Theme.PictureInPictureSample"
        android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
        android:supportsPictureInPicture="true" >
            ...
    </activity>
    
    これで、アクションバーが非表示になりました。
    PiP_movie_3.gif
    次に、PiPの切り替えでアクションバーの表示/非表示が行われるようにします。
    Activityのメンバ変数としてPiPのStateを持ち、 onPictureInPictureModeChanged()をオーバーライドしてStateの書き換えを行います。
    private var isInPiPModeState = mutableStateOf(false)
    
    override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
        super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
        isInPiPModeState.value = isInPictureInPictureMode
    }
    
    これを使用して、アクションバーの表示制御を行います。
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        initializePlayer()
        setContent {
            val isInPiPMode by isInPiPModeState
            MaterialTheme {
                VideoPlayer(
                    player = exoPlayer,
                    isInPiPMode = isInPiPMode
                )
            }
        }
    }
    
    @kotlin.OptIn(ExperimentalMaterial3Api::class)
    @Composable
    fun VideoPlayer(player: Player, isInPiPMode: Boolean) {
        Scaffold(
            topBar = {
                if (!isInPiPMode) {
                    TopAppBar(
                        title = {
                            Text(text = "PictureInPictureSample")
                        }
                    )
                }
            }
        ) { innerPadding -&gt;
            AndroidView(
                factory = {
                    PlayerView(it).apply {
                        this.player = player
                        playerView = this
                    }
                },
                modifier = Modifier
                    .fillMaxSize()
                    .padding(innerPadding)
            )
        }
    }
    
    これにより、PiPモードに切り替わった時に動画のみが表示されるようになりました。
    PiP_movie_4.gif
  5. カスタムアクションの設定
    AndroidのPiPには、標準動作として設定画面への遷移、フル画面表示、閉じる(D&D又はボタン押下)が備わっています。

    PiP_movie_5.gif
    PiP_movie_6.gif
    アクティブなMediaSessionが存在する場合は、それに加え動画の再生/停止、次へ、前へボタンが表示されるようです。
    これらの標準動作については、現在の仕様では動作を変えたりボタンを消したりという変更は難しそうでしたが、PiPのウィンドウに追加で自前のボタンを設置することは可能です。
    今回は、お気に入りボタンを設置してみたいと思います。 まずは、お気に入りアクション検知用の定数と、BroadCastReceiverを定義します。

    companion object {
        const val ACTION_FAVORITE = "favorite"
    }
    
    class PiPActionReceiver : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            if (intent.action == ACTION_FAVORITE) {
                Toast.makeText(context, "お気に入りに登録したよ!", Toast.LENGTH_SHORT).show()
            }
        }
    }
    
    作成したBroadCastReceiverをManifestに追加します。
    <receiver android:name=".MainActivity$PiPActionReceiver" />
    
    次に、RemoteActionを作成し、PiP起動時にパラメーターにセットします。
    private fun createRemoteAction(
        @DrawableRes iconRes: Int,
        requestCode: Int,
        actionIntent: Intent,
        title: String,
        description: String
    ): RemoteAction {
        val pendingIntent = PendingIntent.getBroadcast(
            this,
            requestCode,
            actionIntent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
        val icon = Icon.createWithResource(this, iconRes)
        return RemoteAction(icon, title, description, pendingIntent)
    }
    
    override fun onUserLeaveHint() {
        val favoriteAction = createRemoteAction(
            R.drawable.ic_favorite,
            1,
            Intent(this, PiPActionReceiver::class.java).apply {
                action = ACTION_FAVORITE
            },
            "Favorite",
            "Add to favorite"
        )
        val params = PictureInPictureParams.Builder()
            .setAspectRatio(Rational(16, 9))
            .setActions(listOf(favoriteAction))
            .build()
        enterPictureInPictureMode(params)
        super.onUserLeaveHint()
     }
    
    これで、PiPのウィンドウにお気に入りボタンが表示され、押下時にトーストを出すことができました。
    PiP_movie_7.gif

まとめ

今回の実装では、動画再生、PiPモードへの切り替え、UIの調整、カスタムアクションの追加といった一連の手順を紹介しました。
導入ということで機能は単純なものを扱いましたが、実際にプロダクトで実装する時はアプリに合わせた追加機能やユーザーがより便利にアプリを使えるよう導線や動作を調整していきたいと思います。
最後までお読みいただきありがとうございました。

chiaki.kyui

目次