Android O の Picture in Picture を試してみる

# 概要

Android　**O**　のPreview版が出ましたね。
色々な機能が追加されましたが、中でも目玉の一つなのが今までTVのみの対応だったPictureInPicture機能がスマホ、タブレットでも使用できるようになったことです。正式リリースが楽しみですが、一足先にSampleAppを読みながら使い方を見ていきたいと思います。

# PinPの最小構成

PinPを実現するのはとても簡単です。
ManifestのPinPをさせたいActivityにsupportsPictureInPictureを定義してやり、ActivityからenterPictureInPictureMode()を実行するだけです。

```AndroidManifest.xml
<activity android:name=".MainActivity"
          android:supportsPictureInPicture="true">
```

```MainActivity.java
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(view -> {
    enterPictureInPictureMode();
});
```

![PinP前](https://qiita-image-store.s3.amazonaws.com/0/154578/133d5bb6-a504-9589-4b01-c101c09e8713.png)　　![PinP後](https://qiita-image-store.s3.amazonaws.com/0/154578/6ace19a3-cce3-3937-8109-bb3af0a1c6a6.png)

見ての通り、PinP時にはActivityがそのまま縮小されてしまうため、表示したいViewを限定し、SYSTEM_UI_FLAG_LAYOUT_STABLEを設定してやるなどの対応が必要です。またPinP時には、ワンタップでAction表示、ダブルタップでPinPモード終了とタッチイベントの挙動があらかじめ決められており、Buttonなどの通常のウィジェットのタッチイベントを取得することは出来ません。PinPからのユーザ操作はRemoteAction(後述)を使用してのみ可能になります。

# PinP時のレイヤー

PinP時には、他のアプリやDialogを立ちあげていても最前面に来るようになっています。上から引っ張れるNotificationやSystemPanelは、PinPよりも上に表示されます。

![screen3.png](https://qiita-image-store.s3.amazonaws.com/0/154578/c5bcbe6d-0dd6-322e-f7c5-827c664bce12.png)　　![screen4.png](https://qiita-image-store.s3.amazonaws.com/0/154578/d251535d-2521-134a-df91-d469e57302f4.png)


![screen13.png](https://qiita-image-store.s3.amazonaws.com/0/154578/d563738c-13b2-834c-25f3-55efc9a90da9.png)　　![screen14.png](https://qiita-image-store.s3.amazonaws.com/0/154578/0f576174-0b04-2b39-9c31-a05262640d9a.png)


# PinP時の挙動を設定する

PinP時の挙動は、PictureInPictureArgsを使用して設定することが可能です。
PictureInPictureArgsに設定したい値を登録し、setPictureInPictureArgs(PictureInPictureArgs)を使用してActivityにセットします。設定できる内容は、以下の通りです。

 - PinP時のアスペクト比
 - RemoteAction

## アスペクト比の変更

PinP時のViewのアスペクト比を変更するには、setAspectRatio(float)を使用します。
画像は左から、0.5、1、2.35で指定した時の画像です。指定できるのは、　**0.5 ~ 2.35** までで、それ以外を指定すると以下のエラー吐いて落ちました。

![screen8.png](https://qiita-image-store.s3.amazonaws.com/0/154578/cc945a11-df13-895d-ae28-a2d1bb1307fc.png)　　![screen7.png](https://qiita-image-store.s3.amazonaws.com/0/154578/10fda7af-1314-0d78-5073-d4a7a0ad6a06.png)　　![screen10.png](https://qiita-image-store.s3.amazonaws.com/0/154578/a3f22e4b-f70e-ab0a-3168-64142c8c9e3d.png)

```
E/AndroidRuntime(18463): java.lang.IllegalArgumentException: 
        setPictureInPictureArgs: Aspect ratio is too extreme 
        (must be between 0.500000 and 2.350000).
```


# RemoteActionを定義する

PinP時には、通常のウィジェットを使用することが出来ず、RemoteActionを使用して専用のアクションを設定してやる必要があります。設定したアクションは、PinPのViewを一度タップすると表示されます。左上に表示される削除ボタンは、デフォルトで表示されるものです。


RemoteActionの定義には、引数として以下を設定します。

 - Icon　:　PinP時に表示されるアイコン。
 - String　:　タイトル。
 - String　:　説明。
 - PendingIntent　:　アイコン押下時に投げられるIntentを定義する。

定義したRemoteoActionクラスは、PictureInPictureArgsに付与して、Activityに渡すことで、PinP時に使用することができます。

```java
final ArrayList<RemoteAction> actions = new ArrayList<>();
final PendingIntent intent = PendingIntent.getBroadcast(MainActivity.this,
        REQUEST_CODE_ACTION_HOGE_A, new Intent(ACTION_HOGE)
        .putExtra(EXTRA_CONTROL_TYPE, CONTROL_TYPE_A), 0);
final Icon icon = Icon.createWithResource(MainActivity.this, R.drawable.ic_info_24dp);
actions.add(new RemoteAction(icon, "title", "content description", intent));

pictureInPictureArgs.setActions(actions);
setPictureInPictureArgs(pictureInPictureArgs);
```

Activityでは、onPictureInPictureModeChanged(boolean) を使用して、PinPになった時のイベントを取得できます。PinPになったタイミングで専用のReceiverを起動して、アクションを受け取るようにします。

```java
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
    super.onPictureInPictureModeChanged(isInPictureInPictureMode);
    if (isInPictureInPictureMode) {
        receiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                if (intent == null || !ACTION_HOGE.equals(intent.getAction())) {
                    return;
                }

                final int controlType = intent.getIntExtra(EXTRA_CONTROL_TYPE, 0);
                switch (controlType) {
                    case CONTROL_TYPE_A:
                        // action hoge A
                        break;
                }
            }
        };
        registerReceiver(receiver, new IntentFilter(ACTION_HOGE));
    } else {
        unregisterReceiver(receiver);
        receiver = null;
    }
}
```

設定できるアクションは３つまでで、４つ以上設定しようとすると起動時に落ちました。

```
E/AndroidRuntime( 2924): java.lang.IllegalArgumentException: 
        setPictureInPictureArgs: Invalid number ofpicture-in-picture actions.  
        Only a maximum of 3 actions allowed
```

# 気をつけポイント

 - あくまでActivityなので、ライフサイクルや使い方に制限がある
 - アスペクト比は変更可能だが、Viewそのものの大きさは変えれない
 - 別アプリで複数同時のPinPはできない
 - 表示されるレイヤーに注意が必要

# まとめ

動画を再生しながらのPinPとか楽しいですよね。
まだまだ制限も多いし、動作も不安定ですが、思った以上にテンションが上がったので皆様も是非試して見てください。