【Android】BadgeDrawableをImageViewでなんとか使ってみる方法

# はじめに

通知の件数表示などに使える `BadgeDrawable` （[Badge - Material Design](https://material.io/develop/android/components/badging)）ですが、サクッと使用することができ非常に便利です。

↓のように書くと…

```kotlin
bottomNavigationView.getOrCreateBadge(R.id.navigation_home).apply {
    number = 10
}
```
HomeのタブにBadgeが付きます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/995/75add244-29e2-443e-e193-8ad5d0fd7c4a.png" width="300" />


ただ、現状（material-components-android v1.2.1）だと `BottomNavigationView` か `TabLayout` からしかサクッと使用することができません。
例えば `BottomNavigationView` で使っているBadgeと同じ見た目のものを他の箇所でも使いたいという場合にこまってしまいます。

---

本記事では、`BadgeDrawable` を `ImageView` でなんとか使ってみる方法をご紹介したいと思います。

↓ "This is home Fragment" の右にあるBadgeが `BadgeDrawable` を `ImageView` で表示してみたものです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/995/540f0813-42b3-df43-8653-7b1b742f3074.png" width="300" />

※ Material Componentsは `com.google.android.material:material:1.2.1` で確認しています。
※ Badgeに関しては https://qiita.com/youmeee/items/75795f5314d852d74485 の記事が非常に参考になりました。 :pray: 

# おもむろに使ってみるが…
`BadgeDrawable` には `create(Context)` というそれっぽいメソッドがあるのですが、それを使って `BadgeDrawable` のインスタンスを生成し、 `ImageView` にセットしてみても何も表示されません。

```kotlin
val badgeImageView: ImageView = ...
val badgeDrawable = BadgeDrawable.create(context).apply {
    number = 10
}
badgeImageView.setImageDrawable(badgeDrawable)
```

↓ Badgeが表示されない。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/995/cf43c506-641f-3d4d-71e1-fdbd46dd7c74.png" width="300" />

表示されない理由としては、 `create(Context)`しただけでは `Drawable` の **サイズ（bounds）がセットされていない** ためです。

# BottomNavigationViewのgetOrCreateBadge(int)を追ってみる
そこで、 `BottomNavigationView` のBadgeを表示するメソッドである `getOrCreateBadge(int)` を追ってみると、
[BadgeUtils](https://github.com/material-components/material-components-android/blob/34df6d91f1164ec981616346e7b675c1e69d4134/lib/java/com/google/android/material/badge/BadgeUtils.java)の[attachBadgeDrawable](https://github.com/material-components/material-components-android/blob/34df6d91f1164ec981616346e7b675c1e69d4134/lib/java/com/google/android/material/badge/BadgeUtils.java#L72) というメソッドが呼ばれ、その中でさらに
[setBadgeDrawableBounds](https://github.com/material-components/material-components-android/blob/34df6d91f1164ec981616346e7b675c1e69d4134/lib/java/com/google/android/material/badge/BadgeUtils.java#L108) というメソッドが呼ばれているのがわかります。

**この `setBadgeDrawableBounds` というメソッドがサイズの計算をしているところとなります。**

`BadgeUtils` は `@RestrictTo(Scope.LIBRARY)` アノテーションが付いているため、そのまま使うとLintの警告が出てしまいます。幸いメソッドの中身の処理は複雑ではないので、それを参考にしてサイズの計算を実現させれば良さそうです。

```java:BadgeUtils.java
  public static void setBadgeDrawableBounds(
      @NonNull BadgeDrawable badgeDrawable,
      @NonNull View anchor,
      @NonNull FrameLayout compatBadgeParent) {
    Rect badgeBounds = new Rect();
    View badgeParent = USE_COMPAT_PARENT ? compatBadgeParent : anchor;
    badgeParent.getDrawingRect(badgeBounds);
    badgeDrawable.setBounds(badgeBounds);
    badgeDrawable.updateBadgeCoordinates(anchor, compatBadgeParent);
  }
```

# ImageViewに表示させる
細かいworkaroundを加えつつ、 `BadgeDrawable` を `ImageView` で使うことができたコードが下記になります。

```kotlin
val badgeImageView: ImageView = ...
val badgeDrawable = BadgeDrawable.create(context).apply {
    number = 10
    // (1)
    val badgeBounds = Rect()
    badgeImageView.getDrawingRect(badgeBounds)
    this.bounds = badgeBounds
    this.updateBadgeCoordinates(badgeImageView, null)
    // (2)
    this.horizontalOffset = (-6).dp
    this.verticalOffset = 8.dp
}
badgeImageView.setImageDrawable(badgeDrawable)
```
## (1)
`setBadgeDrawableBounds` の内部でやっている処理を同じように実行しています。
`setBadgeDrawableBounds` ではSDKバージョンによる分岐がありますが省いています。 `compatBadgeParent` もnullとしています。Badgeのアンカーに `badgeImageView` を指定するのは違和感がありますが、これで動いたので問題は無いようでした。 :thinking: 

## (2)
workaround感満載ですが、描画の位置がズレてしまうようだったのでoffsetを調整しています。

|offsetなし|offsetあり|
|:------------:|:------------:|
|![スクリーンショット 2020-12-03 0.49.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/995/976d3244-d979-7493-552e-c6243e8c0e5c.png)|![スクリーンショット 2020-12-03 1.03.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/995/c198434a-a205-eeed-9ff2-27eddad08c6f.png)
↑ `ImageView` の `background` に色を付けた様子。

## その他
Badgeを表示する場合、描画が欠けないように、親レイアウトにclippingの指定も必要になりました。

```xml
android:clipChildren="false"
android:clipToPadding="false"
```

## 表示された
以上の工程で、なんとか `BadgeDrawable` を `ImageView` で使うことができました。
`ImageView` だとアニメーションなども簡単に適用できるので、いい感じです。
![1606963647.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/995/b1bcbab9-8b28-6951-db6d-2a9046c33c2d.gif)



# まとめ
`BadgeDrawable` を `ImageView` でなんとか使ってみる方法をご紹介しました。
workaroundなどもありつつも、 `ImageView` にBadgeを表示することができ、アニメーションや細かい位置調整なども可能になるので、使用の幅が広がるのではないかと思います。

`BadgeDrawable` についてアドバイスなどありましたら、コメントいただけると嬉しいです。 :pray: 

# 参考
https://stackoverflow.com/a/60010998