Skip to content

Commit

Permalink
[Badge] Add shape appearance for badges
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 512137782
  • Loading branch information
imhappi authored and paulfthomas committed Feb 27, 2023
1 parent 34d6a14 commit 2ddcfe4
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 34 deletions.
31 changes: 20 additions & 11 deletions docs/components/BadgeDrawable.md
Expand Up @@ -84,21 +84,30 @@ top and end edges of the anchor (with some offsets). The other options are
### `BadgeDrawable` center offsets

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

### `BadgeDrawable` Attributes

Feature | Relevant attributes
--------------------- | -----------------------------------------------
Color | `app:backgroundColor` <br> `app:badgeTextColor`
Label | `app:number`
Label Length | `app:maxCharacterCount`
Label Text Color | `app:badgeTextColor`
Label Text Appearance | `app:badgeTextAppearance`
Badge Gravity | `app:badgeGravity`
Offset Alignment | `app:offsetAlignmentMode`
| Feature | Relevant attributes |
| --------------------- | ------------------------------------------ |
| Color | `app:backgroundColor` <br> |
: : `app\:badgeTextColor` :
| Width | `app:badgeWidth` <br> |
: : `app\:badgeWithTextWidth` :
| Height | `app:badgeHeight` <br> |
: : `app\:badgeWithTextHeight` :
| Shape | `app:badgeShapeAppearance` <br> |
: : `app\:badgeShapeAppearanceOverlay` <br> :
: : `app\:badgeWithTextShapeAppearance` <br> :
: : `app\:badgeWithTextShapeAppearanceOverlay` :
| Label | `app:number` |
| Label Length | `app:maxCharacterCount` |
| Label Text Color | `app:badgeTextColor` |
| Label Text Appearance | `app:badgeTextAppearance` |
| Badge Gravity | `app:badgeGravity` |
| Offset Alignment | `app:offsetAlignmentMode` |

### Talkback Support

Expand Down
104 changes: 96 additions & 8 deletions lib/java/com/google/android/material/badge/BadgeDrawable.java
Expand Up @@ -51,6 +51,7 @@
import com.google.android.material.internal.ThemeEnforcement;
import com.google.android.material.resources.TextAppearance;
import com.google.android.material.shape.MaterialShapeDrawable;
import com.google.android.material.shape.ShapeAppearanceModel;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
Expand Down Expand Up @@ -176,6 +177,9 @@ public class BadgeDrawable extends Drawable implements TextDrawableDelegate {
@Retention(RetentionPolicy.SOURCE)
@interface OffsetAlignmentMode {}

/** A value to indicate that a badge radius has not been specified. */
static final int BADGE_RADIUS_NOT_SPECIFIED = -1;

@NonNull private final WeakReference<Context> contextRef;
@NonNull private final MaterialShapeDrawable shapeDrawable;
@NonNull private final TextDrawableHelper textDrawableHelper;
Expand Down Expand Up @@ -249,6 +253,7 @@ private void onVisibilityUpdated() {
}

private void restoreState() {
onBadgeShapeAppearanceUpdated();
onBadgeTextAppearanceUpdated();

onMaxCharacterCountUpdated();
Expand All @@ -272,14 +277,23 @@ private BadgeDrawable(
this.contextRef = new WeakReference<>(context);
ThemeEnforcement.checkMaterialTheme(context);
badgeBounds = new Rect();
shapeDrawable = new MaterialShapeDrawable();

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


this.state = new BadgeState(context, badgeResId, defStyleAttr, defStyleRes, savedState);

shapeDrawable =
new MaterialShapeDrawable(
ShapeAppearanceModel.builder(
context,
state.hasNumber()
? state.getBadgeWithTextShapeAppearanceResId()
: state.getBadgeShapeAppearanceResId(),
state.hasNumber()
? state.getBadgeWithTextShapeAppearanceOverlayResId()
: state.getBadgeShapeAppearanceOverlayResId())
.build());
restoreState();
}

Expand Down Expand Up @@ -520,6 +534,7 @@ public void clearNumber() {

private void onNumberUpdated() {
textDrawableHelper.setTextWidthDirty(true);
onBadgeShapeAppearanceUpdated();
updateCenterAndBounds();
invalidateSelf();
}
Expand Down Expand Up @@ -873,6 +888,68 @@ private void onBadgeTextAppearanceUpdated() {
invalidateSelf();
}

/**
* Sets this badge without text's shape appearance resource.
*
* @param id This badge's shape appearance res id when there is no text.
* @attr ref com.google.android.material.R.styleable#Badge_badgeShapeAppearance
*/
public void setBadgeWithoutTextShapeAppearance(@StyleRes int id) {
state.setBadgeShapeAppearanceResId(id);
onBadgeShapeAppearanceUpdated();
}

/**
* Sets this badge without text's shape appearance overlay resource.
*
* @param id This badge's shape appearance overlay res id when there is no text.
* @attr ref com.google.android.material.R.styleable#Badge_badgeShapeAppearanceOverlay
*/
public void setBadgeWithoutTextShapeAppearanceOverlay(@StyleRes int id) {
state.setBadgeShapeAppearanceOverlayResId(id);
onBadgeShapeAppearanceUpdated();
}

/**
* Sets this badge with text's shape appearance resource.
*
* @param id This badge's shape appearance res id when there is text.
* @attr ref com.google.android.material.R.styleable#Badge_badgeWithTextShapeAppearance
*/
public void setBadgeWithTextShapeAppearance(@StyleRes int id) {
state.setBadgeWithTextShapeAppearanceResId(id);
onBadgeShapeAppearanceUpdated();
}

/**
* Sets this badge with text's shape appearance overlay resource.
*
* @param id This badge's shape appearance overlay res id when there is text.
* @attr ref com.google.android.material.R.styleable#Badge_badgeWithTextShapeAppearanceOverlay
*/
public void setBadgeWithTextShapeAppearanceOverlay(@StyleRes int id) {
state.setBadgeWithTextShapeAppearanceOverlayResId(id);
onBadgeShapeAppearanceUpdated();
}

private void onBadgeShapeAppearanceUpdated() {
Context context = contextRef.get();
if (context == null) {
return;
}
shapeDrawable.setShapeAppearanceModel(
ShapeAppearanceModel.builder(
context,
state.hasNumber()
? state.getBadgeWithTextShapeAppearanceResId()
: state.getBadgeShapeAppearanceResId(),
state.hasNumber()
? state.getBadgeWithTextShapeAppearanceOverlayResId()
: state.getBadgeShapeAppearanceOverlayResId())
.build());
invalidateSelf();
}

private void updateCenterAndBounds() {
Context context = contextRef.get();
View anchorView = anchorViewRef != null ? anchorViewRef.get() : null;
Expand All @@ -898,7 +975,11 @@ private void updateCenterAndBounds() {

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

shapeDrawable.setCornerSize(cornerRadius);
// If there is a badge radius specified, override the corner size set by the shape appearance
// with the badge radius.
if (cornerRadius != BADGE_RADIUS_NOT_SPECIFIED) {
shapeDrawable.setCornerSize(cornerRadius);
}
if (!tmpRect.equals(badgeBounds)) {
shapeDrawable.setBounds(badgeBounds);
}
Expand Down Expand Up @@ -926,15 +1007,22 @@ private int getTotalHorizontalOffsetForState() {
}

private void calculateCenterAndBounds(@NonNull Rect anchorRect, @NonNull View anchorView) {
if (getNumber() <= MAX_CIRCULAR_BADGE_NUMBER_COUNT) {
cornerRadius = !hasNumber() ? state.badgeRadius : state.badgeWithTextRadius;
cornerRadius = !hasNumber() ? state.badgeRadius : state.badgeWithTextRadius;
if (cornerRadius != BADGE_RADIUS_NOT_SPECIFIED) {
halfBadgeHeight = cornerRadius;
halfBadgeWidth = cornerRadius;
} else {
cornerRadius = state.badgeWithTextRadius;
halfBadgeHeight = cornerRadius;
halfBadgeHeight =
Math.round(!hasNumber() ? state.badgeHeight / 2 : state.badgeWithTextHeight / 2);
halfBadgeWidth =
Math.round(!hasNumber() ? state.badgeWidth / 2 : state.badgeWithTextWidth / 2);
}
if (getNumber() > MAX_CIRCULAR_BADGE_NUMBER_COUNT) {
String badgeText = getBadgeText();
halfBadgeWidth = textDrawableHelper.getTextWidth(badgeText) / 2f + state.badgeWidePadding;
halfBadgeWidth =
Math.max(
halfBadgeWidth,
textDrawableHelper.getTextWidth(badgeText) / 2f + state.badgeWidePadding);
}

int totalVerticalOffset = getTotalVerticalOffsetForState();
Expand Down
98 changes: 93 additions & 5 deletions lib/java/com/google/android/material/badge/BadgeState.java
Expand Up @@ -19,6 +19,7 @@
import com.google.android.material.R;

import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static com.google.android.material.badge.BadgeDrawable.BADGE_RADIUS_NOT_SPECIFIED;
import static com.google.android.material.badge.BadgeDrawable.OFFSET_ALIGNMENT_MODE_LEGACY;
import static com.google.android.material.badge.BadgeDrawable.TOP_END;

Expand Down Expand Up @@ -71,6 +72,10 @@ public final class BadgeState {

final float badgeRadius;
final float badgeWithTextRadius;
final float badgeWidth;
final float badgeHeight;
final float badgeWithTextWidth;
final float badgeWithTextHeight;
final float badgeWidePadding;
final int horizontalInset;
final int horizontalInsetWithText;
Expand All @@ -95,8 +100,7 @@ public final class BadgeState {

Resources res = context.getResources();
badgeRadius =
a.getDimensionPixelSize(
R.styleable.Badge_badgeRadius, res.getDimensionPixelSize(R.dimen.mtrl_badge_radius));
a.getDimensionPixelSize(R.styleable.Badge_badgeRadius, BADGE_RADIUS_NOT_SPECIFIED);
badgeWidePadding =
a.getDimensionPixelSize(
R.styleable.Badge_badgeWidePadding,
Expand All @@ -112,9 +116,20 @@ public final class BadgeState {
.getDimensionPixelSize(R.dimen.mtrl_badge_text_horizontal_edge_offset);

badgeWithTextRadius =
a.getDimensionPixelSize(
R.styleable.Badge_badgeWithTextRadius,
res.getDimensionPixelSize(R.dimen.mtrl_badge_with_text_radius));
a.getDimensionPixelSize(R.styleable.Badge_badgeWithTextRadius, BADGE_RADIUS_NOT_SPECIFIED);
badgeWidth =
a.getDimension(R.styleable.Badge_badgeWidth, res.getDimension(R.dimen.m3_badge_size));
badgeWithTextWidth =
a.getDimension(
R.styleable.Badge_badgeWithTextWidth,
res.getDimension(R.dimen.m3_badge_with_text_size));
badgeHeight =
a.getDimension(R.styleable.Badge_badgeHeight, res.getDimension(R.dimen.m3_badge_size));
badgeWithTextHeight =
a.getDimension(
R.styleable.Badge_badgeWithTextHeight,
res.getDimension(R.dimen.m3_badge_with_text_size));

offsetAlignmentMode =
a.getInt(R.styleable.Badge_offsetAlignmentMode, OFFSET_ALIGNMENT_MODE_LEGACY);

Expand Down Expand Up @@ -153,6 +168,30 @@ public final class BadgeState {
currentState.number = State.BADGE_NUMBER_NONE;
}

currentState.badgeShapeAppearanceResId =
storedState.badgeShapeAppearanceResId == null
? a.getResourceId(
R.styleable.Badge_badgeShapeAppearance,
R.style.ShapeAppearance_M3_Sys_Shape_Corner_Full)
: storedState.badgeShapeAppearanceResId;

currentState.badgeShapeAppearanceOverlayResId =
storedState.badgeShapeAppearanceOverlayResId == null
? a.getResourceId(R.styleable.Badge_badgeShapeAppearanceOverlay, 0)
: storedState.badgeShapeAppearanceOverlayResId;

currentState.badgeWithTextShapeAppearanceResId =
storedState.badgeWithTextShapeAppearanceResId == null
? a.getResourceId(
R.styleable.Badge_badgeWithTextShapeAppearance,
R.style.ShapeAppearance_M3_Sys_Shape_Corner_Full)
: storedState.badgeWithTextShapeAppearanceResId;

currentState.badgeWithTextShapeAppearanceOverlayResId =
storedState.badgeWithTextShapeAppearanceOverlayResId == null
? a.getResourceId(R.styleable.Badge_badgeWithTextShapeAppearanceOverlay, 0)
: storedState.badgeWithTextShapeAppearanceOverlayResId;

currentState.backgroundColor =
storedState.backgroundColor == null
? readColorFromAttributes(context, a, R.styleable.Badge_backgroundColor)
Expand Down Expand Up @@ -326,6 +365,42 @@ void setTextAppearanceResId(@StyleRes int textAppearanceResId) {
currentState.badgeTextAppearanceResId = textAppearanceResId;
}

int getBadgeShapeAppearanceResId() {
return currentState.badgeShapeAppearanceResId;
}

void setBadgeShapeAppearanceResId(int shapeAppearanceResId) {
overridingState.badgeShapeAppearanceResId = shapeAppearanceResId;
currentState.badgeShapeAppearanceResId = shapeAppearanceResId;
}

int getBadgeShapeAppearanceOverlayResId() {
return currentState.badgeShapeAppearanceOverlayResId;
}

void setBadgeShapeAppearanceOverlayResId(int shapeAppearanceOverlayResId) {
overridingState.badgeShapeAppearanceOverlayResId = shapeAppearanceOverlayResId;
currentState.badgeShapeAppearanceOverlayResId = shapeAppearanceOverlayResId;
}

int getBadgeWithTextShapeAppearanceResId() {
return currentState.badgeWithTextShapeAppearanceResId;
}

void setBadgeWithTextShapeAppearanceResId(int shapeAppearanceResId) {
overridingState.badgeWithTextShapeAppearanceResId = shapeAppearanceResId;
currentState.badgeWithTextShapeAppearanceResId = shapeAppearanceResId;
}

int getBadgeWithTextShapeAppearanceOverlayResId() {
return currentState.badgeWithTextShapeAppearanceOverlayResId;
}

void setBadgeWithTextShapeAppearanceOverlayResId(int shapeAppearanceOverlayResId) {
overridingState.badgeWithTextShapeAppearanceOverlayResId = shapeAppearanceOverlayResId;
currentState.badgeWithTextShapeAppearanceOverlayResId = shapeAppearanceOverlayResId;
}

@BadgeGravity
int getBadgeGravity() {
return currentState.badgeGravity;
Expand Down Expand Up @@ -456,6 +531,11 @@ public static final class State implements Parcelable {
@ColorInt private Integer badgeTextColor;
@StyleRes private Integer badgeTextAppearanceResId;

@StyleRes private Integer badgeShapeAppearanceResId;
@StyleRes private Integer badgeShapeAppearanceOverlayResId;
@StyleRes private Integer badgeWithTextShapeAppearanceResId;
@StyleRes private Integer badgeWithTextShapeAppearanceOverlayResId;

private int alpha = 255;
private int number = NOT_SET;
private int maxCharacterCount = NOT_SET;
Expand Down Expand Up @@ -493,6 +573,10 @@ public State() {}
backgroundColor = (Integer) in.readSerializable();
badgeTextColor = (Integer) in.readSerializable();
badgeTextAppearanceResId = (Integer) in.readSerializable();
badgeShapeAppearanceResId = (Integer) in.readSerializable();
badgeShapeAppearanceOverlayResId = (Integer) in.readSerializable();
badgeWithTextShapeAppearanceResId = (Integer) in.readSerializable();
badgeWithTextShapeAppearanceOverlayResId = (Integer) in.readSerializable();
alpha = in.readInt();
number = in.readInt();
maxCharacterCount = in.readInt();
Expand Down Expand Up @@ -535,6 +619,10 @@ public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeSerializable(backgroundColor);
dest.writeSerializable(badgeTextColor);
dest.writeSerializable(badgeTextAppearanceResId);
dest.writeSerializable(badgeShapeAppearanceResId);
dest.writeSerializable(badgeShapeAppearanceOverlayResId);
dest.writeSerializable(badgeWithTextShapeAppearanceResId);
dest.writeSerializable(badgeWithTextShapeAppearanceOverlayResId);
dest.writeInt(alpha);
dest.writeInt(number);
dest.writeInt(maxCharacterCount);
Expand Down
Expand Up @@ -25,6 +25,14 @@
<public name="horizontalOffset" type="attr"/>
<public name="verticalOffset" type="attr"/>
<public name="offsetAlignmentMode" type="attr"/>
<public name="badgeWidth" type="attr"/>
<public name="badgeWithTextWidth" type="attr"/>
<public name="badgeHeight" type="attr"/>
<public name="badgeWithTextHeight" type="attr"/>
<public name="badgeShapeAppearance" type="attr"/>
<public name="badgeWithTextShapeAppearance" type="attr"/>
<public name="badgeShapeAppearanceOverlay" type="attr"/>
<public name="badgeWithTextShapeAppearanceOverlay" type="attr"/>
<public name="Widget.MaterialComponents.Badge" type="style"/>
<public name="Widget.Material3.Badge" type="style"/>
</resources>

0 comments on commit 2ddcfe4

Please sign in to comment.