diff --git a/lib/java/com/google/android/material/badge/BadgeDrawable.java b/lib/java/com/google/android/material/badge/BadgeDrawable.java
index b5de8d913a6..6719c6e221a 100644
--- a/lib/java/com/google/android/material/badge/BadgeDrawable.java
+++ b/lib/java/com/google/android/material/badge/BadgeDrawable.java
@@ -23,19 +23,12 @@
import android.content.Context;
import android.content.res.ColorStateList;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
-import android.os.Build.VERSION;
-import android.os.Build.VERSION_CODES;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
@@ -43,7 +36,6 @@
import android.widget.FrameLayout.LayoutParams;
import androidx.annotation.AttrRes;
import androidx.annotation.ColorInt;
-import androidx.annotation.Dimension;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -52,14 +44,11 @@
import androidx.annotation.RestrictTo;
import androidx.annotation.StringRes;
import androidx.annotation.StyleRes;
-import androidx.annotation.StyleableRes;
import androidx.annotation.XmlRes;
import androidx.core.view.ViewCompat;
-import com.google.android.material.drawable.DrawableUtils;
import com.google.android.material.internal.TextDrawableHelper;
import com.google.android.material.internal.TextDrawableHelper.TextDrawableDelegate;
import com.google.android.material.internal.ThemeEnforcement;
-import com.google.android.material.resources.MaterialResources;
import com.google.android.material.resources.TextAppearance;
import com.google.android.material.shape.MaterialShapeDrawable;
import java.lang.annotation.Retention;
@@ -74,7 +63,7 @@
*
You can use {@code BadgeDrawable} to display dynamic information such as a number of pending
* requests in a {@link com.google.android.material.bottomnavigation.BottomNavigationView}. To
* create an instance of {@code BadgeDrawable}, use {@link #create(Context)} or {@link
- * #createFromResources(Context, int)}. How to add and display a {@code BadgeDrawable} on top of its
+ * #createFromResource(Context, int)}. How to add and display a {@code BadgeDrawable} on top of its
* anchor view depends on the API level:
*
*
For API 18+ (APIs supported by {@link android.view.ViewOverlay})
@@ -125,7 +114,7 @@
*
By default, {@code BadgeDrawable} is aligned to the top and end edges of its anchor view (with
* some offsets). Call {@link #setBadgeGravity(int)} to change it to one of the other supported
* modes. To adjust the badge's offsets w.r.t. the anchor's center, use {@link
- * BadgeDrawable#setHoriziontalOffset(int)}, {@link BadgeDrawable#setVerticalOffset(int)}
+ * BadgeDrawable#setHorizontalOffset(int)}, {@link BadgeDrawable#setVerticalOffset(int)}
*
*
Note: This is still under development and may not support the full range of customization
* Material Android components generally support (e.g. themed attributes).
@@ -154,15 +143,6 @@ public class BadgeDrawable extends Drawable implements TextDrawableDelegate {
/** The badge is positioned along the bottom and start edges of its anchor view */
public static final int BOTTOM_START = Gravity.BOTTOM | Gravity.START;
- /**
- * Maximum number of characters a badge supports displaying by default. It could be changed using
- * {@link BadgeDrawable#setMaxBadgeCount(int)}.
- */
- private static final int DEFAULT_MAX_BADGE_CHARACTER_COUNT = 4;
-
- /** Value of -1 denotes a numberless badge. */
- private static final int BADGE_NUMBER_NONE = -1;
-
/** Maximum value of number that can be displayed in a circular badge. */
private static final int MAX_CIRCULAR_BADGE_NUMBER_COUNT = 9;
@@ -179,10 +159,8 @@ public class BadgeDrawable extends Drawable implements TextDrawableDelegate {
@NonNull private final MaterialShapeDrawable shapeDrawable;
@NonNull private final TextDrawableHelper textDrawableHelper;
@NonNull private final Rect badgeBounds;
- private float badgeRadius;
- private float badgeWithTextRadius;
- private float badgeWidePadding;
- @NonNull private final SavedState savedState;
+
+ @NonNull private final BadgeState state;
private float badgeCenterX;
private float badgeCenterY;
@@ -195,140 +173,22 @@ public class BadgeDrawable extends Drawable implements TextDrawableDelegate {
@Nullable private WeakReference anchorViewRef;
@Nullable private WeakReference customBadgeParentRef;
- /**
- * A {@link Parcelable} implementation used to ensure the state of {@code BadgeDrawable} is saved.
- *
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public static final class SavedState implements Parcelable {
-
- @ColorInt private int backgroundColor;
- @ColorInt private int badgeTextColor;
- private int alpha = 255;
- private int number = BADGE_NUMBER_NONE;
- private int maxCharacterCount;
- @Nullable private CharSequence contentDescriptionNumberless;
- @PluralsRes private int contentDescriptionQuantityStrings;
- @StringRes private int contentDescriptionExceedsMaxBadgeNumberRes;
- @BadgeGravity private int badgeGravity;
- private boolean isVisible;
- private Locale numberLocale;
-
- @Dimension(unit = Dimension.PX)
- private int horizontalOffsetWithoutText;
-
- @Dimension(unit = Dimension.PX)
- private int verticalOffsetWithoutText;
-
- @Dimension(unit = Dimension.PX)
- private int horizontalOffsetWithText;
-
- @Dimension(unit = Dimension.PX)
- private int verticalOffsetWithText;
-
- @Dimension(unit = Dimension.PX)
- private int additionalHorizontalOffset;
-
- @Dimension(unit = Dimension.PX)
- private int additionalVerticalOffset;
-
- public SavedState(@NonNull Context context) {
- // If the badge text color attribute was not explicitly set, use the text color specified in
- // the TextAppearance.
- TextAppearance textAppearance =
- new TextAppearance(context, R.style.TextAppearance_MaterialComponents_Badge);
- badgeTextColor = textAppearance.getTextColor().getDefaultColor();
- contentDescriptionNumberless =
- context.getString(R.string.mtrl_badge_numberless_content_description);
- contentDescriptionQuantityStrings = R.plurals.mtrl_badge_content_description;
- contentDescriptionExceedsMaxBadgeNumberRes =
- R.string.mtrl_exceed_max_badge_number_content_description;
- isVisible = true;
- numberLocale =
- VERSION.SDK_INT >= VERSION_CODES.N
- ? Locale.getDefault(Locale.Category.FORMAT)
- : Locale.getDefault();
- }
-
- protected SavedState(@NonNull Parcel in) {
- backgroundColor = in.readInt();
- badgeTextColor = in.readInt();
- alpha = in.readInt();
- number = in.readInt();
- maxCharacterCount = in.readInt();
- contentDescriptionNumberless = in.readString();
- contentDescriptionQuantityStrings = in.readInt();
- badgeGravity = in.readInt();
- horizontalOffsetWithoutText = in.readInt();
- verticalOffsetWithoutText = in.readInt();
- horizontalOffsetWithText = in.readInt();
- verticalOffsetWithText = in.readInt();
- additionalHorizontalOffset = in.readInt();
- additionalVerticalOffset = in.readInt();
- isVisible = in.readInt() != 0;
- numberLocale = (Locale) in.readSerializable();
- }
-
- public static final Creator CREATOR =
- new Creator() {
- @NonNull
- @Override
- public SavedState createFromParcel(@NonNull Parcel in) {
- return new SavedState(in);
- }
-
- @NonNull
- @Override
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(@NonNull Parcel dest, int flags) {
- dest.writeInt(backgroundColor);
- dest.writeInt(badgeTextColor);
- dest.writeInt(alpha);
- dest.writeInt(number);
- dest.writeInt(maxCharacterCount);
- dest.writeString(contentDescriptionNumberless.toString());
- dest.writeInt(contentDescriptionQuantityStrings);
- dest.writeInt(badgeGravity);
- dest.writeInt(horizontalOffsetWithoutText);
- dest.writeInt(verticalOffsetWithoutText);
- dest.writeInt(horizontalOffsetWithText);
- dest.writeInt(verticalOffsetWithText);
- dest.writeInt(additionalHorizontalOffset);
- dest.writeInt(additionalVerticalOffset);
- dest.writeInt(isVisible ? 1 : 0);
- dest.writeSerializable(numberLocale);
- }
- }
-
@NonNull
- public SavedState getSavedState() {
- return savedState;
+ public BadgeState.State getSavedState() {
+ return state.getOverridingState();
}
- /** Creates an instance of {@code BadgeDrawable} with the provided {@link SavedState}. */
+ /** Creates an instance of {@code BadgeDrawable} with the provided {@link BadgeState.State}. */
@NonNull
static BadgeDrawable createFromSavedState(
- @NonNull Context context, @NonNull SavedState savedState) {
- BadgeDrawable badge = new BadgeDrawable(context);
- badge.restoreFromSavedState(savedState);
- return badge;
+ @NonNull Context context, @NonNull BadgeState.State savedState) {
+ return new BadgeDrawable(context, 0, DEFAULT_THEME_ATTR, DEFAULT_STYLE, savedState);
}
/** Creates an instance of {@code BadgeDrawable} with default values. */
@NonNull
public static BadgeDrawable create(@NonNull Context context) {
- return createFromAttributes(context, /* attrs= */ null, DEFAULT_THEME_ATTR, DEFAULT_STYLE);
+ return new BadgeDrawable(context, 0, DEFAULT_THEME_ATTR, DEFAULT_STYLE, null);
}
/**
@@ -345,24 +205,7 @@ public static BadgeDrawable create(@NonNull Context context) {
*/
@NonNull
public static BadgeDrawable createFromResource(@NonNull Context context, @XmlRes int id) {
- AttributeSet attrs = DrawableUtils.parseDrawableXml(context, id, "badge");
- @StyleRes int style = attrs.getStyleAttribute();
- if (style == 0) {
- style = DEFAULT_STYLE;
- }
- return createFromAttributes(context, attrs, DEFAULT_THEME_ATTR, style);
- }
-
- /** Returns a {@code BadgeDrawable} from the given attributes. */
- @NonNull
- private static BadgeDrawable createFromAttributes(
- @NonNull Context context,
- AttributeSet attrs,
- @AttrRes int defStyleAttr,
- @StyleRes int defStyleRes) {
- BadgeDrawable badge = new BadgeDrawable(context);
- badge.loadDefaultStateFromAttributes(context, attrs, defStyleAttr, defStyleRes);
- return badge;
+ return new BadgeDrawable(context, id, DEFAULT_THEME_ATTR, DEFAULT_STYLE, null);
}
/**
@@ -370,8 +213,13 @@ private static BadgeDrawable createFromAttributes(
* restart} parameter hardcoded to false.
*/
public void setVisible(boolean visible) {
+ state.setVisible(visible);
+ onVisibilityUpdated();
+ }
+
+ private void onVisibilityUpdated() {
+ boolean visible = state.isVisible();
setVisible(visible, /* restart= */ false);
- savedState.isVisible = visible;
// When hiding a badge in pre-API 18, invalidate the custom parent in order to trigger a draw
// pass to remove this badge from its foreground.
if (BadgeUtils.USE_COMPAT_PARENT && getCustomBadgeParent() != null && !visible) {
@@ -379,112 +227,39 @@ public void setVisible(boolean visible) {
}
}
- private void restoreFromSavedState(@NonNull SavedState savedState) {
- setMaxCharacterCount(savedState.maxCharacterCount);
- setBadgeNumberLocale(savedState.numberLocale);
-
- // Only set the badge number if it exists in the style.
- // Defaulting it to 0 means the badge will incorrectly show text when the user may want a
- // numberless badge.
- if (savedState.number != BADGE_NUMBER_NONE) {
- setNumber(savedState.number);
- }
-
- setBackgroundColor(savedState.backgroundColor);
-
- // Only set the badge text color if this attribute has explicitly been set, otherwise use the
- // text color specified in the TextAppearance.
- setBadgeTextColor(savedState.badgeTextColor);
-
- setBadgeGravity(savedState.badgeGravity);
-
- setHorizontalOffsetWithoutText(savedState.horizontalOffsetWithoutText);
- setVerticalOffsetWithoutText(savedState.verticalOffsetWithoutText);
-
- setHorizontalOffsetWithText(savedState.horizontalOffsetWithText);
- setVerticalOffsetWithText(savedState.verticalOffsetWithText);
-
- setAdditionalHorizontalOffset(savedState.additionalHorizontalOffset);
- setAdditionalVerticalOffset(savedState.additionalVerticalOffset);
-
- setVisible(savedState.isVisible);
- }
-
- private void loadDefaultStateFromAttributes(
- Context context, AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
- TypedArray a =
- ThemeEnforcement.obtainStyledAttributes(
- context, attrs, R.styleable.Badge, defStyleAttr, defStyleRes);
+ private void restoreState() {
+ onMaxCharacterCountUpdated();
- setMaxCharacterCount(
- a.getInt(R.styleable.Badge_maxCharacterCount, DEFAULT_MAX_BADGE_CHARACTER_COUNT));
+ onNumberUpdated();
+ onAlphaUpdated();
+ onBackgroundColorUpdated();
+ onBadgeTextColorUpdated();
+ onBadgeGravityUpdated();
- // Only set the badge number if it exists in the style.
- // Defaulting it to 0 means the badge will incorrectly show text when the user may want a
- // numberless badge.
- if (a.hasValue(R.styleable.Badge_number)) {
- setNumber(a.getInt(R.styleable.Badge_number, 0));
- }
-
- setBackgroundColor(readColorFromAttributes(context, a, R.styleable.Badge_backgroundColor));
-
- // Only set the badge text color if this attribute has explicitly been set, otherwise use the
- // text color specified in the TextAppearance.
- if (a.hasValue(R.styleable.Badge_badgeTextColor)) {
- setBadgeTextColor(readColorFromAttributes(context, a, R.styleable.Badge_badgeTextColor));
- }
-
- setBadgeGravity(a.getInt(R.styleable.Badge_badgeGravity, TOP_END));
-
- setHorizontalOffsetWithoutText(
- a.getDimensionPixelOffset(R.styleable.Badge_horizontalOffset, 0));
- setVerticalOffsetWithoutText(a.getDimensionPixelOffset(R.styleable.Badge_verticalOffset, 0));
-
- // Set the offsets when the badge has text. Default to using the badge "dot" offsets
- // (horizontalOffsetWithoutText and verticalOffsetWithoutText) if there is no offsets defined
- // for badges with text.
- setHorizontalOffsetWithText(
- a.getDimensionPixelOffset(
- R.styleable.Badge_horizontalOffsetWithText, getHorizontalOffsetWithoutText()));
- setVerticalOffsetWithText(
- a.getDimensionPixelOffset(
- R.styleable.Badge_verticalOffsetWithText, getVerticalOffsetWithoutText()));
-
- if (a.hasValue(R.styleable.Badge_badgeRadius)) {
- badgeRadius = a.getDimensionPixelSize(R.styleable.Badge_badgeRadius, (int) badgeRadius);
- }
- if (a.hasValue(R.styleable.Badge_badgeWidePadding)) {
- badgeWidePadding =
- a.getDimensionPixelSize(R.styleable.Badge_badgeWidePadding, (int) badgeWidePadding);
- }
- if (a.hasValue(R.styleable.Badge_badgeWithTextRadius)) {
- badgeWithTextRadius =
- a.getDimensionPixelSize(R.styleable.Badge_badgeWithTextRadius, (int) badgeWithTextRadius);
- }
-
- a.recycle();
- }
-
- private static int readColorFromAttributes(
- Context context, @NonNull TypedArray a, @StyleableRes int index) {
- return MaterialResources.getColorStateList(context, a, index).getDefaultColor();
+ updateCenterAndBounds();
+ onVisibilityUpdated();
}
- private BadgeDrawable(@NonNull Context context) {
+ private BadgeDrawable(
+ @NonNull Context context,
+ @XmlRes int badgeResId,
+ @AttrRes int defStyleAttr,
+ @StyleRes int defStyleRes,
+ @Nullable BadgeState.State savedState) {
this.contextRef = new WeakReference<>(context);
ThemeEnforcement.checkMaterialTheme(context);
- Resources res = context.getResources();
badgeBounds = new Rect();
shapeDrawable = new MaterialShapeDrawable();
- badgeRadius = res.getDimensionPixelSize(R.dimen.mtrl_badge_radius);
- badgeWidePadding = res.getDimensionPixelSize(R.dimen.mtrl_badge_long_text_horizontal_padding);
- badgeWithTextRadius = res.getDimensionPixelSize(R.dimen.mtrl_badge_with_text_radius);
-
textDrawableHelper = new TextDrawableHelper(/* delegate= */ this);
textDrawableHelper.getTextPaint().setTextAlign(Paint.Align.CENTER);
- this.savedState = new SavedState(context);
+
+ // TODO(b/209973014): make sure this is right
setTextAppearanceResource(R.style.TextAppearance_MaterialComponents_Badge);
+
+ this.state = new BadgeState(context, badgeResId, defStyleAttr, defStyleRes, savedState);
+
+ restoreState();
}
/**
@@ -625,8 +400,12 @@ public int getBackgroundColor() {
* @attr ref com.google.android.material.R.styleable#Badge_backgroundColor
*/
public void setBackgroundColor(@ColorInt int backgroundColor) {
- savedState.backgroundColor = backgroundColor;
- ColorStateList backgroundColorStateList = ColorStateList.valueOf(backgroundColor);
+ state.setBackgroundColor(backgroundColor);
+ onBackgroundColorUpdated();
+ }
+
+ private void onBackgroundColorUpdated() {
+ ColorStateList backgroundColorStateList = ColorStateList.valueOf(state.getBackgroundColor());
if (shapeDrawable.getFillColor() != backgroundColorStateList) {
shapeDrawable.setFillColor(backgroundColorStateList);
invalidateSelf();
@@ -651,30 +430,34 @@ public int getBadgeTextColor() {
* @attr ref com.google.android.material.R.styleable#Badge_badgeTextColor
*/
public void setBadgeTextColor(@ColorInt int badgeTextColor) {
- savedState.badgeTextColor = badgeTextColor;
if (textDrawableHelper.getTextPaint().getColor() != badgeTextColor) {
- textDrawableHelper.getTextPaint().setColor(badgeTextColor);
- invalidateSelf();
+ state.setBadgeTextColor(badgeTextColor);
+ onBadgeTextColorUpdated();
}
}
+ private void onBadgeTextColorUpdated() {
+ textDrawableHelper.getTextPaint().setColor(state.getBadgeTextColor());
+ invalidateSelf();
+ }
+
/** Returns the {@link Locale} used to show badge numbers. */
@NonNull
public Locale getBadgeNumberLocale() {
- return savedState.numberLocale;
+ return state.getNumberLocale();
}
/** Sets the {@link Locale} used to show badge numbers. */
public void setBadgeNumberLocale(@NonNull Locale locale) {
- if (!locale.equals(savedState.numberLocale)) {
- savedState.numberLocale = locale;
+ if (!locale.equals(state.getNumberLocale())) {
+ state.setNumberLocale(locale);
invalidateSelf();
}
}
/** Returns whether this badge will display a number. */
public boolean hasNumber() {
- return savedState.number != BADGE_NUMBER_NONE;
+ return state.hasNumber();
}
/**
@@ -687,10 +470,7 @@ public boolean hasNumber() {
* @attr ref com.google.android.material.R.styleable#Badge_number
*/
public int getNumber() {
- if (!hasNumber()) {
- return 0;
- }
- return savedState.number;
+ return hasNumber() ? state.getNumber() : 0;
}
/**
@@ -703,17 +483,22 @@ public int getNumber() {
*/
public void setNumber(int number) {
number = Math.max(0, number);
- if (this.savedState.number != number) {
- this.savedState.number = number;
- textDrawableHelper.setTextWidthDirty(true);
- updateCenterAndBounds();
- invalidateSelf();
+ if (this.state.getNumber() != number) {
+ state.setNumber(number);
+ onNumberUpdated();
}
}
/** Resets any badge number so that a numberless badge will be displayed. */
public void clearNumber() {
- savedState.number = BADGE_NUMBER_NONE;
+ if (hasNumber()) {
+ state.clearNumber();
+ onNumberUpdated();
+ }
+ }
+
+ private void onNumberUpdated() {
+ textDrawableHelper.setTextWidthDirty(true);
updateCenterAndBounds();
invalidateSelf();
}
@@ -725,7 +510,7 @@ public void clearNumber() {
* @attr ref com.google.android.material.R.styleable#Badge_maxCharacterCount
*/
public int getMaxCharacterCount() {
- return savedState.maxCharacterCount;
+ return state.getMaxCharacterCount();
}
/**
@@ -735,18 +520,22 @@ public int getMaxCharacterCount() {
* @attr ref com.google.android.material.R.styleable#Badge_maxCharacterCount
*/
public void setMaxCharacterCount(int maxCharacterCount) {
- if (this.savedState.maxCharacterCount != maxCharacterCount) {
- this.savedState.maxCharacterCount = maxCharacterCount;
- updateMaxBadgeNumber();
- textDrawableHelper.setTextWidthDirty(true);
- updateCenterAndBounds();
- invalidateSelf();
+ if (this.state.getMaxCharacterCount() != maxCharacterCount) {
+ this.state.setMaxCharacterCount(maxCharacterCount);
+ onMaxCharacterCountUpdated();
}
}
+ private void onMaxCharacterCountUpdated() {
+ updateMaxBadgeNumber();
+ textDrawableHelper.setTextWidthDirty(true);
+ updateCenterAndBounds();
+ invalidateSelf();
+ }
+
@BadgeGravity
public int getBadgeGravity() {
- return savedState.badgeGravity;
+ return state.getBadgeGravity();
}
/**
@@ -755,12 +544,16 @@ public int getBadgeGravity() {
* @param gravity Constant representing one of 4 possible {@link BadgeGravity} values.
*/
public void setBadgeGravity(@BadgeGravity int gravity) {
- if (savedState.badgeGravity != gravity) {
- savedState.badgeGravity = gravity;
- if (anchorViewRef != null && anchorViewRef.get() != null) {
- updateBadgeCoordinates(
- anchorViewRef.get(), customBadgeParentRef != null ? customBadgeParentRef.get() : null);
- }
+ if (state.getBadgeGravity() != gravity) {
+ state.setBadgeGravity(gravity);
+ onBadgeGravityUpdated();
+ }
+ }
+
+ private void onBadgeGravityUpdated() {
+ if (anchorViewRef != null && anchorViewRef.get() != null) {
+ updateBadgeCoordinates(
+ anchorViewRef.get(), customBadgeParentRef != null ? customBadgeParentRef.get() : null);
}
}
@@ -776,13 +569,17 @@ public void setColorFilter(ColorFilter colorFilter) {
@Override
public int getAlpha() {
- return savedState.alpha;
+ return state.getAlpha();
}
@Override
public void setAlpha(int alpha) {
- this.savedState.alpha = alpha;
- textDrawableHelper.getTextPaint().setAlpha(alpha);
+ state.setAlpha(alpha);
+ onAlphaUpdated();
+ }
+
+ private void onAlphaUpdated() {
+ textDrawableHelper.getTextPaint().setAlpha(getAlpha());
invalidateSelf();
}
@@ -832,16 +629,16 @@ public boolean onStateChange(int[] state) {
}
public void setContentDescriptionNumberless(CharSequence charSequence) {
- savedState.contentDescriptionNumberless = charSequence;
+ state.setContentDescriptionNumberless(charSequence);
}
public void setContentDescriptionQuantityStringsResource(@PluralsRes int stringsResource) {
- savedState.contentDescriptionQuantityStrings = stringsResource;
+ state.setContentDescriptionQuantityStringsResource(stringsResource);
}
public void setContentDescriptionExceedsMaxBadgeNumberStringResource(
@StringRes int stringsResource) {
- savedState.contentDescriptionExceedsMaxBadgeNumberRes = stringsResource;
+ state.setContentDescriptionExceedsMaxBadgeNumberStringResource(stringsResource);
}
@Nullable
@@ -850,7 +647,7 @@ public CharSequence getContentDescription() {
return null;
}
if (hasNumber()) {
- if (savedState.contentDescriptionQuantityStrings > 0) {
+ if (state.getContentDescriptionQuantityStrings() != 0) {
Context context = contextRef.get();
if (context == null) {
return null;
@@ -859,16 +656,16 @@ public CharSequence getContentDescription() {
return context
.getResources()
.getQuantityString(
- savedState.contentDescriptionQuantityStrings, getNumber(), getNumber());
+ state.getContentDescriptionQuantityStrings(), getNumber(), getNumber());
} else {
return context.getString(
- savedState.contentDescriptionExceedsMaxBadgeNumberRes, maxBadgeNumber);
+ state.getContentDescriptionExceedsMaxBadgeNumberStringResource(), maxBadgeNumber);
}
} else {
return null;
}
} else {
- return savedState.contentDescriptionNumberless;
+ return state.getContentDescriptionNumberless();
}
}
@@ -893,7 +690,7 @@ public void setHorizontalOffset(int px) {
* #getHorizontalOffsetWithText}.
*/
public int getHorizontalOffset() {
- return savedState.horizontalOffsetWithoutText;
+ return state.getHorizontalOffsetWithoutText();
}
/**
@@ -903,7 +700,7 @@ public int getHorizontalOffset() {
* @param px badge's horizontal offset when the badge does not have text
*/
public void setHorizontalOffsetWithoutText(@Px int px) {
- savedState.horizontalOffsetWithoutText = px;
+ state.setHorizontalOffsetWithoutText(px);
updateCenterAndBounds();
}
@@ -913,7 +710,7 @@ public void setHorizontalOffsetWithoutText(@Px int px) {
*/
@Px
public int getHorizontalOffsetWithoutText() {
- return savedState.horizontalOffsetWithoutText;
+ return state.getHorizontalOffsetWithoutText();
}
/**
@@ -923,7 +720,7 @@ public int getHorizontalOffsetWithoutText() {
* @param px badge's horizontal offset when the badge has text.
*/
public void setHorizontalOffsetWithText(@Px int px) {
- savedState.horizontalOffsetWithText = px;
+ state.setHorizontalOffsetWithText(px);
updateCenterAndBounds();
}
@@ -933,7 +730,7 @@ public void setHorizontalOffsetWithText(@Px int px) {
*/
@Px
public int getHorizontalOffsetWithText() {
- return savedState.horizontalOffsetWithText;
+ return state.getHorizontalOffsetWithText();
}
/**
@@ -942,12 +739,12 @@ public int getHorizontalOffsetWithText() {
* placement of badges on toolbar items.
*/
void setAdditionalHorizontalOffset(int px) {
- savedState.additionalHorizontalOffset = px;
+ state.setAdditionalHorizontalOffset(px);
updateCenterAndBounds();
}
int getAdditionalHorizontalOffset() {
- return savedState.additionalHorizontalOffset;
+ return state.getAdditionalHorizontalOffset();
}
/**
@@ -971,7 +768,7 @@ public void setVerticalOffset(int px) {
* #getVerticalOffsetWithText}.
*/
public int getVerticalOffset() {
- return savedState.verticalOffsetWithoutText;
+ return state.getVerticalOffsetWithoutText();
}
/**
@@ -981,7 +778,7 @@ public int getVerticalOffset() {
* @param px badge's vertical offset when the badge does not have text
*/
public void setVerticalOffsetWithoutText(@Px int px) {
- savedState.verticalOffsetWithoutText = px;
+ state.setVerticalOffsetWithoutText(px);
updateCenterAndBounds();
}
@@ -991,7 +788,7 @@ public void setVerticalOffsetWithoutText(@Px int px) {
*/
@Px
public int getVerticalOffsetWithoutText() {
- return savedState.verticalOffsetWithoutText;
+ return state.getVerticalOffsetWithoutText();
}
/**
@@ -1001,7 +798,7 @@ public int getVerticalOffsetWithoutText() {
* @param px badge's vertical offset when the badge has text.
*/
public void setVerticalOffsetWithText(@Px int px) {
- savedState.verticalOffsetWithText = px;
+ state.setVerticalOffsetWithText(px);
updateCenterAndBounds();
}
@@ -1011,7 +808,7 @@ public void setVerticalOffsetWithText(@Px int px) {
*/
@Px
public int getVerticalOffsetWithText() {
- return savedState.verticalOffsetWithText;
+ return state.getVerticalOffsetWithText();
}
/**
@@ -1019,13 +816,14 @@ public int getVerticalOffsetWithText() {
* move this badge towards the center of its anchor. Currently used to adjust the placement of
* badges on toolbar items.
*/
- void setAdditionalVerticalOffset(int px) {
- savedState.additionalVerticalOffset = px;
+ void setAdditionalVerticalOffset(@Px int px) {
+ state.setAdditionalVerticalOffset(px);
updateCenterAndBounds();
}
+ @Px
int getAdditionalVerticalOffset() {
- return savedState.additionalVerticalOffset;
+ return state.getAdditionalVerticalOffset();
}
private void setTextAppearanceResource(@StyleRes int id) {
@@ -1081,20 +879,20 @@ private void updateCenterAndBounds() {
private int getTotalVerticalOffsetForState() {
int vOffset =
- hasNumber() ? savedState.verticalOffsetWithText : savedState.verticalOffsetWithoutText;
- return vOffset + savedState.additionalVerticalOffset;
+ hasNumber() ? state.getVerticalOffsetWithText() : state.getVerticalOffsetWithoutText();
+ return vOffset + state.getAdditionalVerticalOffset();
}
private int getTotalHorizontalOffsetForState() {
int hOffset =
- hasNumber() ? savedState.horizontalOffsetWithText : savedState.horizontalOffsetWithoutText;
- return hOffset + savedState.additionalHorizontalOffset;
+ hasNumber() ? state.getHorizontalOffsetWithText() : state.getHorizontalOffsetWithoutText();
+ return hOffset + state.getAdditionalHorizontalOffset();
}
private void calculateCenterAndBounds(
@NonNull Context context, @NonNull Rect anchorRect, @NonNull View anchorView) {
int totalVerticalOffset = getTotalVerticalOffsetForState();
- switch (savedState.badgeGravity) {
+ switch (state.getBadgeGravity()) {
case BOTTOM_END:
case BOTTOM_START:
badgeCenterY = anchorRect.bottom - totalVerticalOffset;
@@ -1107,14 +905,14 @@ private void calculateCenterAndBounds(
}
if (getNumber() <= MAX_CIRCULAR_BADGE_NUMBER_COUNT) {
- cornerRadius = !hasNumber() ? badgeRadius : badgeWithTextRadius;
+ cornerRadius = !hasNumber() ? state.badgeRadius : state.badgeWithTextRadius;
halfBadgeHeight = cornerRadius;
halfBadgeWidth = cornerRadius;
} else {
- cornerRadius = badgeWithTextRadius;
+ cornerRadius = state.badgeWithTextRadius;
halfBadgeHeight = cornerRadius;
String badgeText = getBadgeText();
- halfBadgeWidth = textDrawableHelper.getTextWidth(badgeText) / 2f + badgeWidePadding;
+ halfBadgeWidth = textDrawableHelper.getTextWidth(badgeText) / 2f + state.badgeWidePadding;
}
int inset =
@@ -1128,7 +926,7 @@ private void calculateCenterAndBounds(
int totalHorizontalOffset = getTotalHorizontalOffsetForState();
// Update the centerX based on the badge width and 'inset' from start or end boundary of anchor.
- switch (savedState.badgeGravity) {
+ switch (state.getBadgeGravity()) {
case BOTTOM_START:
case TOP_START:
badgeCenterX =
@@ -1162,7 +960,7 @@ private void drawText(Canvas canvas) {
private String getBadgeText() {
// If number exceeds max count, show badgeMaxCount+ instead of the number.
if (getNumber() <= maxBadgeNumber) {
- return NumberFormat.getInstance(savedState.numberLocale).format(getNumber());
+ return NumberFormat.getInstance(state.getNumberLocale()).format(getNumber());
} else {
Context context = contextRef.get();
if (context == null) {
@@ -1170,7 +968,7 @@ private String getBadgeText() {
}
return String.format(
- savedState.numberLocale,
+ state.getNumberLocale(),
context.getString(R.string.mtrl_exceed_max_badge_number_suffix),
maxBadgeNumber,
DEFAULT_EXCEED_MAX_BADGE_NUMBER_SUFFIX);
diff --git a/lib/java/com/google/android/material/badge/BadgeState.java b/lib/java/com/google/android/material/badge/BadgeState.java
new file mode 100644
index 00000000000..6f0fa5da541
--- /dev/null
+++ b/lib/java/com/google/android/material/badge/BadgeState.java
@@ -0,0 +1,515 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.material.badge;
+
+import com.google.android.material.R;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import static com.google.android.material.badge.BadgeDrawable.TOP_END;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import androidx.annotation.AttrRes;
+import androidx.annotation.ColorInt;
+import androidx.annotation.Dimension;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.PluralsRes;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.StringRes;
+import androidx.annotation.StyleRes;
+import androidx.annotation.StyleableRes;
+import androidx.annotation.XmlRes;
+import com.google.android.material.badge.BadgeDrawable.BadgeGravity;
+import com.google.android.material.drawable.DrawableUtils;
+import com.google.android.material.internal.ThemeEnforcement;
+import com.google.android.material.resources.MaterialResources;
+import com.google.android.material.resources.TextAppearance;
+import java.util.Locale;
+
+/**
+ * Provides a {@link Parcelable} implementation ({@link State}) used to ensure the state of {@code
+ * BadgeDrawable} is saved and restored properly, and the default values of the states are correctly
+ * loaded during every restoration.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public final class BadgeState {
+ /**
+ * Maximum number of characters a badge supports displaying by default. It could be changed using
+ * {@link BadgeDrawable#setMaxCharacterCount(int)}.
+ */
+ private static final int DEFAULT_MAX_BADGE_CHARACTER_COUNT = 4;
+
+ private static final String BADGE_RESOURCE_TAG = "badge";
+
+ private final State overridingState;
+ private final State currentState = new State();
+
+ final float badgeRadius;
+ final float badgeWithTextRadius;
+ final float badgeWidePadding;
+
+ BadgeState(
+ Context context,
+ @XmlRes int badgeResId,
+ @AttrRes int defStyleAttr,
+ @StyleRes int defStyleRes,
+ @Nullable State storedState) {
+ if (storedState == null) {
+ storedState = new State();
+ }
+ if (badgeResId != 0) {
+ storedState.badgeResId = badgeResId;
+ }
+
+ TypedArray a = generateTypedArray(context, storedState.badgeResId, defStyleAttr, defStyleRes);
+
+ Resources res = context.getResources();
+ badgeRadius =
+ a.getDimensionPixelSize(
+ R.styleable.Badge_badgeRadius, res.getDimensionPixelSize(R.dimen.mtrl_badge_radius));
+ badgeWidePadding =
+ a.getDimensionPixelSize(
+ R.styleable.Badge_badgeWidePadding,
+ res.getDimensionPixelSize(R.dimen.mtrl_badge_long_text_horizontal_padding));
+ badgeWithTextRadius =
+ a.getDimensionPixelSize(
+ R.styleable.Badge_badgeWithTextRadius,
+ res.getDimensionPixelSize(R.dimen.mtrl_badge_with_text_radius));
+
+ currentState.alpha = storedState.alpha == State.NOT_SET ? 255 : storedState.alpha;
+
+ currentState.contentDescriptionNumberless =
+ storedState.contentDescriptionNumberless == null
+ ? context.getString(R.string.mtrl_badge_numberless_content_description)
+ : storedState.contentDescriptionNumberless;
+
+ currentState.contentDescriptionQuantityStrings =
+ storedState.contentDescriptionQuantityStrings == 0
+ ? R.plurals.mtrl_badge_content_description
+ : storedState.contentDescriptionQuantityStrings;
+
+ currentState.contentDescriptionExceedsMaxBadgeNumberRes =
+ storedState.contentDescriptionExceedsMaxBadgeNumberRes == 0
+ ? R.string.mtrl_exceed_max_badge_number_content_description
+ : storedState.contentDescriptionExceedsMaxBadgeNumberRes;
+
+ currentState.isVisible = storedState.isVisible == null || storedState.isVisible;
+
+ currentState.maxCharacterCount =
+ storedState.maxCharacterCount == State.NOT_SET
+ ? a.getInt(R.styleable.Badge_maxCharacterCount, DEFAULT_MAX_BADGE_CHARACTER_COUNT)
+ : storedState.maxCharacterCount;
+
+ // Only set the badge number if it exists in the style.
+ // Defaulting it to 0 means the badge will incorrectly show text when the user may want a
+ // numberless badge.
+ if (storedState.number != State.NOT_SET) {
+ currentState.number = storedState.number;
+ } else if (a.hasValue(R.styleable.Badge_number)) {
+ currentState.number = a.getInt(R.styleable.Badge_number, 0);
+ } else {
+ currentState.number = State.BADGE_NUMBER_NONE;
+ }
+
+ currentState.backgroundColor =
+ storedState.backgroundColor == null
+ ? readColorFromAttributes(context, a, R.styleable.Badge_backgroundColor)
+ : storedState.backgroundColor;
+
+ // Only set the badge text color if this attribute has explicitly been set, otherwise use the
+ // text color specified in the TextAppearance.
+ if (storedState.badgeTextColor != null) {
+ currentState.badgeTextColor = storedState.badgeTextColor;
+ } else if (a.hasValue(R.styleable.Badge_badgeTextColor)) {
+ currentState.badgeTextColor =
+ readColorFromAttributes(context, a, R.styleable.Badge_badgeTextColor);
+ } else {
+ TextAppearance textAppearance =
+ new TextAppearance(context, R.style.TextAppearance_MaterialComponents_Badge);
+ currentState.badgeTextColor = textAppearance.getTextColor().getDefaultColor();
+ }
+
+ currentState.badgeGravity =
+ storedState.badgeGravity == null
+ ? a.getInt(R.styleable.Badge_badgeGravity, TOP_END)
+ : storedState.badgeGravity;
+
+ currentState.horizontalOffsetWithoutText =
+ storedState.horizontalOffsetWithoutText == null
+ ? a.getDimensionPixelOffset(R.styleable.Badge_horizontalOffset, 0)
+ : storedState.horizontalOffsetWithoutText;
+
+ currentState.verticalOffsetWithoutText =
+ storedState.horizontalOffsetWithoutText == null
+ ? a.getDimensionPixelOffset(R.styleable.Badge_verticalOffset, 0)
+ : storedState.verticalOffsetWithoutText;
+
+ // Set the offsets when the badge has text. Default to using the badge "dot" offsets
+ // (horizontalOffsetWithoutText and verticalOffsetWithoutText) if there is no offsets defined
+ // for badges with text.
+ currentState.horizontalOffsetWithText =
+ storedState.horizontalOffsetWithText == null
+ ? a.getDimensionPixelOffset(
+ R.styleable.Badge_horizontalOffsetWithText,
+ currentState.horizontalOffsetWithoutText)
+ : storedState.horizontalOffsetWithText;
+
+ currentState.verticalOffsetWithText =
+ storedState.verticalOffsetWithText == null
+ ? a.getDimensionPixelOffset(
+ R.styleable.Badge_verticalOffsetWithText, currentState.verticalOffsetWithoutText)
+ : storedState.verticalOffsetWithText;
+
+ currentState.additionalHorizontalOffset =
+ storedState.additionalHorizontalOffset == null ? 0 : storedState.additionalHorizontalOffset;
+
+ currentState.additionalVerticalOffset =
+ storedState.additionalVerticalOffset == null ? 0 : storedState.additionalVerticalOffset;
+
+ a.recycle();
+
+ if (storedState.numberLocale == null) {
+ currentState.numberLocale =
+ VERSION.SDK_INT >= VERSION_CODES.N
+ ? Locale.getDefault(Locale.Category.FORMAT)
+ : Locale.getDefault();
+ } else {
+ currentState.numberLocale = storedState.numberLocale;
+ }
+
+ overridingState = storedState;
+ }
+
+ private TypedArray generateTypedArray(
+ Context context,
+ @XmlRes int badgeResId,
+ @AttrRes int defStyleAttr,
+ @StyleRes int defStyleRes) {
+ AttributeSet attrs = null;
+ @StyleRes int style = 0;
+ if (badgeResId != 0) {
+ attrs = DrawableUtils.parseDrawableXml(context, badgeResId, BADGE_RESOURCE_TAG);
+ style = attrs.getStyleAttribute();
+ }
+ if (style == 0) {
+ style = defStyleRes;
+ }
+
+ return ThemeEnforcement.obtainStyledAttributes(
+ context, attrs, R.styleable.Badge, defStyleAttr, style);
+ }
+
+ State getOverridingState() {
+ return overridingState;
+ }
+
+ boolean isVisible() {
+ return currentState.isVisible;
+ }
+
+ void setVisible(boolean visible) {
+ overridingState.isVisible = visible;
+ currentState.isVisible = visible;
+ }
+
+ boolean hasNumber() {
+ return currentState.number != State.BADGE_NUMBER_NONE;
+ }
+
+ int getNumber() {
+ return currentState.number;
+ }
+
+ void setNumber(int number) {
+ overridingState.number = number;
+ currentState.number = number;
+ }
+
+ void clearNumber() {
+ setNumber(State.BADGE_NUMBER_NONE);
+ }
+
+ int getAlpha() {
+ return currentState.alpha;
+ }
+
+ void setAlpha(int alpha) {
+ overridingState.alpha = alpha;
+ currentState.alpha = alpha;
+ }
+
+ int getMaxCharacterCount() {
+ return currentState.maxCharacterCount;
+ }
+
+ void setMaxCharacterCount(int maxCharacterCount) {
+ overridingState.maxCharacterCount = maxCharacterCount;
+ currentState.maxCharacterCount = maxCharacterCount;
+ }
+
+ @ColorInt
+ int getBackgroundColor() {
+ return currentState.backgroundColor;
+ }
+
+ void setBackgroundColor(@ColorInt int backgroundColor) {
+ overridingState.backgroundColor = backgroundColor;
+ currentState.backgroundColor = backgroundColor;
+ }
+
+ @ColorInt
+ int getBadgeTextColor() {
+ return currentState.badgeTextColor;
+ }
+
+ void setBadgeTextColor(@ColorInt int badgeTextColor) {
+ overridingState.badgeTextColor = badgeTextColor;
+ currentState.badgeTextColor = badgeTextColor;
+ }
+
+ @BadgeGravity
+ int getBadgeGravity() {
+ return currentState.badgeGravity;
+ }
+
+ void setBadgeGravity(@BadgeGravity int badgeGravity) {
+ overridingState.badgeGravity = badgeGravity;
+ currentState.badgeGravity = badgeGravity;
+ }
+
+ @Dimension(unit = Dimension.PX)
+ int getHorizontalOffsetWithoutText() {
+ return currentState.horizontalOffsetWithoutText;
+ }
+
+ void setHorizontalOffsetWithoutText(@Dimension(unit = Dimension.PX) int offset) {
+ overridingState.horizontalOffsetWithoutText = offset;
+ currentState.horizontalOffsetWithoutText = offset;
+ }
+
+ @Dimension(unit = Dimension.PX)
+ int getVerticalOffsetWithoutText() {
+ return currentState.verticalOffsetWithoutText;
+ }
+
+ void setVerticalOffsetWithoutText(@Dimension(unit = Dimension.PX) int offset) {
+ overridingState.verticalOffsetWithoutText = offset;
+ currentState.verticalOffsetWithoutText = offset;
+ }
+
+ @Dimension(unit = Dimension.PX)
+ int getHorizontalOffsetWithText() {
+ return currentState.horizontalOffsetWithText;
+ }
+
+ void setHorizontalOffsetWithText(@Dimension(unit = Dimension.PX) int offset) {
+ overridingState.horizontalOffsetWithText = offset;
+ currentState.horizontalOffsetWithText = offset;
+ }
+
+ @Dimension(unit = Dimension.PX)
+ int getVerticalOffsetWithText() {
+ return currentState.verticalOffsetWithText;
+ }
+
+ void setVerticalOffsetWithText(@Dimension(unit = Dimension.PX) int offset) {
+ overridingState.verticalOffsetWithText = offset;
+ currentState.verticalOffsetWithText = offset;
+ }
+
+ @Dimension(unit = Dimension.PX)
+ int getAdditionalHorizontalOffset() {
+ return currentState.additionalHorizontalOffset;
+ }
+
+ void setAdditionalHorizontalOffset(@Dimension(unit = Dimension.PX) int offset) {
+ overridingState.additionalHorizontalOffset = offset;
+ currentState.additionalHorizontalOffset = offset;
+ }
+
+ @Dimension(unit = Dimension.PX)
+ int getAdditionalVerticalOffset() {
+ return currentState.additionalVerticalOffset;
+ }
+
+ void setAdditionalVerticalOffset(@Dimension(unit = Dimension.PX) int offset) {
+ overridingState.additionalVerticalOffset = offset;
+ currentState.additionalVerticalOffset = offset;
+ }
+
+ CharSequence getContentDescriptionNumberless() {
+ return currentState.contentDescriptionNumberless;
+ }
+
+ void setContentDescriptionNumberless(CharSequence contentDescriptionNumberless) {
+ overridingState.contentDescriptionNumberless = contentDescriptionNumberless;
+ currentState.contentDescriptionNumberless = contentDescriptionNumberless;
+ }
+
+ @PluralsRes
+ int getContentDescriptionQuantityStrings() {
+ return currentState.contentDescriptionQuantityStrings;
+ }
+
+ void setContentDescriptionQuantityStringsResource(@PluralsRes int stringsResource) {
+ overridingState.contentDescriptionQuantityStrings = stringsResource;
+ currentState.contentDescriptionQuantityStrings = stringsResource;
+ }
+
+ @StringRes
+ int getContentDescriptionExceedsMaxBadgeNumberStringResource() {
+ return currentState.contentDescriptionExceedsMaxBadgeNumberRes;
+ }
+
+ void setContentDescriptionExceedsMaxBadgeNumberStringResource(@StringRes int stringsResource) {
+ overridingState.contentDescriptionExceedsMaxBadgeNumberRes = stringsResource;
+ currentState.contentDescriptionExceedsMaxBadgeNumberRes = stringsResource;
+ }
+
+ Locale getNumberLocale() {
+ return currentState.numberLocale;
+ }
+
+ void setNumberLocale(Locale locale) {
+ overridingState.numberLocale = locale;
+ currentState.numberLocale = locale;
+ }
+
+ private static int readColorFromAttributes(
+ Context context, @NonNull TypedArray a, @StyleableRes int index) {
+ return MaterialResources.getColorStateList(context, a, index).getDefaultColor();
+ }
+
+ /**
+ * Internal {@link Parcelable} state used to represent, save, and restore {@link BadgeDrawable}
+ * states.
+ */
+ public static final class State implements Parcelable {
+ /** Value of -1 denotes a numberless badge. */
+ private static final int BADGE_NUMBER_NONE = -1;
+
+ /** Value of -2 denotes a not-set state. */
+ private static final int NOT_SET = -2;
+
+ @XmlRes private int badgeResId;
+
+ @ColorInt private Integer backgroundColor;
+ @ColorInt private Integer badgeTextColor;
+ private int alpha = 255;
+ private int number = NOT_SET;
+ private int maxCharacterCount = NOT_SET;
+ private Locale numberLocale;
+
+ @Nullable private CharSequence contentDescriptionNumberless;
+ @PluralsRes private int contentDescriptionQuantityStrings;
+ @StringRes private int contentDescriptionExceedsMaxBadgeNumberRes;
+
+ @BadgeGravity private Integer badgeGravity;
+ private Boolean isVisible = true;
+
+ @Dimension(unit = Dimension.PX)
+ private Integer horizontalOffsetWithoutText;
+
+ @Dimension(unit = Dimension.PX)
+ private Integer verticalOffsetWithoutText;
+
+ @Dimension(unit = Dimension.PX)
+ private Integer horizontalOffsetWithText;
+
+ @Dimension(unit = Dimension.PX)
+ private Integer verticalOffsetWithText;
+
+ @Dimension(unit = Dimension.PX)
+ private Integer additionalHorizontalOffset;
+
+ @Dimension(unit = Dimension.PX)
+ private Integer additionalVerticalOffset;
+
+ public State() {}
+
+ State(@NonNull Parcel in) {
+ badgeResId = in.readInt();
+ backgroundColor = (Integer) in.readSerializable();
+ badgeTextColor = (Integer) in.readSerializable();
+ alpha = in.readInt();
+ number = in.readInt();
+ maxCharacterCount = in.readInt();
+ contentDescriptionNumberless = in.readString();
+ contentDescriptionQuantityStrings = in.readInt();
+ badgeGravity = (Integer) in.readSerializable();
+ horizontalOffsetWithoutText = (Integer) in.readSerializable();
+ verticalOffsetWithoutText = (Integer) in.readSerializable();
+ horizontalOffsetWithText = (Integer) in.readSerializable();
+ verticalOffsetWithText = (Integer) in.readSerializable();
+ additionalHorizontalOffset = (Integer) in.readSerializable();
+ additionalVerticalOffset = (Integer) in.readSerializable();
+ isVisible = (Boolean) in.readSerializable();
+ numberLocale = (Locale) in.readSerializable();
+ }
+
+ public static final Creator CREATOR =
+ new Creator() {
+ @NonNull
+ @Override
+ public BadgeState.State createFromParcel(@NonNull Parcel in) {
+ return new State(in);
+ }
+
+ @NonNull
+ @Override
+ public State[] newArray(int size) {
+ return new State[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(badgeResId);
+ dest.writeSerializable(backgroundColor);
+ dest.writeSerializable(badgeTextColor);
+ dest.writeInt(alpha);
+ dest.writeInt(number);
+ dest.writeInt(maxCharacterCount);
+ dest.writeString(
+ contentDescriptionNumberless == null ? null : contentDescriptionNumberless.toString());
+ dest.writeInt(contentDescriptionQuantityStrings);
+ dest.writeSerializable(badgeGravity);
+ dest.writeSerializable(horizontalOffsetWithoutText);
+ dest.writeSerializable(verticalOffsetWithoutText);
+ dest.writeSerializable(horizontalOffsetWithText);
+ dest.writeSerializable(verticalOffsetWithText);
+ dest.writeSerializable(additionalHorizontalOffset);
+ dest.writeSerializable(additionalVerticalOffset);
+ dest.writeSerializable(isVisible);
+ dest.writeSerializable(numberLocale);
+ }
+ }
+}
diff --git a/lib/java/com/google/android/material/badge/BadgeUtils.java b/lib/java/com/google/android/material/badge/BadgeUtils.java
index 9953f2bee40..bcdf89dcf9b 100644
--- a/lib/java/com/google/android/material/badge/BadgeUtils.java
+++ b/lib/java/com/google/android/material/badge/BadgeUtils.java
@@ -33,7 +33,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
-import com.google.android.material.badge.BadgeDrawable.SavedState;
import com.google.android.material.internal.ParcelableSparseArray;
import com.google.android.material.internal.ToolbarUtils;
@@ -228,12 +227,12 @@ public static ParcelableSparseArray createParcelableBadgeStates(
}
/**
- * Given a map of int keys to {@link BadgeDrawable.SavedState SavedStates}, creates a parcelable
+ * Given a map of int keys to {@link BadgeState.State SavedStates}, creates a parcelable
* map of int keys to {@link BadgeDrawable BadgeDrawbles}. Useful for state restoration.
*
* @param context Current context
* @param badgeStates A parcelable {@link SparseArray} that contains a map of int keys (e.g.
- * menuItemId) to {@link BadgeDrawable.SavedState states}.
+ * menuItemId) to {@link BadgeState.State states}.
* @return A {@link SparseArray} that contains a map of int keys (e.g. menuItemId) to {@code
* BadgeDrawable BadgeDrawbles}.
*/
@@ -243,7 +242,7 @@ public static SparseArray createBadgeDrawablesFromSavedStates(
SparseArray badgeDrawables = new SparseArray<>(badgeStates.size());
for (int i = 0; i < badgeStates.size(); i++) {
int key = badgeStates.keyAt(i);
- BadgeDrawable.SavedState savedState = (SavedState) badgeStates.valueAt(i);
+ BadgeState.State savedState = (BadgeState.State) badgeStates.valueAt(i);
if (savedState == null) {
throw new IllegalArgumentException("BadgeDrawable's savedState cannot be null");
}
diff --git a/lib/java/com/google/android/material/navigation/NavigationBarMenuView.java b/lib/java/com/google/android/material/navigation/NavigationBarMenuView.java
index d20d51bdd67..497d6836c06 100644
--- a/lib/java/com/google/android/material/navigation/NavigationBarMenuView.java
+++ b/lib/java/com/google/android/material/navigation/NavigationBarMenuView.java
@@ -91,7 +91,8 @@ public abstract class NavigationBarMenuView extends ViewGroup implements MenuVie
@StyleRes private int itemTextAppearanceActive;
private Drawable itemBackground;
private int itemBackgroundRes;
- @NonNull private SparseArray badgeDrawables = new SparseArray<>(ITEM_POOL_SIZE);
+ @NonNull private final SparseArray badgeDrawables =
+ new SparseArray<>(ITEM_POOL_SIZE);
private int itemPaddingTop = NO_PADDING;
private int itemPaddingBottom = NO_PADDING;
private boolean itemActiveIndicatorEnabled;
@@ -786,8 +787,14 @@ SparseArray getBadgeDrawables() {
return badgeDrawables;
}
- void setBadgeDrawables(SparseArray badgeDrawables) {
- this.badgeDrawables = badgeDrawables;
+ void restoreBadgeDrawables(SparseArray badgeDrawables) {
+ for (int i = 0; i < badgeDrawables.size(); i++) {
+ int key = badgeDrawables.keyAt(i);
+ if (this.badgeDrawables.indexOfKey(key) < 0) {
+ // badge doesn't exist yet, restore it
+ this.badgeDrawables.append(key, badgeDrawables.get(key));
+ }
+ }
if (buttons != null) {
for (NavigationBarItemView itemView : buttons) {
itemView.setBadge(badgeDrawables.get(itemView.getId()));
diff --git a/lib/java/com/google/android/material/navigation/NavigationBarPresenter.java b/lib/java/com/google/android/material/navigation/NavigationBarPresenter.java
index 02c32bb5fb6..4def96dc338 100644
--- a/lib/java/com/google/android/material/navigation/NavigationBarPresenter.java
+++ b/lib/java/com/google/android/material/navigation/NavigationBarPresenter.java
@@ -127,7 +127,7 @@ public void onRestoreInstanceState(@NonNull Parcelable state) {
SparseArray badgeDrawables =
BadgeUtils.createBadgeDrawablesFromSavedStates(
menuView.getContext(), ((SavedState) state).badgeSavedStates);
- menuView.setBadgeDrawables(badgeDrawables);
+ menuView.restoreBadgeDrawables(badgeDrawables);
}
}
diff --git a/lib/javatests/com/google/android/material/badge/BadgeDrawableTest.java b/lib/javatests/com/google/android/material/badge/BadgeDrawableTest.java
index 322ede4e35c..942f986a5af 100644
--- a/lib/javatests/com/google/android/material/badge/BadgeDrawableTest.java
+++ b/lib/javatests/com/google/android/material/badge/BadgeDrawableTest.java
@@ -29,7 +29,6 @@
import androidx.annotation.XmlRes;
import androidx.core.content.res.ResourcesCompat;
import androidx.test.core.app.ApplicationProvider;
-import com.google.android.material.badge.BadgeDrawable.SavedState;
import com.google.android.material.drawable.DrawableUtils;
import java.util.Locale;
import org.junit.Before;
@@ -65,7 +64,7 @@ public void testSavedState() {
int testBadgeTextColor =
ResourcesCompat.getColor(context.getResources(), android.R.color.white, context.getTheme());
BadgeDrawable badgeDrawable = BadgeDrawable.create(context);
- SavedState drawableState = badgeDrawable.getSavedState();
+ BadgeState.State drawableState = badgeDrawable.getSavedState();
badgeDrawable.setNumber(TEST_BADGE_NUMBER);
badgeDrawable.setBadgeGravity(BadgeDrawable.TOP_START);
@@ -92,7 +91,7 @@ public void testSavedState() {
drawableState.writeToParcel(parcel, drawableState.describeContents());
parcel.setDataPosition(0);
- SavedState createdFromParcel = SavedState.CREATOR.createFromParcel(parcel);
+ BadgeState.State createdFromParcel = BadgeState.State.CREATOR.createFromParcel(parcel);
BadgeDrawable restoredBadgeDrawable =
BadgeDrawable.createFromSavedState(context, createdFromParcel);
assertThat(restoredBadgeDrawable.getNumber()).isEqualTo(TEST_BADGE_NUMBER);