From 89a2a64805ee11005cd62a539e531af9d9b8cc62 Mon Sep 17 00:00:00 2001 From: rightnao Date: Fri, 24 Feb 2023 20:35:05 +0000 Subject: [PATCH] [Badge] Add shape appearance for badges PiperOrigin-RevId: 512137782 (cherry picked from commit 2ddcfe46b7603c89b6715ac4406aa3eb6c275f60) --- docs/components/BadgeDrawable.md | 31 ++++-- .../android/material/badge/BadgeDrawable.java | 104 ++++++++++++++++-- .../android/material/badge/BadgeState.java | 98 ++++++++++++++++- .../badge/res-public/values/public.xml | 8 ++ .../material/badge/res/values/attrs.xml | 24 +++- .../material/badge/res/values/dimens.xml | 8 +- .../material/badge/res/values/styles.xml | 14 ++- 7 files changed, 253 insertions(+), 34 deletions(-) diff --git a/docs/components/BadgeDrawable.md b/docs/components/BadgeDrawable.md index 2bc5d9091df..9a0b463adbe 100644 --- a/docs/components/BadgeDrawable.md +++ b/docs/components/BadgeDrawable.md @@ -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`
`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`
| +: : `app\:badgeTextColor` : +| Width | `app:badgeWidth`
| +: : `app\:badgeWithTextWidth` : +| Height | `app:badgeHeight`
| +: : `app\:badgeWithTextHeight` : +| Shape | `app:badgeShapeAppearance`
| +: : `app\:badgeShapeAppearanceOverlay`
: +: : `app\:badgeWithTextShapeAppearance`
: +: : `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 diff --git a/lib/java/com/google/android/material/badge/BadgeDrawable.java b/lib/java/com/google/android/material/badge/BadgeDrawable.java index 0fa0c68f47f..d926292425c 100644 --- a/lib/java/com/google/android/material/badge/BadgeDrawable.java +++ b/lib/java/com/google/android/material/badge/BadgeDrawable.java @@ -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; @@ -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 contextRef; @NonNull private final MaterialShapeDrawable shapeDrawable; @NonNull private final TextDrawableHelper textDrawableHelper; @@ -249,6 +253,7 @@ private void onVisibilityUpdated() { } private void restoreState() { + onBadgeShapeAppearanceUpdated(); onBadgeTextAppearanceUpdated(); onMaxCharacterCountUpdated(); @@ -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(); } @@ -520,6 +534,7 @@ public void clearNumber() { private void onNumberUpdated() { textDrawableHelper.setTextWidthDirty(true); + onBadgeShapeAppearanceUpdated(); updateCenterAndBounds(); invalidateSelf(); } @@ -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; @@ -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); } @@ -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(); diff --git a/lib/java/com/google/android/material/badge/BadgeState.java b/lib/java/com/google/android/material/badge/BadgeState.java index c74f3022bb3..a62cc5629ca 100644 --- a/lib/java/com/google/android/material/badge/BadgeState.java +++ b/lib/java/com/google/android/material/badge/BadgeState.java @@ -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; @@ -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; @@ -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, @@ -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); @@ -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) @@ -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; @@ -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; @@ -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(); @@ -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); diff --git a/lib/java/com/google/android/material/badge/res-public/values/public.xml b/lib/java/com/google/android/material/badge/res-public/values/public.xml index 5f5a79aaeb0..e157e8f2573 100644 --- a/lib/java/com/google/android/material/badge/res-public/values/public.xml +++ b/lib/java/com/google/android/material/badge/res-public/values/public.xml @@ -25,6 +25,14 @@ + + + + + + + + diff --git a/lib/java/com/google/android/material/badge/res/values/attrs.xml b/lib/java/com/google/android/material/badge/res/values/attrs.xml index d48c74562f9..a221639bd69 100644 --- a/lib/java/com/google/android/material/badge/res/values/attrs.xml +++ b/lib/java/com/google/android/material/badge/res/values/attrs.xml @@ -21,12 +21,22 @@ - + - + + + + + + + + + @@ -43,6 +53,16 @@ + + + + + + + + diff --git a/lib/java/com/google/android/material/badge/res/values/dimens.xml b/lib/java/com/google/android/material/badge/res/values/dimens.xml index 42be9f322f0..b32eb0d5891 100644 --- a/lib/java/com/google/android/material/badge/res/values/dimens.xml +++ b/lib/java/com/google/android/material/badge/res/values/dimens.xml @@ -16,8 +16,8 @@ --> - 4dp - 8dp + 8dp + 16dp 4dp @@ -32,8 +32,8 @@ 12dp - 3dp - 8dp + 6dp + 16dp 1.5dp 1.5dp diff --git a/lib/java/com/google/android/material/badge/res/values/styles.xml b/lib/java/com/google/android/material/badge/res/values/styles.xml index 0ca533cdc07..a0d3010c17d 100644 --- a/lib/java/com/google/android/material/badge/res/values/styles.xml +++ b/lib/java/com/google/android/material/badge/res/values/styles.xml @@ -17,14 +17,18 @@