mirror of
https://github.com/material-components/material-components-android.git
synced 2026-02-20 08:39:55 +08:00
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
516 lines
17 KiB
Java
516 lines
17 KiB
Java
/*
|
|
* 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<State> CREATOR =
|
|
new Creator<State>() {
|
|
@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);
|
|
}
|
|
}
|
|
}
|