@@ -125,10 +125,10 @@ public class BadgeDrawable extends Drawable implements TextDrawableDelegate {
125125
126126 /** Position the badge can be set to. */
127127 @ IntDef ({
128- TOP_END ,
129- TOP_START ,
130- BOTTOM_END ,
131- BOTTOM_START ,
128+ TOP_END ,
129+ TOP_START ,
130+ BOTTOM_END ,
131+ BOTTOM_START ,
132132 })
133133 @ Retention (RetentionPolicy .SOURCE )
134134 public @interface BadgeGravity {}
@@ -145,18 +145,21 @@ public class BadgeDrawable extends Drawable implements TextDrawableDelegate {
145145 /** The badge is positioned along the bottom and start edges of its anchor view */
146146 public static final int BOTTOM_START = Gravity .BOTTOM | Gravity .START ;
147147
148- /** Maximum value of number that can be displayed in a circular badge. */
149- private static final int MAX_CIRCULAR_BADGE_NUMBER_COUNT = 9 ;
150-
151148 @ StyleRes private static final int DEFAULT_STYLE = R .style .Widget_MaterialComponents_Badge ;
152149 @ AttrRes private static final int DEFAULT_THEME_ATTR = R .attr .badgeStyle ;
153150
154151 /**
155152 * If the badge number exceeds the maximum allowed number, append this suffix to the max badge
156- * number and display is as the badge text instead.
153+ * number and display it as the badge text instead.
157154 */
158155 static final String DEFAULT_EXCEED_MAX_BADGE_NUMBER_SUFFIX = "+" ;
159156
157+ /**
158+ * If the badge string exceeds the maximum allowed number of characters, append this suffix to the
159+ * truncated badge text and display it as the badge text instead.
160+ */
161+ static final String DEFAULT_EXCEED_MAX_BADGE_TEXT_SUFFIX = "…" ;
162+
160163 /**
161164 * The badge offset begins at the edge of the anchor.
162165 */
@@ -181,6 +184,9 @@ public class BadgeDrawable extends Drawable implements TextDrawableDelegate {
181184 /** A value to indicate that a badge radius has not been specified. */
182185 static final int BADGE_RADIUS_NOT_SPECIFIED = -1 ;
183186
187+ /** A value to indicate that badge content should not be truncated. */
188+ public static final int BADGE_CONTENT_NOT_TRUNCATED = -2 ;
189+
184190 @ NonNull private final WeakReference <Context > contextRef ;
185191 @ NonNull private final MaterialShapeDrawable shapeDrawable ;
186192 @ NonNull private final TextDrawableHelper textDrawableHelper ;
@@ -257,7 +263,7 @@ private void restoreState() {
257263 onBadgeShapeAppearanceUpdated ();
258264 onBadgeTextAppearanceUpdated ();
259265
260- onMaxCharacterCountUpdated ();
266+ onMaxBadgeLengthUpdated ();
261267
262268 onBadgeContentUpdated ();
263269 onAlphaUpdated ();
@@ -527,14 +533,20 @@ public void setNumber(int number) {
527533 number = Math .max (0 , number );
528534 if (this .state .getNumber () != number ) {
529535 state .setNumber (number );
530- onBadgeContentUpdated ();
536+ onNumberUpdated ();
531537 }
532538 }
533539
534- /** Resets any badge number so that a numberless badge will be displayed . */
540+ /** Clears the badge's number. */
535541 public void clearNumber () {
536- if (hasNumber ()) {
537- state .clearNumber ();
542+ state .clearNumber ();
543+ onNumberUpdated ();
544+ }
545+
546+ private void onNumberUpdated () {
547+ // The text has priority over the number so when the number changes, the badge is updated
548+ // only if there is no text.
549+ if (!hasText ()) {
538550 onBadgeContentUpdated ();
539551 }
540552 }
@@ -556,17 +568,17 @@ public String getText() {
556568 }
557569
558570 /**
559- * Sets the badge's text.
571+ * Sets the badge's text. The specified text will be displayed, unless its length exceeds {@code
572+ * maxCharacterCount} in which case a truncated version will be shown.
560573 *
561574 * @see #getText()
562575 * @attr ref com.google.android.material.R.styleable#Badge_badgeText
563576 */
564577 public void setText (@ Nullable String text ) {
565- if (TextUtils .equals (state .getText (), text )) {
566- return ;
578+ if (!TextUtils .equals (state .getText (), text )) {
579+ state .setText (text );
580+ onTextUpdated ();
567581 }
568- state .setText (text );
569- onBadgeContentUpdated ();
570582 }
571583
572584 /**
@@ -575,15 +587,13 @@ public void setText(@Nullable String text) {
575587 public void clearText () {
576588 if (state .hasText ()) {
577589 state .clearText ();
578- onBadgeContentUpdated ();
590+ onTextUpdated ();
579591 }
580592 }
581593
582- private void onBadgeContentUpdated () {
583- textDrawableHelper .setTextSizeDirty (true );
584- onBadgeShapeAppearanceUpdated ();
585- updateCenterAndBounds ();
586- invalidateSelf ();
594+ private void onTextUpdated () {
595+ // The text has priority over the number so any text change updates the badge content.
596+ onBadgeContentUpdated ();
587597 }
588598
589599 /**
@@ -605,11 +615,34 @@ public int getMaxCharacterCount() {
605615 public void setMaxCharacterCount (int maxCharacterCount ) {
606616 if (this .state .getMaxCharacterCount () != maxCharacterCount ) {
607617 this .state .setMaxCharacterCount (maxCharacterCount );
608- onMaxCharacterCountUpdated ();
618+ onMaxBadgeLengthUpdated ();
609619 }
610620 }
611621
612- private void onMaxCharacterCountUpdated () {
622+ /**
623+ * Returns this badge's max number. If maxCharacterCount is set, it will override this number.
624+ *
625+ * @see #setMaxNumber(int)
626+ * @attr ref com.google.android.material.R.styleable#Badge_maxNumber
627+ */
628+ public int getMaxNumber () {
629+ return state .getMaxNumber ();
630+ }
631+
632+ /**
633+ * Sets this badge's max number. If maxCharacterCount is set, it will override this number.
634+ *
635+ * @param maxNumber This badge's max number.
636+ * @attr ref com.google.android.material.R.styleable#Badge_maxNumber
637+ */
638+ public void setMaxNumber (int maxNumber ) {
639+ if (this .state .getMaxNumber () != maxNumber ) {
640+ this .state .setMaxNumber (maxNumber );
641+ onMaxBadgeLengthUpdated ();
642+ }
643+ }
644+
645+ private void onMaxBadgeLengthUpdated () {
613646 updateMaxBadgeNumber ();
614647 textDrawableHelper .setTextSizeDirty (true );
615648 updateCenterAndBounds ();
@@ -691,7 +724,7 @@ public void draw(@NonNull Canvas canvas) {
691724 }
692725 shapeDrawable .draw (canvas );
693726 if (hasBadgeContent ()) {
694- drawText (canvas );
727+ drawBadgeContent (canvas );
695728 }
696729 }
697730
@@ -763,7 +796,7 @@ private String getNumberContentDescription() {
763796 if (context == null ) {
764797 return null ;
765798 }
766- if (getNumber () <= maxBadgeNumber ) {
799+ if (maxBadgeNumber == BADGE_CONTENT_NOT_TRUNCATED || getNumber () <= maxBadgeNumber ) {
767800 return context
768801 .getResources ()
769802 .getQuantityString (
@@ -776,6 +809,7 @@ private String getNumberContentDescription() {
776809 return null ;
777810 }
778811
812+ @ Nullable
779813 private CharSequence getTextContentDescription () {
780814 final CharSequence contentDescription = state .getContentDescriptionForText ();
781815 if (contentDescription != null ) {
@@ -1083,10 +1117,10 @@ private void onBadgeShapeAppearanceUpdated() {
10831117 shapeDrawable .setShapeAppearanceModel (
10841118 ShapeAppearanceModel .builder (
10851119 context ,
1086- state . hasNumber ()
1120+ hasBadgeContent ()
10871121 ? state .getBadgeWithTextShapeAppearanceResId ()
10881122 : state .getBadgeShapeAppearanceResId (),
1089- state . hasNumber ()
1123+ hasBadgeContent ()
10901124 ? state .getBadgeWithTextShapeAppearanceOverlayResId ()
10911125 : state .getBadgeShapeAppearanceOverlayResId ())
10921126 .build ());
@@ -1154,20 +1188,28 @@ private int getTotalHorizontalOffsetForState() {
11541188 }
11551189
11561190 private void calculateCenterAndBounds (@ NonNull Rect anchorRect , @ NonNull View anchorView ) {
1157- cornerRadius = ! hasBadgeContent () ? state .badgeRadius : state .badgeWithTextRadius ;
1191+ cornerRadius = hasBadgeContent () ? state .badgeWithTextRadius : state .badgeRadius ;
11581192 if (cornerRadius != BADGE_RADIUS_NOT_SPECIFIED ) {
1159- halfBadgeHeight = cornerRadius ;
11601193 halfBadgeWidth = cornerRadius ;
1194+ halfBadgeHeight = cornerRadius ;
11611195 } else {
1162- halfBadgeHeight =
1163- Math .round (!hasBadgeContent () ? state .badgeHeight / 2 : state .badgeWithTextHeight / 2 );
11641196 halfBadgeWidth =
1165- Math .round (!hasBadgeContent () ? state .badgeWidth / 2 : state .badgeWithTextWidth / 2 );
1197+ Math .round (hasBadgeContent () ? state .badgeWithTextWidth / 2 : state .badgeWidth / 2 );
1198+ halfBadgeHeight =
1199+ Math .round (hasBadgeContent () ? state .badgeWithTextHeight / 2 : state .badgeHeight / 2 );
11661200 }
1167- String badgeContent = getBadgeContent ();
1201+
11681202 // If the badge has a number, we want to make sure that the badge is at least tall/wide
11691203 // enough to encompass the text with padding.
11701204 if (hasBadgeContent ()) {
1205+ String badgeContent = getBadgeContent ();
1206+
1207+ halfBadgeWidth =
1208+ Math .max (
1209+ halfBadgeWidth ,
1210+ textDrawableHelper .getTextWidth (badgeContent ) / 2f
1211+ + state .getBadgeHorizontalPadding ());
1212+
11711213 halfBadgeHeight =
11721214 Math .max (
11731215 halfBadgeHeight ,
@@ -1178,17 +1220,6 @@ private void calculateCenterAndBounds(@NonNull Rect anchorRect, @NonNull View an
11781220 halfBadgeWidth = Math .max (halfBadgeWidth , halfBadgeHeight );
11791221 }
11801222
1181- // If the badge has a number and it exceeds the max circular badge count, or the badge
1182- // has text then the width of the badge should encapsulate the whole text.
1183- if ((getNumber () > MAX_CIRCULAR_BADGE_NUMBER_COUNT )
1184- || (hasText () && !getText ().isEmpty ())) {
1185- halfBadgeWidth =
1186- Math .max (
1187- halfBadgeWidth ,
1188- textDrawableHelper .getTextWidth (badgeContent ) / 2f
1189- + state .getBadgeHorizontalPadding ());
1190- }
1191-
11921223 int totalVerticalOffset = getTotalVerticalOffsetForState ();
11931224
11941225 switch (state .getBadgeGravity ()) {
@@ -1328,26 +1359,28 @@ private float getRightCutoff(View anchorParent, float anchorViewOffset) {
13281359 return rightCutOff ;
13291360 }
13301361
1331- private void drawText (Canvas canvas ) {
1332- Rect textBounds = new Rect ();
1362+ private void drawBadgeContent (Canvas canvas ) {
13331363 String badgeContent = getBadgeContent ();
1334- textDrawableHelper
1335- .getTextPaint ()
1336- .getTextBounds (badgeContent , 0 , badgeContent .length (), textBounds );
1337-
1338- // The text is centered horizontally using Paint.Align.Center. We calculate the correct
1339- // y-coordinate ourselves using textbounds.exactCenterY, but this can look askew at low
1340- // screen densities due to canvas.drawText rounding the coordinates to the nearest integer.
1341- // To mitigate this, we round the y-coordinate following these rules:
1342- // If the badge.bottom is <= 0, the text is drawn above its original origin (0,0) so
1343- // we round down the y-coordinate since we want to keep it above its new origin.
1344- // If the badge.bottom is positive, we round up for the opposite reason.
1345- float exactCenterY = badgeCenterY - textBounds .exactCenterY ();
1346- canvas .drawText (
1347- badgeContent ,
1348- badgeCenterX ,
1349- textBounds .bottom <= 0 ? (int ) exactCenterY : Math .round (exactCenterY ),
1350- textDrawableHelper .getTextPaint ());
1364+ if (badgeContent != null ) {
1365+ Rect textBounds = new Rect ();
1366+ textDrawableHelper
1367+ .getTextPaint ()
1368+ .getTextBounds (badgeContent , 0 , badgeContent .length (), textBounds );
1369+
1370+ // The text is centered horizontally using Paint.Align.Center. We calculate the correct
1371+ // y-coordinate ourselves using textbounds.exactCenterY, but this can look askew at low
1372+ // screen densities due to canvas.drawText rounding the coordinates to the nearest integer.
1373+ // To mitigate this, we round the y-coordinate following these rules:
1374+ // If the badge.bottom is <= 0, the text is drawn above its original origin (0,0) so
1375+ // we round down the y-coordinate since we want to keep it above its new origin.
1376+ // If the badge.bottom is positive, we round up for the opposite reason.
1377+ float exactCenterY = badgeCenterY - textBounds .exactCenterY ();
1378+ canvas .drawText (
1379+ badgeContent ,
1380+ badgeCenterX ,
1381+ textBounds .bottom <= 0 ? (int ) exactCenterY : Math .round (exactCenterY ),
1382+ textDrawableHelper .getTextPaint ());
1383+ }
13511384 }
13521385
13531386 private boolean hasBadgeContent () {
@@ -1367,13 +1400,32 @@ private String getBadgeContent() {
13671400
13681401 @ Nullable
13691402 private String getTextBadgeText () {
1370- return getText ();
1403+ String text = getText ();
1404+ final int maxCharacterCount = getMaxCharacterCount ();
1405+ if (maxCharacterCount == BADGE_CONTENT_NOT_TRUNCATED ) {
1406+ return text ;
1407+ }
1408+
1409+ if (text != null && text .length () > maxCharacterCount ) {
1410+ Context context = contextRef .get ();
1411+ if (context == null ) {
1412+ return "" ;
1413+ }
1414+
1415+ text = text .substring (0 , maxCharacterCount - 1 );
1416+ return String .format (
1417+ context .getString (R .string .m3_exceed_max_badge_text_suffix ),
1418+ text ,
1419+ DEFAULT_EXCEED_MAX_BADGE_TEXT_SUFFIX );
1420+ } else {
1421+ return text ;
1422+ }
13711423 }
13721424
13731425 @ NonNull
13741426 private String getNumberBadgeText () {
13751427 // If number exceeds max count, show badgeMaxCount+ instead of the number.
1376- if (getNumber () <= maxBadgeNumber ) {
1428+ if (maxBadgeNumber == BADGE_CONTENT_NOT_TRUNCATED || getNumber () <= maxBadgeNumber ) {
13771429 return NumberFormat .getInstance (state .getNumberLocale ()).format (getNumber ());
13781430 } else {
13791431 Context context = contextRef .get ();
@@ -1389,7 +1441,21 @@ private String getNumberBadgeText() {
13891441 }
13901442 }
13911443
1444+ private void onBadgeContentUpdated () {
1445+ textDrawableHelper .setTextSizeDirty (true );
1446+ onBadgeShapeAppearanceUpdated ();
1447+ updateCenterAndBounds ();
1448+ invalidateSelf ();
1449+ }
1450+
13921451 private void updateMaxBadgeNumber () {
1393- maxBadgeNumber = (int ) Math .pow (10.0d , (double ) getMaxCharacterCount () - 1 ) - 1 ;
1452+ if (getMaxCharacterCount () != BADGE_CONTENT_NOT_TRUNCATED ) {
1453+ // If there exists a max character count, we set the maximum number a badge can have as the
1454+ // largest number that has maxCharCount - 1 digits, which accounts for the `+` as a character.
1455+ maxBadgeNumber = (int ) Math .pow (10.0d , (double ) getMaxCharacterCount () - 1 ) - 1 ;
1456+ } else {
1457+ maxBadgeNumber = getMaxNumber ();
1458+ }
13941459 }
13951460}
1461+
0 commit comments