Skip to content

Commit 2ddcfe4

Browse files
imhappipaulfthomas
authored andcommitted
[Badge] Add shape appearance for badges
PiperOrigin-RevId: 512137782
1 parent 34d6a14 commit 2ddcfe4

File tree

7 files changed

+253
-34
lines changed

7 files changed

+253
-34
lines changed

docs/components/BadgeDrawable.md

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,21 +84,30 @@ top and end edges of the anchor (with some offsets). The other options are
8484
### `BadgeDrawable` center offsets
8585

8686
By default, `BadgeDrawable` is aligned with the top and end edges of its anchor
87-
view (with some offsets). Call `setBadgeGravity(int)` to change it to one of the
87+
view (with some offsets if `offsetAlignmentMode` is `legacy`). Call `setBadgeGravity(int)` to change it to one of the
8888
other supported modes. To adjust the badge's offsets relative to the anchor's
89-
center, use `setHoriziontalOffset(int)` or `setVerticalOffset(int)`
89+
center, use `setHorizontalOffset(int)` or `setVerticalOffset(int)`
9090

9191
### `BadgeDrawable` Attributes
9292

93-
Feature | Relevant attributes
94-
--------------------- | -----------------------------------------------
95-
Color | `app:backgroundColor` <br> `app:badgeTextColor`
96-
Label | `app:number`
97-
Label Length | `app:maxCharacterCount`
98-
Label Text Color | `app:badgeTextColor`
99-
Label Text Appearance | `app:badgeTextAppearance`
100-
Badge Gravity | `app:badgeGravity`
101-
Offset Alignment | `app:offsetAlignmentMode`
93+
| Feature | Relevant attributes |
94+
| --------------------- | ------------------------------------------ |
95+
| Color | `app:backgroundColor` <br> |
96+
: : `app\:badgeTextColor` :
97+
| Width | `app:badgeWidth` <br> |
98+
: : `app\:badgeWithTextWidth` :
99+
| Height | `app:badgeHeight` <br> |
100+
: : `app\:badgeWithTextHeight` :
101+
| Shape | `app:badgeShapeAppearance` <br> |
102+
: : `app\:badgeShapeAppearanceOverlay` <br> :
103+
: : `app\:badgeWithTextShapeAppearance` <br> :
104+
: : `app\:badgeWithTextShapeAppearanceOverlay` :
105+
| Label | `app:number` |
106+
| Label Length | `app:maxCharacterCount` |
107+
| Label Text Color | `app:badgeTextColor` |
108+
| Label Text Appearance | `app:badgeTextAppearance` |
109+
| Badge Gravity | `app:badgeGravity` |
110+
| Offset Alignment | `app:offsetAlignmentMode` |
102111

103112
### Talkback Support
104113

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

Lines changed: 96 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import com.google.android.material.internal.ThemeEnforcement;
5252
import com.google.android.material.resources.TextAppearance;
5353
import com.google.android.material.shape.MaterialShapeDrawable;
54+
import com.google.android.material.shape.ShapeAppearanceModel;
5455
import java.lang.annotation.Retention;
5556
import java.lang.annotation.RetentionPolicy;
5657
import java.lang.ref.WeakReference;
@@ -176,6 +177,9 @@ public class BadgeDrawable extends Drawable implements TextDrawableDelegate {
176177
@Retention(RetentionPolicy.SOURCE)
177178
@interface OffsetAlignmentMode {}
178179

180+
/** A value to indicate that a badge radius has not been specified. */
181+
static final int BADGE_RADIUS_NOT_SPECIFIED = -1;
182+
179183
@NonNull private final WeakReference<Context> contextRef;
180184
@NonNull private final MaterialShapeDrawable shapeDrawable;
181185
@NonNull private final TextDrawableHelper textDrawableHelper;
@@ -249,6 +253,7 @@ private void onVisibilityUpdated() {
249253
}
250254

251255
private void restoreState() {
256+
onBadgeShapeAppearanceUpdated();
252257
onBadgeTextAppearanceUpdated();
253258

254259
onMaxCharacterCountUpdated();
@@ -272,14 +277,23 @@ private BadgeDrawable(
272277
this.contextRef = new WeakReference<>(context);
273278
ThemeEnforcement.checkMaterialTheme(context);
274279
badgeBounds = new Rect();
275-
shapeDrawable = new MaterialShapeDrawable();
276280

277281
textDrawableHelper = new TextDrawableHelper(/* delegate= */ this);
278282
textDrawableHelper.getTextPaint().setTextAlign(Paint.Align.CENTER);
279283

280284

281285
this.state = new BadgeState(context, badgeResId, defStyleAttr, defStyleRes, savedState);
282-
286+
shapeDrawable =
287+
new MaterialShapeDrawable(
288+
ShapeAppearanceModel.builder(
289+
context,
290+
state.hasNumber()
291+
? state.getBadgeWithTextShapeAppearanceResId()
292+
: state.getBadgeShapeAppearanceResId(),
293+
state.hasNumber()
294+
? state.getBadgeWithTextShapeAppearanceOverlayResId()
295+
: state.getBadgeShapeAppearanceOverlayResId())
296+
.build());
283297
restoreState();
284298
}
285299

@@ -520,6 +534,7 @@ public void clearNumber() {
520534

521535
private void onNumberUpdated() {
522536
textDrawableHelper.setTextWidthDirty(true);
537+
onBadgeShapeAppearanceUpdated();
523538
updateCenterAndBounds();
524539
invalidateSelf();
525540
}
@@ -873,6 +888,68 @@ private void onBadgeTextAppearanceUpdated() {
873888
invalidateSelf();
874889
}
875890

891+
/**
892+
* Sets this badge without text's shape appearance resource.
893+
*
894+
* @param id This badge's shape appearance res id when there is no text.
895+
* @attr ref com.google.android.material.R.styleable#Badge_badgeShapeAppearance
896+
*/
897+
public void setBadgeWithoutTextShapeAppearance(@StyleRes int id) {
898+
state.setBadgeShapeAppearanceResId(id);
899+
onBadgeShapeAppearanceUpdated();
900+
}
901+
902+
/**
903+
* Sets this badge without text's shape appearance overlay resource.
904+
*
905+
* @param id This badge's shape appearance overlay res id when there is no text.
906+
* @attr ref com.google.android.material.R.styleable#Badge_badgeShapeAppearanceOverlay
907+
*/
908+
public void setBadgeWithoutTextShapeAppearanceOverlay(@StyleRes int id) {
909+
state.setBadgeShapeAppearanceOverlayResId(id);
910+
onBadgeShapeAppearanceUpdated();
911+
}
912+
913+
/**
914+
* Sets this badge with text's shape appearance resource.
915+
*
916+
* @param id This badge's shape appearance res id when there is text.
917+
* @attr ref com.google.android.material.R.styleable#Badge_badgeWithTextShapeAppearance
918+
*/
919+
public void setBadgeWithTextShapeAppearance(@StyleRes int id) {
920+
state.setBadgeWithTextShapeAppearanceResId(id);
921+
onBadgeShapeAppearanceUpdated();
922+
}
923+
924+
/**
925+
* Sets this badge with text's shape appearance overlay resource.
926+
*
927+
* @param id This badge's shape appearance overlay res id when there is text.
928+
* @attr ref com.google.android.material.R.styleable#Badge_badgeWithTextShapeAppearanceOverlay
929+
*/
930+
public void setBadgeWithTextShapeAppearanceOverlay(@StyleRes int id) {
931+
state.setBadgeWithTextShapeAppearanceOverlayResId(id);
932+
onBadgeShapeAppearanceUpdated();
933+
}
934+
935+
private void onBadgeShapeAppearanceUpdated() {
936+
Context context = contextRef.get();
937+
if (context == null) {
938+
return;
939+
}
940+
shapeDrawable.setShapeAppearanceModel(
941+
ShapeAppearanceModel.builder(
942+
context,
943+
state.hasNumber()
944+
? state.getBadgeWithTextShapeAppearanceResId()
945+
: state.getBadgeShapeAppearanceResId(),
946+
state.hasNumber()
947+
? state.getBadgeWithTextShapeAppearanceOverlayResId()
948+
: state.getBadgeShapeAppearanceOverlayResId())
949+
.build());
950+
invalidateSelf();
951+
}
952+
876953
private void updateCenterAndBounds() {
877954
Context context = contextRef.get();
878955
View anchorView = anchorViewRef != null ? anchorViewRef.get() : null;
@@ -898,7 +975,11 @@ private void updateCenterAndBounds() {
898975

899976
updateBadgeBounds(badgeBounds, badgeCenterX, badgeCenterY, halfBadgeWidth, halfBadgeHeight);
900977

901-
shapeDrawable.setCornerSize(cornerRadius);
978+
// If there is a badge radius specified, override the corner size set by the shape appearance
979+
// with the badge radius.
980+
if (cornerRadius != BADGE_RADIUS_NOT_SPECIFIED) {
981+
shapeDrawable.setCornerSize(cornerRadius);
982+
}
902983
if (!tmpRect.equals(badgeBounds)) {
903984
shapeDrawable.setBounds(badgeBounds);
904985
}
@@ -926,15 +1007,22 @@ private int getTotalHorizontalOffsetForState() {
9261007
}
9271008

9281009
private void calculateCenterAndBounds(@NonNull Rect anchorRect, @NonNull View anchorView) {
929-
if (getNumber() <= MAX_CIRCULAR_BADGE_NUMBER_COUNT) {
930-
cornerRadius = !hasNumber() ? state.badgeRadius : state.badgeWithTextRadius;
1010+
cornerRadius = !hasNumber() ? state.badgeRadius : state.badgeWithTextRadius;
1011+
if (cornerRadius != BADGE_RADIUS_NOT_SPECIFIED) {
9311012
halfBadgeHeight = cornerRadius;
9321013
halfBadgeWidth = cornerRadius;
9331014
} else {
934-
cornerRadius = state.badgeWithTextRadius;
935-
halfBadgeHeight = cornerRadius;
1015+
halfBadgeHeight =
1016+
Math.round(!hasNumber() ? state.badgeHeight / 2 : state.badgeWithTextHeight / 2);
1017+
halfBadgeWidth =
1018+
Math.round(!hasNumber() ? state.badgeWidth / 2 : state.badgeWithTextWidth / 2);
1019+
}
1020+
if (getNumber() > MAX_CIRCULAR_BADGE_NUMBER_COUNT) {
9361021
String badgeText = getBadgeText();
937-
halfBadgeWidth = textDrawableHelper.getTextWidth(badgeText) / 2f + state.badgeWidePadding;
1022+
halfBadgeWidth =
1023+
Math.max(
1024+
halfBadgeWidth,
1025+
textDrawableHelper.getTextWidth(badgeText) / 2f + state.badgeWidePadding);
9381026
}
9391027

9401028
int totalVerticalOffset = getTotalVerticalOffsetForState();

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

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.google.android.material.R;
2020

2121
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
22+
import static com.google.android.material.badge.BadgeDrawable.BADGE_RADIUS_NOT_SPECIFIED;
2223
import static com.google.android.material.badge.BadgeDrawable.OFFSET_ALIGNMENT_MODE_LEGACY;
2324
import static com.google.android.material.badge.BadgeDrawable.TOP_END;
2425

@@ -71,6 +72,10 @@ public final class BadgeState {
7172

7273
final float badgeRadius;
7374
final float badgeWithTextRadius;
75+
final float badgeWidth;
76+
final float badgeHeight;
77+
final float badgeWithTextWidth;
78+
final float badgeWithTextHeight;
7479
final float badgeWidePadding;
7580
final int horizontalInset;
7681
final int horizontalInsetWithText;
@@ -95,8 +100,7 @@ public final class BadgeState {
95100

96101
Resources res = context.getResources();
97102
badgeRadius =
98-
a.getDimensionPixelSize(
99-
R.styleable.Badge_badgeRadius, res.getDimensionPixelSize(R.dimen.mtrl_badge_radius));
103+
a.getDimensionPixelSize(R.styleable.Badge_badgeRadius, BADGE_RADIUS_NOT_SPECIFIED);
100104
badgeWidePadding =
101105
a.getDimensionPixelSize(
102106
R.styleable.Badge_badgeWidePadding,
@@ -112,9 +116,20 @@ public final class BadgeState {
112116
.getDimensionPixelSize(R.dimen.mtrl_badge_text_horizontal_edge_offset);
113117

114118
badgeWithTextRadius =
115-
a.getDimensionPixelSize(
116-
R.styleable.Badge_badgeWithTextRadius,
117-
res.getDimensionPixelSize(R.dimen.mtrl_badge_with_text_radius));
119+
a.getDimensionPixelSize(R.styleable.Badge_badgeWithTextRadius, BADGE_RADIUS_NOT_SPECIFIED);
120+
badgeWidth =
121+
a.getDimension(R.styleable.Badge_badgeWidth, res.getDimension(R.dimen.m3_badge_size));
122+
badgeWithTextWidth =
123+
a.getDimension(
124+
R.styleable.Badge_badgeWithTextWidth,
125+
res.getDimension(R.dimen.m3_badge_with_text_size));
126+
badgeHeight =
127+
a.getDimension(R.styleable.Badge_badgeHeight, res.getDimension(R.dimen.m3_badge_size));
128+
badgeWithTextHeight =
129+
a.getDimension(
130+
R.styleable.Badge_badgeWithTextHeight,
131+
res.getDimension(R.dimen.m3_badge_with_text_size));
132+
118133
offsetAlignmentMode =
119134
a.getInt(R.styleable.Badge_offsetAlignmentMode, OFFSET_ALIGNMENT_MODE_LEGACY);
120135

@@ -153,6 +168,30 @@ public final class BadgeState {
153168
currentState.number = State.BADGE_NUMBER_NONE;
154169
}
155170

171+
currentState.badgeShapeAppearanceResId =
172+
storedState.badgeShapeAppearanceResId == null
173+
? a.getResourceId(
174+
R.styleable.Badge_badgeShapeAppearance,
175+
R.style.ShapeAppearance_M3_Sys_Shape_Corner_Full)
176+
: storedState.badgeShapeAppearanceResId;
177+
178+
currentState.badgeShapeAppearanceOverlayResId =
179+
storedState.badgeShapeAppearanceOverlayResId == null
180+
? a.getResourceId(R.styleable.Badge_badgeShapeAppearanceOverlay, 0)
181+
: storedState.badgeShapeAppearanceOverlayResId;
182+
183+
currentState.badgeWithTextShapeAppearanceResId =
184+
storedState.badgeWithTextShapeAppearanceResId == null
185+
? a.getResourceId(
186+
R.styleable.Badge_badgeWithTextShapeAppearance,
187+
R.style.ShapeAppearance_M3_Sys_Shape_Corner_Full)
188+
: storedState.badgeWithTextShapeAppearanceResId;
189+
190+
currentState.badgeWithTextShapeAppearanceOverlayResId =
191+
storedState.badgeWithTextShapeAppearanceOverlayResId == null
192+
? a.getResourceId(R.styleable.Badge_badgeWithTextShapeAppearanceOverlay, 0)
193+
: storedState.badgeWithTextShapeAppearanceOverlayResId;
194+
156195
currentState.backgroundColor =
157196
storedState.backgroundColor == null
158197
? readColorFromAttributes(context, a, R.styleable.Badge_backgroundColor)
@@ -326,6 +365,42 @@ void setTextAppearanceResId(@StyleRes int textAppearanceResId) {
326365
currentState.badgeTextAppearanceResId = textAppearanceResId;
327366
}
328367

368+
int getBadgeShapeAppearanceResId() {
369+
return currentState.badgeShapeAppearanceResId;
370+
}
371+
372+
void setBadgeShapeAppearanceResId(int shapeAppearanceResId) {
373+
overridingState.badgeShapeAppearanceResId = shapeAppearanceResId;
374+
currentState.badgeShapeAppearanceResId = shapeAppearanceResId;
375+
}
376+
377+
int getBadgeShapeAppearanceOverlayResId() {
378+
return currentState.badgeShapeAppearanceOverlayResId;
379+
}
380+
381+
void setBadgeShapeAppearanceOverlayResId(int shapeAppearanceOverlayResId) {
382+
overridingState.badgeShapeAppearanceOverlayResId = shapeAppearanceOverlayResId;
383+
currentState.badgeShapeAppearanceOverlayResId = shapeAppearanceOverlayResId;
384+
}
385+
386+
int getBadgeWithTextShapeAppearanceResId() {
387+
return currentState.badgeWithTextShapeAppearanceResId;
388+
}
389+
390+
void setBadgeWithTextShapeAppearanceResId(int shapeAppearanceResId) {
391+
overridingState.badgeWithTextShapeAppearanceResId = shapeAppearanceResId;
392+
currentState.badgeWithTextShapeAppearanceResId = shapeAppearanceResId;
393+
}
394+
395+
int getBadgeWithTextShapeAppearanceOverlayResId() {
396+
return currentState.badgeWithTextShapeAppearanceOverlayResId;
397+
}
398+
399+
void setBadgeWithTextShapeAppearanceOverlayResId(int shapeAppearanceOverlayResId) {
400+
overridingState.badgeWithTextShapeAppearanceOverlayResId = shapeAppearanceOverlayResId;
401+
currentState.badgeWithTextShapeAppearanceOverlayResId = shapeAppearanceOverlayResId;
402+
}
403+
329404
@BadgeGravity
330405
int getBadgeGravity() {
331406
return currentState.badgeGravity;
@@ -456,6 +531,11 @@ public static final class State implements Parcelable {
456531
@ColorInt private Integer badgeTextColor;
457532
@StyleRes private Integer badgeTextAppearanceResId;
458533

534+
@StyleRes private Integer badgeShapeAppearanceResId;
535+
@StyleRes private Integer badgeShapeAppearanceOverlayResId;
536+
@StyleRes private Integer badgeWithTextShapeAppearanceResId;
537+
@StyleRes private Integer badgeWithTextShapeAppearanceOverlayResId;
538+
459539
private int alpha = 255;
460540
private int number = NOT_SET;
461541
private int maxCharacterCount = NOT_SET;
@@ -493,6 +573,10 @@ public State() {}
493573
backgroundColor = (Integer) in.readSerializable();
494574
badgeTextColor = (Integer) in.readSerializable();
495575
badgeTextAppearanceResId = (Integer) in.readSerializable();
576+
badgeShapeAppearanceResId = (Integer) in.readSerializable();
577+
badgeShapeAppearanceOverlayResId = (Integer) in.readSerializable();
578+
badgeWithTextShapeAppearanceResId = (Integer) in.readSerializable();
579+
badgeWithTextShapeAppearanceOverlayResId = (Integer) in.readSerializable();
496580
alpha = in.readInt();
497581
number = in.readInt();
498582
maxCharacterCount = in.readInt();
@@ -535,6 +619,10 @@ public void writeToParcel(@NonNull Parcel dest, int flags) {
535619
dest.writeSerializable(backgroundColor);
536620
dest.writeSerializable(badgeTextColor);
537621
dest.writeSerializable(badgeTextAppearanceResId);
622+
dest.writeSerializable(badgeShapeAppearanceResId);
623+
dest.writeSerializable(badgeShapeAppearanceOverlayResId);
624+
dest.writeSerializable(badgeWithTextShapeAppearanceResId);
625+
dest.writeSerializable(badgeWithTextShapeAppearanceOverlayResId);
538626
dest.writeInt(alpha);
539627
dest.writeInt(number);
540628
dest.writeInt(maxCharacterCount);

lib/java/com/google/android/material/badge/res-public/values/public.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@
2525
<public name="horizontalOffset" type="attr"/>
2626
<public name="verticalOffset" type="attr"/>
2727
<public name="offsetAlignmentMode" type="attr"/>
28+
<public name="badgeWidth" type="attr"/>
29+
<public name="badgeWithTextWidth" type="attr"/>
30+
<public name="badgeHeight" type="attr"/>
31+
<public name="badgeWithTextHeight" type="attr"/>
32+
<public name="badgeShapeAppearance" type="attr"/>
33+
<public name="badgeWithTextShapeAppearance" type="attr"/>
34+
<public name="badgeShapeAppearanceOverlay" type="attr"/>
35+
<public name="badgeWithTextShapeAppearanceOverlay" type="attr"/>
2836
<public name="Widget.MaterialComponents.Badge" type="style"/>
2937
<public name="Widget.Material3.Badge" type="style"/>
3038
</resources>

0 commit comments

Comments
 (0)