From bbbeacd64e3b1cc64e8edc6adf430aad45d03e32 Mon Sep 17 00:00:00 2001 From: conradchen Date: Thu, 6 Jan 2022 13:47:11 -0500 Subject: [PATCH] [Badge] Refactor Badge state managing logic This CL fixes a couple of badge state related issues. First, it prevents the badges created before restoring instance states from being overwritten by the old instance states. Second, it makes badges reload their default style settings everytime when its being recreated so if the environment has been changed, the default values will reflect the environment change. This CL also fixes that several attributes were not correctly saved/restored or their default values were not correctly loaded. Resolves https://github.com/material-components/material-components-android/issues/2032 PiperOrigin-RevId: 420096508 --- .../android/material/badge/BadgeDrawable.java | 462 +++++----------- .../android/material/badge/BadgeState.java | 515 ++++++++++++++++++ .../android/material/badge/BadgeUtils.java | 7 +- .../navigation/NavigationBarMenuView.java | 13 +- .../navigation/NavigationBarPresenter.java | 2 +- .../material/badge/BadgeDrawableTest.java | 5 +- 6 files changed, 661 insertions(+), 343 deletions(-) create mode 100644 lib/java/com/google/android/material/badge/BadgeState.java 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);