Androidの通知をカスタムする(API21以降)

皆さんGoogle Homeは購入しましたか？

自分は miniだけ購入して、音楽再生や天気を教えてもらうなどデフォルトの機能を使っています。

音楽再生にはSpotifyで連携しています。Google Homeで再生中のとき、スマホに再生中の通知が表示されます。

今回のテーマはその通知です。
Google Homeが表示する通知が地味にリッチなので真似出来ないかなと思い、実装方法を調査しました。

こんな通知。展開したり折りたたむと、アニメーションします。

![spotify.gif](https://qiita-image-store.s3.amazonaws.com/0/2783/ddc8f5ec-4755-afd3-9a22-b72b8bd3f039.gif)

## NotificationCompat.Style
通知の種類はいくつか用意されています。

`BigTextStyle` などは馴染みがあると思いますが、今回のSpotifyの通知は `MediaStyle` というものでした。

音楽系のサービスで利用されている `MediaStyle` と、通知領域をカスタムできる `DecoratedCustomViewStyle` と `DecoratedMediaCustomViewStyle` について紹介します。

`Style` を変更するには `NoficationCompat.Builder` で `Style` を設定します。

```kotlin
builder.setStyle(NotificationCompat.BigTextStyle().also { 
    // Styleならではの記述
})
```

## MediaStyle

`MediaStyle` はSupport Library 26.0.0から `v4 media-compat library` に移ったようです。なので依存関係に追加が必要です。

```groovy
implementation "com.android.support:support-media-compat:27.0.2"
```

Styleは以下のようになります。 __`android.support.v4.media.app`からimportする__ので注意が必要です。`android.support.v4.app` と `NotificationCompat` が被ります。

```groovy
builder.setStyle(android.support.v4.media.app.NotificationCompat.MediaStyle()
                        .setShowActionsInCompactView(0)
                )
```

`MediaSession` が必要かと思いきや、無くても利用可能です。

通常の通知と異なり、 `NotificationCompat.Builder#addAction` で__追加したActionは、通知領域の中に表示__されます。

`MediaStyle#setShowActionsInCompactView` の引数で、`compact` 状態のときに表示するActionを指定できます。追加したAction順にintで指定します。

以下の例は、 `setShowActionsInCompactView(0)` で、Actionを3つ追加したものです。

![media.gif](https://qiita-image-store.s3.amazonaws.com/0/2783/1edb7901-69fc-49c0-9ad5-ca908cb7da9e.gif)

## DecoratedCustomViewStyle
通知のViewは `RemoteViews` を使うことでカスタムできますが、__バージョン間や端末による違いなどを考慮する必要__がでてきます。また、__Actionが正しく表示されなくなります__。

これらの問題を解決しつつ、カスタムできるStyleが `DecoratedCustomViewStyle` です。

```kotlin
builder.setStyle(NotificationCompat.DecoratedCustomViewStyle())
```

カスタムViewを適応できる状態は3種類あります。

- ContentView
    - 通知領域で折りたたまれてる状態です。
    - こちらを変更するには`NotificationCompat.Builder#setCustomContentView`を使います。
    - __`NotificationCompat.Builder#setContent` は内部的には `Deprecated` になっている__ので利用しないようにしましょう。

- BigContentView
    - 通知領域で展開されてる状態です。
    - こちらを変更するには`NotificationCompat.Builder#setCustomBigContentView`を使います。

- HeadsUpContentView
    - HeadsUp通知で表示されてる状態です。
    - こちらを__設定していない場合は `ContentView` が表示__されます。
    - こちらを変更するには`NotificationCompat.Builder#setCustomHeadsUpContentView`を使います。
    - HeadUp通知の最大領域は、Nougat以降では[148dp](https://github.com/aosp-mirror/platform_frameworks_base/blob/master/packages/SystemUI/res/values/dimens.xml#L93)になるようです。
        - それ以上カスタムViewで表示させようとすると見切れます。

比較するとわかりますが、デフォルトで用意される領域は残しつつカスタムViewが適応できる形です。

|DecoratedCustomViewStyle|デフォルト|
|:---:|:---:|
|![decorate.png](https://qiita-image-store.s3.amazonaws.com/0/2783/894f57b7-4a86-9d8e-b586-e2cfa59de866.png)|![no_decorate.png](https://qiita-image-store.s3.amazonaws.com/0/2783/2f3d37e2-4b20-6609-5f89-9c65c11c38ac.png)|


## DecoratedMediaCustomViewStyle
`DecoratedMediaCustomViewStyle` も `MediaStyle` と同様に、 `v4 media-compat library` から利用します。

Styleは以下のようになります。 

```kotlin
builder.setStyle(android.support.v4.media.app.NotificationCompat.DecoratedMediaCustomViewStyle()
                        .setShowActionsInCompactView(0)
                )
```

変更できる3つのViewを設定した状態が次のような状態です。

![decorate_media.gif](https://qiita-image-store.s3.amazonaws.com/0/2783/08e8e247-9f52-1595-9aa8-43645ca4e8a9.gif)

Actionがアイコンで表示されるため、若干テキストで表示できる領域が大きいです。

## OSバージョンによる違い
エミュレータで試した結果なので利用する端末によっては違う可能性があります。

|L|M|N|O|
|:---:|:---:|:---:|:---:|
|![color_lollipop.png](https://qiita-image-store.s3.amazonaws.com/0/2783/b4485389-c6e7-c79d-1c17-37868cabf905.png)|![color_marshmallow.png](https://qiita-image-store.s3.amazonaws.com/0/2783/156749df-dabf-eed0-03b6-298df65a98b3.png)|![color_nougat.png](https://qiita-image-store.s3.amazonaws.com/0/2783/f80ae901-f1ca-f258-26e5-2ec618b80445.png)|![color_oreo.png](https://qiita-image-store.s3.amazonaws.com/0/2783/dae35c5f-b005-01ee-abfd-e1ac957eba88.png)|

### HeadsUp通知の条件
Oreoでは、Channelの設定で`IMPORTANCE_HIGH`になってないと__通知のPriorityを変えたり、LEDや通知音を設定してもHeadsUpとして表示されません__。

### 背景色の変更
OreoからはViewのカスタムをしなくても[背景色を変更](https://developer.android.com/about/versions/oreo/android-8.0.html#ux)できます。
`NotificationCompat.Builder#setColorized` で変更できますが、__以下のどちらかを満たす必要があります。__

- ServiceからのForeground通知
- `MediaStyle` or `DecoratedMediaCustomViewStyle` で `MediaSession` を設定する

逆にNougatだと、どちらを設定しても背景色はそのままです。カスタムViewで背景色を変更すれば可能です。

Marshmallowだと、 `NotificationCompat.Builder#setColor` だけで背景色がつきます。

Lollipopだと、Marshmallowと挙動似ていますがHeads Up通知には背景色がつきません。

###Heads Up通知の文字色
カスタムViewを当てた場合、__MarshmallowとLollipopだと、Heads Up通知はデフォルトで白になる__ ので変更する際は注意が必要です。

###mipmapを使うとクラッシュする
Oreoで __`NotificationCompat.Builder#setSsmallIcon` に `mipmap` のリソースを利用するとSystem UIがクラッシュします。__

System UIを再起動しても、__通知が残っている限りクラッシュ__し続けます。

## まとめ
Inboxのアプリ以外でも `InboxStyle` を使っても良いように、__メディア再生を伴わないアプリで `MediaStyle` を使っても__問題ありません。

__通知領域からアイコンでユーザにアクションを促したい場合__などはうまく使えると思います。NougatからActionで画像が利用できなくなったので、代替出来ると思います。

DecoratedなStyleを利用すれば、OSに馴染みつつカスタマイズできます。しかし、乱用してオレオレUIにして、ただただ他のアプリより目立つ風にしてしまうではなく、[デザインガイドライン](https://material.io/guidelines/patterns/notifications.html)を読んで、意義に成り立つようなUIを設計してください。

リポジトリはこちらです。

https://github.com/sakebook/NotificationAnimationSample

### 参考
[Revision 26.0.0 Release / Recent Support Library Revisions | Android Developers](https://developer.android.com/topic/libraries/support-library/revisions.html#26-0-0)

[Android N の通知 / Google Developers Japan](https://developers-jp.googleblog.com/2016/06/android-n.html)

[Heads-up notifications
 / Android Developers](https://developer.android.com/guide/topics/ui/notifiers/notifications.html#Heads-up)

[Android 8.0 の機能と API / Android Developers](https://developer.android.com/about/versions/oreo/android-8.0.html#ux)

[Android Oreo Notification Crashes System UI / Stack Overflow](https://stackoverflow.com/questions/47368187/android-oreo-notification-crashes-system-ui)

[Patterns– Notifications / Material Design](https://material.io/guidelines/patterns/notifications.html)