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


はじめに

通知の件数表示などに使える BadgeDrawableBadge - Material Design)ですが、サクッと使用することができ非常に便利です。

↓のように書くと…

bottomNavigationView.getOrCreateBadge(R.id.navigation_home).apply {
    number = 10
}

HomeのタブにBadgeが付きます。

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


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

↓ "This is home Fragment" の右にあるBadgeが BadgeDrawableImageView で表示してみたものです。

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

おもむろに使ってみるが…

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

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

↓ Badgeが表示されない。

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

BottomNavigationViewのgetOrCreateBadge(int)を追ってみる

そこで、 BottomNavigationView のBadgeを表示するメソッドである getOrCreateBadge(int) を追ってみると、
BadgeUtilsattachBadgeDrawable というメソッドが呼ばれ、その中でさらに
setBadgeDrawableBounds というメソッドが呼ばれているのがわかります。

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

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

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を加えつつ、 BadgeDrawableImageView で使うことができたコードが下記になります。

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 を指定するのは違和感がありますが、これで動いたので問題は無いようでした。

(2)

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

offsetなし offsetあり

ImageViewbackground に色を付けた様子。

その他

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

android:clipChildren="false"
android:clipToPadding="false"

表示された

以上の工程で、なんとか BadgeDrawableImageView で使うことができました。
ImageView だとアニメーションなども簡単に適用できるので、いい感じです。

まとめ

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

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

参考