Skip to content

Commit 00ffe5e

Browse files
committed
Begin integrating BadgeDrawable into BottomNavigationView.
TODO: - update javadocs to provide guidance on how to call Badging API. - Save badge states. - Support displaying badges when bottom navigation item doesn't show an icon. PiperOrigin-RevId: 242675939
1 parent fd2a6f2 commit 00ffe5e

9 files changed

Lines changed: 461 additions & 5 deletions

File tree

lib/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ dependencies {
2828
def srcDirs = [
2929
'com/google/android/material/animation',
3030
'com/google/android/material/appbar',
31+
'com/google/android/material/badge',
3132
'com/google/android/material/behavior',
3233
'com/google/android/material/bottomappbar',
3334
'com/google/android/material/bottomnavigation',

lib/java/com/google/android/material/badge/BadgeUtils.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,20 @@ public static void detachBadgeDrawable(
112112
anchor.getOverlay().remove(badgeDrawable);
113113
}
114114
}
115+
116+
/**
117+
* Sets the bounds of a BadgeDrawable to match its associated anchor. For API 18+, the
118+
* BadgeDrawable will match the bounds of its anchor. For pre-API 18, the BadgeDrawable will match
119+
* the bounds of its anchor's FrameLayout ancestor.
120+
*/
121+
public static void setBadgeDrawableBounds(
122+
BadgeDrawable badgeDrawable, View anchor, FrameLayout preApi18BadgeParent) {
123+
Rect badgeBounds = new Rect();
124+
if (VERSION.SDK_INT < VERSION_CODES.JELLY_BEAN_MR2) {
125+
preApi18BadgeParent.getDrawingRect(badgeBounds);
126+
} else {
127+
anchor.getDrawingRect(badgeBounds);
128+
}
129+
badgeDrawable.setBounds(badgeBounds);
130+
}
115131
}

lib/java/com/google/android/material/bottomnavigation/BottomNavigationItemView.java

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,15 @@
2424
import android.content.res.ColorStateList;
2525
import android.content.res.Resources;
2626
import android.graphics.drawable.Drawable;
27+
import android.os.Build.VERSION;
28+
import android.os.Build.VERSION_CODES;
29+
import androidx.annotation.ColorInt;
2730
import androidx.annotation.NonNull;
2831
import androidx.annotation.Nullable;
2932
import androidx.annotation.RestrictTo;
3033
import androidx.annotation.StyleRes;
34+
import com.google.android.material.badge.BadgeDrawable;
35+
import com.google.android.material.badge.BadgeUtils;
3136
import androidx.core.content.ContextCompat;
3237
import androidx.core.graphics.drawable.DrawableCompat;
3338
import 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

Comments
 (0)