2424import android .content .res .ColorStateList ;
2525import android .content .res .Resources ;
2626import android .graphics .drawable .Drawable ;
27+ import android .os .Build .VERSION ;
28+ import android .os .Build .VERSION_CODES ;
29+ import androidx .annotation .ColorInt ;
2730import androidx .annotation .NonNull ;
2831import androidx .annotation .Nullable ;
2932import androidx .annotation .RestrictTo ;
3033import androidx .annotation .StyleRes ;
34+ import com .google .android .material .badge .BadgeDrawable ;
35+ import com .google .android .material .badge .BadgeUtils ;
3136import androidx .core .content .ContextCompat ;
3237import androidx .core .graphics .drawable .DrawableCompat ;
3338import androidx .core .view .PointerIconCompat ;
@@ -71,6 +76,13 @@ public class BottomNavigationItemView extends FrameLayout implements MenuView.It
7176 private Drawable originalIconDrawable ;
7277 private Drawable wrappedIconDrawable ;
7378
79+ private BadgeDrawable badgeDrawable ;
80+ private int badgeNumber = BadgeUtils .ICON_ONLY_BADGE_NUMBER ;
81+ private boolean isBadgeVisible ;
82+ private int badgeMaxCount = BadgeUtils .DEFAULT_MAX_BADGE_CHARACTER_COUNT ;
83+ @ ColorInt private int badgeBackgroundColor ;
84+ @ ColorInt private int badgeTextColor ;
85+
7486 public BottomNavigationItemView (@ NonNull Context context ) {
7587 this (context , null );
7688 }
@@ -82,6 +94,9 @@ public BottomNavigationItemView(@NonNull Context context, AttributeSet attrs) {
8294 public BottomNavigationItemView (Context context , AttributeSet attrs , int defStyleAttr ) {
8395 super (context , attrs , defStyleAttr );
8496 final Resources res = getResources ();
97+ // Avoid clipping a badge if it's displayed.
98+ setClipChildren (false );
99+ setClipToPadding (false );
85100
86101 LayoutInflater .from (context ).inflate (R .layout .design_bottom_navigation_item , this , true );
87102 setBackgroundResource (R .drawable .design_bottom_navigation_item_background );
@@ -96,6 +111,35 @@ public BottomNavigationItemView(Context context, AttributeSet attrs, int defStyl
96111 ViewCompat .setImportantForAccessibility (largeLabel , ViewCompat .IMPORTANT_FOR_ACCESSIBILITY_NO );
97112 setFocusable (true );
98113 calculateTextScaleFactors (smallLabel .getTextSize (), largeLabel .getTextSize ());
114+
115+ // TODO: Support displaying a badge on label-only bottom navigation views.
116+ if (icon != null ) {
117+ icon .addOnLayoutChangeListener (
118+ new OnLayoutChangeListener () {
119+ @ Override
120+ public void onLayoutChange (
121+ View v ,
122+ int left ,
123+ int top ,
124+ int right ,
125+ int bottom ,
126+ int oldLeft ,
127+ int oldTop ,
128+ int oldRight ,
129+ int oldBottom ) {
130+ if (icon .getVisibility () == VISIBLE ) {
131+ tryUpdateBadgeDrawableBounds (icon , getCustomParentForBadge (icon ));
132+ }
133+ }
134+ });
135+ }
136+
137+ badgeBackgroundColor =
138+ BadgeDrawable .getDefaultBackgroundColor (
139+ context , attrs , /* defStyleAttr= */ 0 , R .style .Widget_MaterialComponents_Badge );
140+ badgeTextColor =
141+ BadgeDrawable .getDefaultTextColor (
142+ context , attrs , /* defStyleAttr= */ 0 , R .style .Widget_MaterialComponents_Badge );
99143 }
100144
101145 @ Override
@@ -354,4 +398,141 @@ public void setItemBackground(@Nullable Drawable background) {
354398 }
355399 ViewCompat .setBackground (this , background );
356400 }
401+
402+ // TODO: Add getter for badgeBackgroundColor, isBadgeVisible, badgeMaxCharacterCount.
403+
404+ void setBadgeBackgroundColor (@ ColorInt int color ) {
405+ this .badgeBackgroundColor = color ;
406+ if (hasBadgeDrawable ()) {
407+ badgeDrawable .setBackgroundColor (color );
408+ }
409+ }
410+
411+ void setBadgeTextColor (@ ColorInt int color ) {
412+ this .badgeTextColor = color ;
413+ if (hasBadgeDrawable ()) {
414+ badgeDrawable .setBadgeTextColor (color );
415+ }
416+ }
417+
418+ @ ColorInt
419+ int getBadgeTextColor () {
420+ return badgeTextColor ;
421+ }
422+
423+ void setBadgeVisible (boolean isVisible ) {
424+ if (this .isBadgeVisible == isVisible ) {
425+ return ;
426+ }
427+ this .isBadgeVisible = isVisible ;
428+ if (hasBadgeDrawable ()) {
429+ badgeDrawable .setVisible (isBadgeVisible , /* restart= */ false );
430+ if (isBadgeVisible ) {
431+ tryAttachBadgeToAnchor (icon );
432+ } else {
433+ tryRemoveBadgeFromAnchor (icon );
434+ }
435+ } else if (isBadgeVisible ) {
436+ // Only create and populate a new instance of BadgeDrawable if isBadgeVisible == true.
437+ initializeBadgeForIcon ();
438+ }
439+ }
440+
441+ void setBadgeNumber (int number ) {
442+ badgeNumber = number ;
443+ if (hasBadgeDrawable ()) {
444+ badgeDrawable .setNumber (number );
445+ }
446+ }
447+
448+ void clearBadgeNumber () {
449+ badgeNumber = BadgeUtils .ICON_ONLY_BADGE_NUMBER ;
450+ if (hasBadgeDrawable ()) {
451+ badgeDrawable .clearBadgeNumber ();
452+ }
453+ }
454+
455+ int getBadgeNumber () {
456+ // Don't return badgeNumber because it defaults to BadgeUtils.ICON_ONLY_BADGE_NUMBER == -1
457+ return badgeDrawable == null ? 0 : badgeDrawable .getNumber ();
458+ }
459+
460+ void setBadgeMaxCharacterCount (int maxCount ) {
461+ badgeMaxCount = maxCount ;
462+ if (hasBadgeDrawable ()) {
463+ badgeDrawable .setMaxCharacterCount (maxCount );
464+ }
465+ }
466+
467+ private void initializeBadgeForIcon () {
468+ createBadgeDrawable (icon , getCustomParentForBadge (icon ), /* attrs= */ null );
469+ tryAttachBadgeToAnchor (icon );
470+ setupBadge ();
471+ }
472+
473+ private void setupBadge () {
474+ if (!hasBadgeDrawable ()) {
475+ throw new IllegalArgumentException ("Trying to setup a null instance of badgeDrawable." );
476+ }
477+
478+ setBadgeVisible (isBadgeVisible );
479+ setBadgeMaxCharacterCount (badgeMaxCount );
480+ setBadgeBackgroundColor (badgeBackgroundColor );
481+ setBadgeTextColor (badgeTextColor );
482+ if (badgeNumber != BadgeUtils .ICON_ONLY_BADGE_NUMBER ) {
483+ setBadgeNumber (badgeNumber );
484+ }
485+ }
486+
487+ private boolean hasBadgeDrawable () {
488+ return badgeDrawable != null ;
489+ }
490+
491+ private void tryUpdateBadgeDrawableBounds (View anchor , @ Nullable FrameLayout customBadgeParent ) {
492+ if (hasBadgeDrawable ()) {
493+ BadgeUtils .setBadgeDrawableBounds (badgeDrawable , anchor , customBadgeParent );
494+ badgeDrawable .updateBadgeCoordinates (anchor , customBadgeParent );
495+ }
496+ }
497+
498+ @ Nullable
499+ private FrameLayout getCustomParentForBadge (View anchor ) {
500+ if (anchor == icon ) {
501+ return (VERSION .SDK_INT < VERSION_CODES .JELLY_BEAN_MR2 )
502+ ? ((FrameLayout ) icon .getParent ())
503+ : null ;
504+ }
505+ // TODO: Support displaying a badge on label-only bottom navigation views.
506+ return null ;
507+ }
508+
509+ private BadgeDrawable createBadgeDrawable (
510+ View anchor , @ Nullable FrameLayout customBadgeParent , AttributeSet attrs ) {
511+ return badgeDrawable =
512+ BadgeDrawable .createFromAttributes (
513+ anchor ,
514+ customBadgeParent ,
515+ attrs ,
516+ 0 /* defStyleAttr */ ,
517+ R .style .Widget_MaterialComponents_Badge );
518+ }
519+
520+ private void tryAttachBadgeToAnchor (View anchorView ) {
521+ if (!hasBadgeDrawable ()) {
522+ return ;
523+ }
524+ if (anchorView != null ) {
525+ BadgeUtils .attachBadgeDrawable (badgeDrawable , anchorView , getCustomParentForBadge (icon ));
526+ }
527+ }
528+
529+ private void tryRemoveBadgeFromAnchor (View anchorView ) {
530+ if (!hasBadgeDrawable ()) {
531+ return ;
532+ }
533+ if (anchorView != null ) {
534+ BadgeUtils .detachBadgeDrawable (
535+ badgeDrawable , anchorView , getCustomParentForBadge (anchorView ));
536+ }
537+ }
357538}
0 commit comments