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);