Skip to content

Commit 4d50aa4

Browse files
pubiqqdrchen
authored andcommitted
[Badge] Badge cleanup/fixes:
- Allow single digit badges to be non-circular if the label text is too big horizontally. - Change maxCharacterCount to truncate both strings and numbers, and add new attribute maxNumber to truncate only numbers - Updated maxCharacterCount so that if it doesn't exist, it does not truncate instead of defaulting to a value Resolves #3321 GIT_ORIGIN_REV_ID=a8f5866eef5ffd4d949b8c6d7f1451b563536a6e Co-authored-by: imhappi PiperOrigin-RevId: 523453145
1 parent 69b5386 commit 4d50aa4

File tree

10 files changed

+175
-89
lines changed

10 files changed

+175
-89
lines changed

docs/components/BadgeDrawable.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,13 @@ center, use `setHorizontalOffset(int)` or `setVerticalOffset(int)`
9191
### `BadgeDrawable` Attributes
9292

9393
| Feature | Relevant attributes |
94-
|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|
95-
| Color | `app:backgroundColor` <br> `app:badgeTextColor` |
96-
| Width | `app:badgeWidth` <br> `app:badgeWithTextWidth` |
97-
| Height | `app:badgeHeight` <br> `app:badgeWithTextHeight` |
98-
| Shape | `app:badgeShapeAppearance` <br> `app:badgeShapeAppearanceOverlay` <br> `app:badgeWithTextShapeAppearance` <br> `app:badgeWithTextShapeAppearanceOverlay` |
99-
| Label | `app:badgeText` (for text) <br> `app:number` (for numbers) |
100-
| Label Length | `app:maxCharacterCount` |
94+
|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
95+
| Color | `app:backgroundColor` <br> `app:badgeTextColor` |
96+
| Width | `app:badgeWidth` <br> `app:badgeWithTextWidth` |
97+
| Height | `app:badgeHeight` <br> `app:badgeWithTextHeight` |
98+
| Shape | `app:badgeShapeAppearance` <br> `app:badgeShapeAppearanceOverlay` <br> `app:badgeWithTextShapeAppearance` <br> `app:badgeWithTextShapeAppearanceOverlay` |
99+
| Label | `app:badgeText` (for text) <br> `app:number` (for numbers) |
100+
| Label Length | `app:maxCharacterCount` (for all text) <br> `app:maxNumber` (for numbers only) |
101101
| Label Text Color | `app:badgeTextColor` |
102102
| Label Text Appearance | `app:badgeTextAppearance` |
103103
| Badge Gravity | `app:badgeGravity` |

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

Lines changed: 135 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)