/* * Copyright (C) 2017 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.button; import com.google.android.material.R; import static android.view.Gravity.CENTER_HORIZONTAL; import static android.view.Gravity.END; import static android.view.Gravity.LEFT; import static android.view.Gravity.RIGHT; import static android.view.Gravity.START; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap; import static java.lang.Math.ceil; import static java.lang.Math.max; import static java.lang.Math.min; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuff.Mode; 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 androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.widget.AppCompatButton; import android.text.Layout.Alignment; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.Button; import android.widget.Checkable; import android.widget.CompoundButton; import android.widget.LinearLayout; import android.widget.LinearLayout.LayoutParams; import androidx.annotation.AttrRes; import androidx.annotation.ColorInt; import androidx.annotation.ColorRes; import androidx.annotation.DimenRes; import androidx.annotation.Dimension; import androidx.annotation.DrawableRes; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.Px; import androidx.annotation.RestrictTo; import androidx.core.graphics.drawable.DrawableCompat; import androidx.customview.view.AbsSavedState; import androidx.dynamicanimation.animation.FloatPropertyCompat; import androidx.dynamicanimation.animation.SpringAnimation; import androidx.dynamicanimation.animation.SpringForce; import com.google.android.material.internal.ThemeEnforcement; import com.google.android.material.internal.ViewUtils; import com.google.android.material.motion.MotionUtils; import androidx.resourceinspection.annotation.Attribute; import com.google.android.material.resources.MaterialResources; import com.google.android.material.shape.MaterialShapeDrawable; import com.google.android.material.shape.MaterialShapeUtils; import com.google.android.material.shape.ShapeAppearance; import com.google.android.material.shape.ShapeAppearanceModel; import com.google.android.material.shape.Shapeable; import com.google.android.material.shape.StateListShapeAppearanceModel; import com.google.android.material.shape.StateListSizeChange; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.LinkedHashSet; /** * A convenience class for creating a new Material button. * *

This class supplies updated Material styles for the button in the constructor. The widget will * display the correct default Material styles without the use of the style flag. * *

All attributes from {@code com.google.android.material.R.styleable#MaterialButton} are * supported. Do not use the {@code android:background} attribute. MaterialButton manages its own * background drawable, and setting a new background means {@link MaterialButton} can no longer * guarantee that the new attributes it introduces will function properly. If the default background * is changed, {@link MaterialButton} cannot guarantee well-defined behavior. * *

For filled buttons, this class uses your theme's {@code ?attr/colorPrimary} for the background * tint color and {@code ?attr/colorOnPrimary} for the text color. For unfilled buttons, this class * uses {@code ?attr/colorPrimary} for the text color and transparent for the background tint. * *

Add icons to the start, center, or end of this button using the {@code app:icon}, {@code * app:iconPadding}, {@code app:iconTint}, {@code app:iconTintMode} and {@code app:iconGravity} * attributes. * *

If a start-aligned icon is added to this button, please use a style like one of the ".Icon" * styles specified in the default MaterialButton styles. The ".Icon" styles adjust padding slightly * to achieve a better visual balance. This style should only be used with a start-aligned icon * button. If your icon is end-aligned, you cannot use a ".Icon" style and must instead manually * adjust your padding such that the visual adjustment is mirrored. * *

Specify background tint using the {@code app:backgroundTint} and {@code * app:backgroundTintMode} attributes, which accepts either a color or a color state list. * *

Ripple color / press state color can be specified using the {@code app:rippleColor} attribute. * Ripple opacity will be determined by the Android framework when available. Otherwise, this color * will be overlaid on the button at a 50% opacity when button is pressed. * *

Set the stroke color using the {@code app:strokeColor} attribute, which accepts either a color * or a color state list. Stroke width can be set using the {@code app:strokeWidth} attribute. * *

Specify the radius of all four corners of the button using the {@code app:cornerRadius} * attribute. * *

For more information, see the component * developer guidance and design * guidelines. */ public class MaterialButton extends AppCompatButton implements Checkable, Shapeable { /** Interface definition for a callback to be invoked when the button checked state changes. */ public interface OnCheckedChangeListener { /** * Called when the checked state of a MaterialButton has changed. * * @param button The MaterialButton whose state has changed. * @param isChecked The new checked state of MaterialButton. */ void onCheckedChanged(MaterialButton button, boolean isChecked); } /** Interface to listen for press state changes on this button. Internal use only. */ interface OnPressedChangeListener { void onPressedChanged(MaterialButton button, boolean isPressed); } private static final int[] CHECKABLE_STATE_SET = {android.R.attr.state_checkable}; private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked}; /** * Gravity used to position the icon at the start of the view. * * @see #setIconGravity(int) * @see #getIconGravity() */ public static final int ICON_GRAVITY_START = 0x1; /** * Gravity used to position the icon in the center of the view at the start of the text * * @see #setIconGravity(int) * @see #getIconGravity() */ public static final int ICON_GRAVITY_TEXT_START = 0x2; /** * Gravity used to position the icon at the end of the view. * * @see #setIconGravity(int) * @see #getIconGravity() */ public static final int ICON_GRAVITY_END = 0x3; /** * Gravity used to position the icon in the center of the view at the end of the text * * @see #setIconGravity(int) * @see #getIconGravity() */ public static final int ICON_GRAVITY_TEXT_END = 0x4; /** * Gravity used to position the icon at the top of the view. * * @see #setIconGravity(int) * @see #getIconGravity() */ public static final int ICON_GRAVITY_TOP = 0x10; /** * Gravity used to position the icon in the center of the view at the top of the text * * @see #setIconGravity(int) * @see #getIconGravity() */ public static final int ICON_GRAVITY_TEXT_TOP = 0x20; /** Positions the icon can be set to. */ @IntDef({ ICON_GRAVITY_START, ICON_GRAVITY_TEXT_START, ICON_GRAVITY_END, ICON_GRAVITY_TEXT_END, ICON_GRAVITY_TOP, ICON_GRAVITY_TEXT_TOP }) @Retention(RetentionPolicy.SOURCE) public @interface IconGravity {} enum WidthChangeDirection { NONE, START, END, BOTH } private static final String LOG_TAG = "MaterialButton"; private static final int DEF_STYLE_RES = R.style.Widget_MaterialComponents_Button; @AttrRes private static final int MATERIAL_SIZE_OVERLAY_ATTR = R.attr.materialSizeOverlay; private static final float OPTICAL_CENTER_RATIO = 0.11f; private static final int UNSET = -1; @NonNull private final MaterialButtonHelper materialButtonHelper; @NonNull private final LinkedHashSet onCheckedChangeListeners = new LinkedHashSet<>(); @Nullable private OnPressedChangeListener onPressedChangeListenerInternal; @Nullable private Mode iconTintMode; @Nullable private ColorStateList iconTint; @Nullable private Drawable icon; @Nullable private String accessibilityClassName; @Px private int iconSize; @Px private int iconLeft; @Px private int iconTop; @Px private int iconPadding; private boolean checked = false; private boolean broadcasting = false; @IconGravity private int iconGravity; private int orientation = UNSET; private float originalWidth = UNSET; @Px private int originalPaddingStart = UNSET; @Px private int originalPaddingEnd = UNSET; @Nullable private LayoutParams originalLayoutParams; // Fields for optical center. private boolean opticalCenterEnabled; private int opticalCenterShift; private boolean isInHorizontalButtonGroup; // Fields for size morphing. @Px int allowedWidthDecrease = UNSET; @Nullable StateListSizeChange sizeChange; @Px int widthChangeMax; private WidthChangeDirection widthChangeDirection = WidthChangeDirection.BOTH; private float displayedWidthIncrease; private float displayedWidthDecrease; @Nullable private SpringAnimation widthIncreaseSpringAnimation; public MaterialButton(@NonNull Context context) { this(context, null /* attrs */); } public MaterialButton(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, R.attr.materialButtonStyle); } public MaterialButton(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super( wrap(context, attrs, defStyleAttr, DEF_STYLE_RES, new int[] {MATERIAL_SIZE_OVERLAY_ATTR}), attrs, defStyleAttr); // Ensure we are using the correctly themed context rather than the context that was passed in. context = getContext(); TypedArray attributes = ThemeEnforcement.obtainStyledAttributes( context, attrs, R.styleable.MaterialButton, defStyleAttr, DEF_STYLE_RES); iconPadding = attributes.getDimensionPixelSize(R.styleable.MaterialButton_iconPadding, 0); iconTintMode = ViewUtils.parseTintMode( attributes.getInt(R.styleable.MaterialButton_iconTintMode, -1), Mode.SRC_IN); iconTint = MaterialResources.getColorStateList( getContext(), attributes, R.styleable.MaterialButton_iconTint); icon = MaterialResources.getDrawable(getContext(), attributes, R.styleable.MaterialButton_icon); iconGravity = attributes.getInteger(R.styleable.MaterialButton_iconGravity, ICON_GRAVITY_START); iconSize = attributes.getDimensionPixelSize(R.styleable.MaterialButton_iconSize, 0); StateListShapeAppearanceModel stateListShapeAppearanceModel = StateListShapeAppearanceModel.create( context, attributes, R.styleable.MaterialButton_shapeAppearance); ShapeAppearance shapeAppearance = stateListShapeAppearanceModel != null ? stateListShapeAppearanceModel : ShapeAppearanceModel.builder(context, attrs, defStyleAttr, DEF_STYLE_RES).build(); boolean opticalCenterEnabled = attributes.getBoolean(R.styleable.MaterialButton_opticalCenterEnabled, false); // Loads and sets background drawable attributes materialButtonHelper = new MaterialButtonHelper(this, shapeAppearance); materialButtonHelper.loadFromAttributes(attributes); // Sets the checked state after the MaterialButtonHelper is initialized. setCheckedInternal(attributes.getBoolean(R.styleable.MaterialButton_android_checked, false)); if (shapeAppearance instanceof StateListShapeAppearanceModel) { materialButtonHelper.setCornerSpringForce(createSpringForce()); } setOpticalCenterEnabled(opticalCenterEnabled); attributes.recycle(); setCompoundDrawablePadding(iconPadding); updateIcon(/* needsIconReset= */ icon != null); } private void initializeSizeAnimation() { widthIncreaseSpringAnimation = new SpringAnimation(this, WIDTH_INCREASE); widthIncreaseSpringAnimation.setSpring(createSpringForce()); } private SpringForce createSpringForce() { return MotionUtils.resolveThemeSpringForce( getContext(), R.attr.motionSpringFastSpatial, R.style.Motion_Material3_Spring_Standard_Fast_Spatial); } @NonNull @SuppressLint("KotlinPropertyAccess") String getA11yClassName() { if (!TextUtils.isEmpty(accessibilityClassName)) { return accessibilityClassName; } // Use the platform widget classes so Talkback can recognize this as a button. return (isCheckable() ? CompoundButton.class : Button.class).getName(); } @RestrictTo(LIBRARY_GROUP) public void setA11yClassName(@Nullable String className) { accessibilityClassName = className; } @Override public void onInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.setClassName(getA11yClassName()); info.setCheckable(isCheckable()); info.setChecked(isChecked()); info.setClickable(isClickable()); } @Override public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent accessibilityEvent) { super.onInitializeAccessibilityEvent(accessibilityEvent); accessibilityEvent.setClassName(getA11yClassName()); accessibilityEvent.setChecked(isChecked()); } @NonNull @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState savedState = new SavedState(superState); savedState.checked = checked; return savedState; } @Override public void onRestoreInstanceState(@Nullable Parcelable state) { if (!(state instanceof SavedState)) { super.onRestoreInstanceState(state); return; } SavedState savedState = (SavedState) state; super.onRestoreInstanceState(savedState.getSuperState()); setChecked(savedState.checked); } /** * This should be accessed via {@link * androidx.core.view.ViewCompat#setBackgroundTintList(android.view.View, ColorStateList)} * * @hide */ @RestrictTo(LIBRARY_GROUP) @Override public void setSupportBackgroundTintList(@Nullable ColorStateList tint) { if (isUsingOriginalBackground()) { materialButtonHelper.setSupportBackgroundTintList(tint); } else { // If default MaterialButton background has been overwritten, we will let AppCompatButton // handle the tinting super.setSupportBackgroundTintList(tint); } } /** * This should be accessed via {@link android.view.View#getBackgroundTintList()} * * @hide */ @RestrictTo(LIBRARY_GROUP) @Override @Nullable public ColorStateList getSupportBackgroundTintList() { if (isUsingOriginalBackground()) { return materialButtonHelper.getSupportBackgroundTintList(); } else { // If default MaterialButton background has been overwritten, we will let AppCompatButton // handle the tinting // return null; return super.getSupportBackgroundTintList(); } } /** * This should be accessed via {@link * androidx.core.view.ViewCompat#setBackgroundTintMode(android.view.View, PorterDuff.Mode)} * * @hide */ @RestrictTo(LIBRARY_GROUP) @Override public void setSupportBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) { if (isUsingOriginalBackground()) { materialButtonHelper.setSupportBackgroundTintMode(tintMode); } else { // If default MaterialButton background has been overwritten, we will let AppCompatButton // handle the tint Mode super.setSupportBackgroundTintMode(tintMode); } } /** * This should be accessed via {@link android.view.View#getBackgroundTintMode()} * * @hide */ @RestrictTo(LIBRARY_GROUP) @Override @Nullable public PorterDuff.Mode getSupportBackgroundTintMode() { if (isUsingOriginalBackground()) { return materialButtonHelper.getSupportBackgroundTintMode(); } else { // If default MaterialButton background has been overwritten, we will let AppCompatButton // handle the tint mode return super.getSupportBackgroundTintMode(); } } @Override public void setBackgroundTintList(@Nullable ColorStateList tintList) { setSupportBackgroundTintList(tintList); } @Nullable @Override public ColorStateList getBackgroundTintList() { return getSupportBackgroundTintList(); } @Override public void setBackgroundTintMode(@Nullable Mode tintMode) { setSupportBackgroundTintMode(tintMode); } @Nullable @Override public Mode getBackgroundTintMode() { return getSupportBackgroundTintMode(); } @Override public void setBackgroundColor(@ColorInt int color) { if (isUsingOriginalBackground()) { materialButtonHelper.setBackgroundColor(color); } else { // If default MaterialButton background has been overwritten, we will let View handle // setting the background color. super.setBackgroundColor(color); } } @Override public void setBackground(@NonNull Drawable background) { setBackgroundDrawable(background); } @Override public void setBackgroundResource(@DrawableRes int backgroundResourceId) { Drawable background = null; if (backgroundResourceId != 0) { background = AppCompatResources.getDrawable(getContext(), backgroundResourceId); } setBackgroundDrawable(background); } @Override public void setBackgroundDrawable(@NonNull Drawable background) { if (isUsingOriginalBackground()) { if (background != this.getBackground()) { Log.w( LOG_TAG, "MaterialButton manages its own background to control elevation, shape, color and" + " states. Consider using backgroundTint, shapeAppearance and other attributes" + " where available. A custom background will ignore these attributes and you" + " should consider handling interaction states such as pressed, focused and" + " disabled"); materialButtonHelper.setBackgroundOverwritten(); super.setBackgroundDrawable(background); } else { // ViewCompat.setBackgroundTintList() and setBackgroundTintMode() call setBackground() on // the view in API 21, since background state doesn't automatically update in API 21. We // capture this case here, and update our background without replacing it or re-tinting it. getBackground().setState(background.getState()); } } else { super.setBackgroundDrawable(background); } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); // Workaround for API 21 ripple bug (possibly internal in GradientDrawable) if (VERSION.SDK_INT == VERSION_CODES.LOLLIPOP && materialButtonHelper != null) { materialButtonHelper.updateMaskBounds(bottom - top, right - left); } updateIconPosition(getMeasuredWidth(), getMeasuredHeight()); int curOrientation = getResources().getConfiguration().orientation; if (orientation != curOrientation) { orientation = curOrientation; originalWidth = UNSET; } if (originalWidth == UNSET) { originalWidth = getMeasuredWidth(); // The width morph leverage the width of the layout params. However, it's not available if // layout_weight is used. We need to hardcode the width here. The original layout params will // be preserved for the correctness of distribution when buttons are added or removed into the // group programmatically. if (originalLayoutParams == null && getParent() instanceof MaterialButtonGroup && ((MaterialButtonGroup) getParent()).getButtonSizeChange() != null) { originalLayoutParams = (LayoutParams) getLayoutParams(); LayoutParams newLayoutParams = new LayoutParams(originalLayoutParams); newLayoutParams.width = (int) originalWidth; setLayoutParams(newLayoutParams); } } if (allowedWidthDecrease == UNSET) { int localIconSizeAndPadding = icon == null ? 0 : getIconPadding() + (iconSize == 0 ? icon.getIntrinsicWidth() : iconSize); allowedWidthDecrease = getMeasuredWidth() - getTextLayoutWidth() - localIconSizeAndPadding; } if (originalPaddingStart == UNSET) { originalPaddingStart = getPaddingStart(); } if (originalPaddingEnd == UNSET) { originalPaddingEnd = getPaddingEnd(); } isInHorizontalButtonGroup = isInHorizontalButtonGroup(); } void recoverOriginalLayoutParams() { if (originalLayoutParams != null) { setLayoutParams(originalLayoutParams); originalLayoutParams = null; originalWidth = UNSET; } } @Override public void setWidth(@Px int pixels) { originalWidth = UNSET; super.setWidth(pixels); } @Override protected void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { super.onTextChanged(charSequence, i, i1, i2); updateIconPosition(getMeasuredWidth(), getMeasuredHeight()); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (isUsingOriginalBackground()) { MaterialShapeUtils.setParentAbsoluteElevation( this, materialButtonHelper.getMaterialShapeDrawable()); } } @Override public void setElevation(float elevation) { super.setElevation(elevation); if (isUsingOriginalBackground()) { materialButtonHelper.getMaterialShapeDrawable().setElevation(elevation); } } @Override public void refreshDrawableState() { super.refreshDrawableState(); if (this.icon != null) { final int[] state = getDrawableState(); boolean changed = icon.setState(state); // Force the view to draw if icon state has changed. if (changed) { invalidate(); } } } @Override public void setTextAlignment(int textAlignment) { super.setTextAlignment(textAlignment); updateIconPosition(getMeasuredWidth(), getMeasuredHeight()); } /** * This method and {@link #getActualTextAlignment()} is modified from Android framework TextView's * private method getLayoutAlignment(). Please note that the logic here assumes the actual text * direction is the same as the layout direction, which is not always the case, especially when * the text mixes different languages. However, this is probably the best we can do for now, * unless we have a good way to detect the final text direction being used by TextView. */ private Alignment getGravityTextAlignment() { switch (getGravity() & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) { case CENTER_HORIZONTAL: return Alignment.ALIGN_CENTER; case END: case RIGHT: return Alignment.ALIGN_OPPOSITE; case START: case LEFT: default: return Alignment.ALIGN_NORMAL; } } /** * This method and {@link #getGravityTextAlignment()} is modified from Android framework * TextView's private method getLayoutAlignment(). Please note that the logic here assumes the * actual text direction is the same as the layout direction, which is not always the case, * especially when the text mixes different languages. However, this is probably the best we can * do for now, unless we have a good way to detect the final text direction being used by * TextView. */ private Alignment getActualTextAlignment() { switch (getTextAlignment()) { case TEXT_ALIGNMENT_GRAVITY: return getGravityTextAlignment(); case TEXT_ALIGNMENT_CENTER: return Alignment.ALIGN_CENTER; case TEXT_ALIGNMENT_TEXT_END: case TEXT_ALIGNMENT_VIEW_END: return Alignment.ALIGN_OPPOSITE; case TEXT_ALIGNMENT_TEXT_START: case TEXT_ALIGNMENT_VIEW_START: case TEXT_ALIGNMENT_INHERIT: default: return Alignment.ALIGN_NORMAL; } } private void updateIconPosition(int buttonWidth, int buttonHeight) { if (icon == null || getLayout() == null) { return; } if (isIconStart() || isIconEnd()) { iconTop = 0; Alignment textAlignment = getActualTextAlignment(); if (iconGravity == ICON_GRAVITY_START || iconGravity == ICON_GRAVITY_END || (iconGravity == ICON_GRAVITY_TEXT_START && textAlignment == Alignment.ALIGN_NORMAL) || (iconGravity == ICON_GRAVITY_TEXT_END && textAlignment == Alignment.ALIGN_OPPOSITE)) { iconLeft = 0; updateIcon(/* needsIconReset= */ false); return; } int localIconSize = iconSize == 0 ? icon.getIntrinsicWidth() : iconSize; int availableWidth = buttonWidth - getTextLayoutWidth() - getPaddingEnd() - localIconSize - iconPadding - getPaddingStart(); int newIconLeft = textAlignment == Alignment.ALIGN_CENTER ? availableWidth / 2 : availableWidth; // Only flip the bound value if either isLayoutRTL() or iconGravity is textEnd, but not both if (isLayoutRTL() != (iconGravity == ICON_GRAVITY_TEXT_END)) { newIconLeft = -newIconLeft; } if (iconLeft != newIconLeft) { iconLeft = newIconLeft; updateIcon(/* needsIconReset= */ false); } } else if (isIconTop()) { iconLeft = 0; if (iconGravity == ICON_GRAVITY_TOP) { iconTop = 0; updateIcon(/* needsIconReset= */ false); return; } int localIconSize = iconSize == 0 ? icon.getIntrinsicHeight() : iconSize; int newIconTop = max( 0, // Always put the icon on top if the content height is taller than the button. (buttonHeight - getTextHeight() - getPaddingTop() - localIconSize - iconPadding - getPaddingBottom()) / 2); if (iconTop != newIconTop) { iconTop = newIconTop; updateIcon(/* needsIconReset= */ false); } } } private int getTextLayoutWidth() { float maxWidth = 0; int lineCount = getLineCount(); for (int line = 0; line < lineCount; line++) { maxWidth = max(maxWidth, getLayout().getLineWidth(line)); } return (int) ceil(maxWidth); } private int getTextHeight() { if (getLineCount() > 1) { // If it's multi-line, return the internal text layout's height. return getLayout().getHeight(); } Paint textPaint = getPaint(); String buttonText = getText().toString(); if (getTransformationMethod() != null) { // if text is transformed, add that transformation to to ensure correct calculation // of icon padding. buttonText = getTransformationMethod().getTransformation(buttonText, this).toString(); } Rect bounds = new Rect(); textPaint.getTextBounds(buttonText, 0, buttonText.length(), bounds); return min(bounds.height(), getLayout().getHeight()); } private boolean isLayoutRTL() { return getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; } /** * Update the button's background without changing the background state in {@link * MaterialButtonHelper}. This should be used when we initially set the background drawable * created by {@link MaterialButtonHelper}. * * @param background Background to set on this button */ void setInternalBackground(Drawable background) { super.setBackgroundDrawable(background); } /** * Sets the padding between the button icon and the button text, if icon is present. * * @param iconPadding Padding between the button icon and the button text, if icon is present. * @attr ref com.google.android.material.R.styleable#MaterialButton_iconPadding * @see #getIconPadding() */ public void setIconPadding(@Px int iconPadding) { if (this.iconPadding != iconPadding) { this.iconPadding = iconPadding; setCompoundDrawablePadding(iconPadding); } } /** * Gets the padding between the button icon and the button text, if icon is present. * * @return Padding between the button icon and the button text, if icon is present. * @attr ref com.google.android.material.R.styleable#MaterialButton_iconPadding * @see #setIconPadding(int) */ @Attribute("com.google.android.material:iconPadding") @Px public int getIconPadding() { return iconPadding; } /** * Sets the width and height of the icon. Use 0 to use source Drawable size. * * @param iconSize new dimension for width and height of the icon in pixels. * @attr ref com.google.android.material.R.styleable#MaterialButton_iconSize * @see #getIconSize() */ public void setIconSize(@Px int iconSize) { if (iconSize < 0) { throw new IllegalArgumentException("iconSize cannot be less than 0"); } if (this.iconSize != iconSize) { this.iconSize = iconSize; updateIcon(/* needsIconReset= */ true); } } /** * Returns the size of the icon if it was set. * * @return Returns the size of the icon if it was set in pixels, 0 otherwise. * @attr ref com.google.android.material.R.styleable#MaterialButton_iconSize * @see #setIconSize(int) */ @Px public int getIconSize() { return iconSize; } /** * Sets the icon to show for this button. By default, this icon will be shown on the left side of * the button. * * @param icon Drawable to use for the button's icon. * @attr ref com.google.android.material.R.styleable#MaterialButton_icon * @see #setIconResource(int) * @see #getIcon() */ public void setIcon(@Nullable Drawable icon) { if (this.icon != icon) { this.icon = icon; updateIcon(/* needsIconReset= */ true); updateIconPosition(getMeasuredWidth(), getMeasuredHeight()); } } /** * Sets the icon drawable resource to show for this button. By default, this icon will be shown on * the left side of the button. * * @param iconResourceId Drawable resource ID to use for the button's icon. * @attr ref com.google.android.material.R.styleable#MaterialButton_icon * @see #setIcon(Drawable) * @see #getIcon() */ public void setIconResource(@DrawableRes int iconResourceId) { Drawable icon = null; if (iconResourceId != 0) { icon = AppCompatResources.getDrawable(getContext(), iconResourceId); } setIcon(icon); } /** * Gets the icon shown for this button, if present. * * @return Icon shown for this button, if present. * @attr ref com.google.android.material.R.styleable#MaterialButton_icon * @see #setIcon(Drawable) * @see #setIconResource(int) */ public Drawable getIcon() { return icon; } /** * Sets the tint list for the icon shown for this button. * * @param iconTint Tint list for the icon shown for this button. * @attr ref com.google.android.material.R.styleable#MaterialButton_iconTint * @see #setIconTintResource(int) * @see #getIconTint() */ public void setIconTint(@Nullable ColorStateList iconTint) { if (this.iconTint != iconTint) { this.iconTint = iconTint; updateIcon(/* needsIconReset= */ false); } } /** * Sets the tint list color resource for the icon shown for this button. * * @param iconTintResourceId Tint list color resource for the icon shown for this button. * @attr ref com.google.android.material.R.styleable#MaterialButton_iconTint * @see #setIconTint(ColorStateList) * @see #getIconTint() */ public void setIconTintResource(@ColorRes int iconTintResourceId) { setIconTint(AppCompatResources.getColorStateList(getContext(), iconTintResourceId)); } /** * Gets the tint list for the icon shown for this button. * * @return Tint list for the icon shown for this button. * @attr ref com.google.android.material.R.styleable#MaterialButton_iconTint * @see #setIconTint(ColorStateList) * @see #setIconTintResource(int) */ public ColorStateList getIconTint() { return iconTint; } /** * Sets the tint mode for the icon shown for this button. * * @param iconTintMode Tint mode for the icon shown for this button. * @attr ref com.google.android.material.R.styleable#MaterialButton_iconTintMode * @see #getIconTintMode() */ public void setIconTintMode(Mode iconTintMode) { if (this.iconTintMode != iconTintMode) { this.iconTintMode = iconTintMode; updateIcon(/* needsIconReset= */ false); } } /** * Gets the tint mode for the icon shown for this button. * * @return Tint mode for the icon shown for this button. * @attr ref com.google.android.material.R.styleable#MaterialButton_iconTintMode * @see #setIconTintMode(Mode) */ public Mode getIconTintMode() { return iconTintMode; } /** * Updates the icon, icon tint, and icon tint mode for this button. * * @param needsIconReset Whether to force the drawable to be set */ private void updateIcon(boolean needsIconReset) { if (icon != null) { icon = DrawableCompat.wrap(icon).mutate(); icon.setTintList(iconTint); if (iconTintMode != null) { icon.setTintMode(iconTintMode); } int width = iconSize != 0 ? iconSize : icon.getIntrinsicWidth(); int height = iconSize != 0 ? iconSize : icon.getIntrinsicHeight(); icon.setBounds(iconLeft, iconTop, iconLeft + width, iconTop + height); icon.setVisible(true, needsIconReset); } // Forced icon update if (needsIconReset) { resetIconDrawable(); return; } // Otherwise only update if the icon or the position has changed Drawable[] existingDrawables = getCompoundDrawablesRelative(); Drawable drawableStart = existingDrawables[0]; Drawable drawableTop = existingDrawables[1]; Drawable drawableEnd = existingDrawables[2]; boolean hasIconChanged = (isIconStart() && drawableStart != icon) || (isIconEnd() && drawableEnd != icon) || (isIconTop() && drawableTop != icon); if (hasIconChanged) { resetIconDrawable(); } } private void resetIconDrawable() { if (isIconStart()) { setCompoundDrawablesRelative(icon, null, null, null); } else if (isIconEnd()) { setCompoundDrawablesRelative(null, null, icon, null); } else if (isIconTop()) { setCompoundDrawablesRelative(null, icon, null, null); } } private boolean isIconStart() { return iconGravity == ICON_GRAVITY_START || iconGravity == ICON_GRAVITY_TEXT_START; } private boolean isIconEnd() { return iconGravity == ICON_GRAVITY_END || iconGravity == ICON_GRAVITY_TEXT_END; } private boolean isIconTop() { return iconGravity == ICON_GRAVITY_TOP || iconGravity == ICON_GRAVITY_TEXT_TOP; } /** * Sets the ripple color for this button. * * @param rippleColor Color to use for the ripple. * @attr ref com.google.android.material.R.styleable#MaterialButton_rippleColor * @see #setRippleColorResource(int) * @see #getRippleColor() */ public void setRippleColor(@Nullable ColorStateList rippleColor) { if (isUsingOriginalBackground()) { materialButtonHelper.setRippleColor(rippleColor); } } /** * Sets the ripple color resource for this button. * * @param rippleColorResourceId Color resource to use for the ripple. * @attr ref com.google.android.material.R.styleable#MaterialButton_rippleColor * @see #setRippleColor(ColorStateList) * @see #getRippleColor() */ public void setRippleColorResource(@ColorRes int rippleColorResourceId) { if (isUsingOriginalBackground()) { setRippleColor(AppCompatResources.getColorStateList(getContext(), rippleColorResourceId)); } } /** * Gets the ripple color for this button. * * @return The color used for the ripple. * @attr ref com.google.android.material.R.styleable#MaterialButton_rippleColor * @see #setRippleColor(ColorStateList) * @see #setRippleColorResource(int) */ @Nullable public ColorStateList getRippleColor() { return isUsingOriginalBackground() ? materialButtonHelper.getRippleColor() : null; } /** * Sets the stroke color for this button. Both stroke color and stroke width must be set for a * stroke to be drawn. * * @param strokeColor Color to use for the stroke. * @attr ref com.google.android.material.R.styleable#MaterialButton_strokeColor * @see #setStrokeColorResource(int) * @see #getStrokeColor() */ public void setStrokeColor(@Nullable ColorStateList strokeColor) { if (isUsingOriginalBackground()) { materialButtonHelper.setStrokeColor(strokeColor); } } /** * Sets the stroke color resource for this button. Both stroke color and stroke width must be set * for a stroke to be drawn. * * @param strokeColorResourceId Color resource to use for the stroke. * @attr ref com.google.android.material.R.styleable#MaterialButton_strokeColor * @see #setStrokeColor(ColorStateList) * @see #getStrokeColor() */ public void setStrokeColorResource(@ColorRes int strokeColorResourceId) { if (isUsingOriginalBackground()) { setStrokeColor(AppCompatResources.getColorStateList(getContext(), strokeColorResourceId)); } } /** * Gets the stroke color for this button. * * @return The color used for the stroke. * @attr ref com.google.android.material.R.styleable#MaterialButton_strokeColor * @see #setStrokeColor(ColorStateList) * @see #setStrokeColorResource(int) */ public ColorStateList getStrokeColor() { return isUsingOriginalBackground() ? materialButtonHelper.getStrokeColor() : null; } /** * Sets the stroke width for this button. Both stroke color and stroke width must be set for a * stroke to be drawn. * * @param strokeWidth Stroke width for this button. * @attr ref com.google.android.material.R.styleable#MaterialButton_strokeWidth * @see #setStrokeWidthResource(int) * @see #getStrokeWidth() */ public void setStrokeWidth(@Px int strokeWidth) { if (isUsingOriginalBackground()) { materialButtonHelper.setStrokeWidth(strokeWidth); } } /** * Sets the stroke width dimension resource for this button. Both stroke color and stroke width * must be set for a stroke to be drawn. * * @param strokeWidthResourceId Stroke width dimension resource for this button. * @attr ref com.google.android.material.R.styleable#MaterialButton_strokeWidth * @see #setStrokeWidth(int) * @see #getStrokeWidth() */ public void setStrokeWidthResource(@DimenRes int strokeWidthResourceId) { if (isUsingOriginalBackground()) { setStrokeWidth(getResources().getDimensionPixelSize(strokeWidthResourceId)); } } /** * Gets the stroke width for this button. * * @return Stroke width for this button. * @attr ref com.google.android.material.R.styleable#MaterialButton_strokeWidth * @see #setStrokeWidth(int) * @see #setStrokeWidthResource(int) */ @Px public int getStrokeWidth() { return isUsingOriginalBackground() ? materialButtonHelper.getStrokeWidth() : 0; } /** * Sets the corner radius for this button. * * @param cornerRadius Corner radius for this button. * @attr ref com.google.android.material.R.styleable#MaterialButton_cornerRadius * @see #setCornerRadiusResource(int) * @see #getCornerRadius() */ public void setCornerRadius(@Px int cornerRadius) { if (isUsingOriginalBackground()) { materialButtonHelper.setCornerRadius(cornerRadius); } } /** * Sets the corner radius dimension resource for this button. * * @param cornerRadiusResourceId Corner radius dimension resource for this button. * @attr ref com.google.android.material.R.styleable#MaterialButton_cornerRadius * @see #setCornerRadius(int) * @see #getCornerRadius() */ public void setCornerRadiusResource(@DimenRes int cornerRadiusResourceId) { if (isUsingOriginalBackground()) { setCornerRadius(getResources().getDimensionPixelSize(cornerRadiusResourceId)); } } /** * Gets the corner radius for this button. * * @return Corner radius for this button. * @attr ref com.google.android.material.R.styleable#MaterialButton_cornerRadius * @see #setCornerRadius(int) * @see #setCornerRadiusResource(int) */ @Px public int getCornerRadius() { return isUsingOriginalBackground() ? materialButtonHelper.getCornerRadius() : 0; } /** * Gets the icon gravity for this button * * @return Icon gravity of the button. * @attr ref com.google.android.material.R.styleable#MaterialButton_iconGravity * @see #setIconGravity(int) */ @IconGravity public int getIconGravity() { return iconGravity; } /** * Sets the icon gravity for this button * * @attr ref com.google.android.material.R.styleable#MaterialButton_iconGravity * @param iconGravity icon gravity for this button * @see #getIconGravity() */ public void setIconGravity(@IconGravity int iconGravity) { if (this.iconGravity != iconGravity) { this.iconGravity = iconGravity; updateIconPosition(getMeasuredWidth(), getMeasuredHeight()); } } /** * Sets the button bottom inset * * @attr ref com.google.android.material.R.styleable#MaterialButton_android_insetBottom * @see #getInsetBottom() */ public void setInsetBottom(@Dimension int insetBottom) { materialButtonHelper.setInsetBottom(insetBottom); } /** * Gets the bottom inset for this button * * @attr ref com.google.android.material.R.styleable#MaterialButton_android_insetBottom * @see #setInsetTop(int) */ @Dimension public int getInsetBottom() { return materialButtonHelper.getInsetBottom(); } /** * Sets the button top inset * * @attr ref com.google.android.material.R.styleable#MaterialButton_android_insetTop * @see #getInsetBottom() */ public void setInsetTop(@Dimension int insetTop) { materialButtonHelper.setInsetTop(insetTop); } /** * Gets the top inset for this button * * @attr ref com.google.android.material.R.styleable#MaterialButton_android_insetTop * @see #setInsetTop(int) */ @Dimension public int getInsetTop() { return materialButtonHelper.getInsetTop(); } @Override protected int[] onCreateDrawableState(int extraSpace) { final int[] drawableState = super.onCreateDrawableState(extraSpace + 2); if (isCheckable()) { mergeDrawableStates(drawableState, CHECKABLE_STATE_SET); } if (isChecked()) { mergeDrawableStates(drawableState, CHECKED_STATE_SET); } return drawableState; } /** * Add a listener that will be invoked when the checked state of this MaterialButton changes. See * {@link OnCheckedChangeListener}. * *

Components that add a listener should take care to remove it when finished via {@link * #removeOnCheckedChangeListener(OnCheckedChangeListener)}. * * @param listener listener to add */ public void addOnCheckedChangeListener(@NonNull OnCheckedChangeListener listener) { onCheckedChangeListeners.add(listener); } /** * Remove a listener that was previously added via {@link * #addOnCheckedChangeListener(OnCheckedChangeListener)}. * * @param listener listener to remove */ public void removeOnCheckedChangeListener(@NonNull OnCheckedChangeListener listener) { onCheckedChangeListeners.remove(listener); } /** Remove all previously added {@link OnCheckedChangeListener}s. */ public void clearOnCheckedChangeListeners() { onCheckedChangeListeners.clear(); } @Override public void setChecked(boolean checked) { setCheckedInternal(checked); } private void setCheckedInternal(boolean checked) { if (isCheckable() && this.checked != checked) { this.checked = checked; refreshDrawableState(); // Report checked state change to the parent toggle group, if there is one if (getParent() instanceof MaterialButtonToggleGroup) { ((MaterialButtonToggleGroup) getParent()).onButtonCheckedStateChanged(this, this.checked); } // Avoid infinite recursions if setChecked() is called from a listener if (broadcasting) { return; } broadcasting = true; for (OnCheckedChangeListener listener : onCheckedChangeListeners) { listener.onCheckedChanged(this, this.checked); } broadcasting = false; } } @Override public boolean isChecked() { return checked; } @Override public void toggle() { setChecked(!checked); } @Override public boolean performClick() { if (isEnabled() && materialButtonHelper.isToggleCheckedStateOnClick()) { toggle(); } return super.performClick(); } /** * Returns whether or not clicking the button will toggle the checked state. * * @see #setToggleCheckedStateOnClick(boolean) * @attr ref R.styleable#toggleCheckedStateOnClick */ public boolean isToggleCheckedStateOnClick() { return materialButtonHelper.isToggleCheckedStateOnClick(); } /** * Sets whether or not to toggle the button checked state on click. * * @param toggleCheckedStateOnClick whether or not to toggle the checked state on click. * @attr ref R.styleable#toggleCheckedStateOnClick */ public void setToggleCheckedStateOnClick(boolean toggleCheckedStateOnClick) { materialButtonHelper.setToggleCheckedStateOnClick(toggleCheckedStateOnClick); } /** * Returns whether this MaterialButton is checkable. * * @see #setCheckable(boolean) * @attr ref com.google.android.material.R.styleable#MaterialButton_android_checkable */ public boolean isCheckable() { return materialButtonHelper != null && materialButtonHelper.isCheckable(); } /** * Sets whether this MaterialButton is checkable. * * @param checkable Whether this button is checkable. * @attr ref com.google.android.material.R.styleable#MaterialButton_android_checkable */ public void setCheckable(boolean checkable) { if (isUsingOriginalBackground()) { materialButtonHelper.setCheckable(checkable); } } /** * Sets the {@link ShapeAppearanceModel} used for this {@link MaterialButton}'s original * drawables. * * @throws IllegalStateException if the MaterialButton's background has been overwritten. */ @Override public void setShapeAppearanceModel(@NonNull ShapeAppearanceModel shapeAppearanceModel) { if (isUsingOriginalBackground()) { materialButtonHelper.setShapeAppearance(shapeAppearanceModel); } else { throw new IllegalStateException( "Attempted to set ShapeAppearanceModel on a MaterialButton which has an overwritten" + " background."); } } /** * Returns the {@link ShapeAppearanceModel} used for this {@link MaterialButton}'s original * drawables. * *

This {@link ShapeAppearanceModel} can be modified to change the component's shape. * * @throws IllegalStateException if the MaterialButton's background has been overwritten. */ @NonNull @Override public ShapeAppearanceModel getShapeAppearanceModel() { if (isUsingOriginalBackground()) { return materialButtonHelper.getShapeAppearanceModel(); } else { throw new IllegalStateException( "Attempted to get ShapeAppearanceModel from a MaterialButton which has an overwritten" + " background."); } } /** * Sets the {@link ShapeAppearance} used for this {@link MaterialButton}'s original * drawables. * * @throws IllegalStateException if the MaterialButton's background has been overwritten. * @hide */ @RestrictTo(LIBRARY_GROUP) public void setShapeAppearance( @NonNull ShapeAppearance shapeAppearance) { if (isUsingOriginalBackground()) { if (materialButtonHelper.getCornerSpringForce() == null && shapeAppearance.isStateful()) { materialButtonHelper.setCornerSpringForce(createSpringForce()); } materialButtonHelper.setShapeAppearance(shapeAppearance); } else { throw new IllegalStateException( "Attempted to set ShapeAppearance on a MaterialButton which has an" + " overwritten background."); } } /** * Returns the {@link ShapeAppearance} used for this {@link MaterialButton}'s * original drawables. * *

This {@link ShapeAppearance} can be modified to change the component's shape. * * @throws IllegalStateException if the MaterialButton's background has been overwritten. * @hide */ @NonNull @RestrictTo(LIBRARY_GROUP) public ShapeAppearance getShapeAppearance() { if (isUsingOriginalBackground()) { return materialButtonHelper.getShapeAppearance(); } else { throw new IllegalStateException( "Attempted to get ShapeAppearance from a MaterialButton which has an" + " overwritten background."); } } /** * Sets the corner spring force for this {@link MaterialButton}. * * @param springForce The new {@link SpringForce} object. * @hide */ @RestrictTo(LIBRARY_GROUP) public void setCornerSpringForce(@NonNull SpringForce springForce) { materialButtonHelper.setCornerSpringForce(springForce); } /** * Returns the corner spring force for this {@link MaterialButton}. * * @hide */ @Nullable @RestrictTo(LIBRARY_GROUP) public SpringForce getCornerSpringForce() { return materialButtonHelper.getCornerSpringForce(); } /** * Register a callback to be invoked when the pressed state of this button changes. This callback * is used for internal purpose only. */ void setOnPressedChangeListenerInternal(@Nullable OnPressedChangeListener listener) { onPressedChangeListenerInternal = listener; } @Override public void setPressed(boolean pressed) { if (onPressedChangeListenerInternal != null) { onPressedChangeListenerInternal.onPressedChanged(this, pressed); } super.setPressed(pressed); maybeAnimateSize(/* skipAnimation= */ false); } private boolean isUsingOriginalBackground() { return materialButtonHelper != null && !materialButtonHelper.isBackgroundOverwritten(); } void setShouldDrawSurfaceColorStroke(boolean shouldDrawSurfaceColorStroke) { if (isUsingOriginalBackground()) { materialButtonHelper.setShouldDrawSurfaceColorStroke(shouldDrawSurfaceColorStroke); } } private void maybeAnimateSize(boolean skipAnimation) { if (sizeChange == null) { return; } if (widthIncreaseSpringAnimation == null) { initializeSizeAnimation(); } if (isInHorizontalButtonGroup) { // Animate width. int widthChange = min( calculateEffectiveWidthChangeMax(), sizeChange .getSizeChangeForState(getDrawableState()) .widthChange .getChange(getWidth())); widthIncreaseSpringAnimation.animateToFinalPosition(widthChange); if (skipAnimation) { widthIncreaseSpringAnimation.skipToEnd(); } } } /** Returns the effective width change max based on the width change direction. */ private int calculateEffectiveWidthChangeMax() { switch (widthChangeDirection) { case BOTH: return this.widthChangeMax; case START: case END: return this.widthChangeMax / 2; case NONE: } return 0; } private boolean isInHorizontalButtonGroup() { return getParent() instanceof MaterialButtonGroup && ((MaterialButtonGroup) getParent()).getOrientation() == LinearLayout.HORIZONTAL; } void setSizeChange(@NonNull StateListSizeChange sizeChange) { if (this.sizeChange != sizeChange) { this.sizeChange = sizeChange; maybeAnimateSize(/* skipAnimation= */ true); } } void setWidthChangeMax(@Px int widthChangeMax) { if (this.widthChangeMax != widthChangeMax) { this.widthChangeMax = widthChangeMax; maybeAnimateSize(/* skipAnimation= */ true); } } void setWidthChangeDirection(@NonNull WidthChangeDirection widthChangeDirection) { if (this.widthChangeDirection != widthChangeDirection) { this.widthChangeDirection = widthChangeDirection; maybeAnimateSize(/* skipAnimation= */ true); } } @Px int getAllowedWidthDecrease() { return allowedWidthDecrease; } private float getDisplayedWidthIncrease() { return displayedWidthIncrease; } private void setDisplayedWidthIncrease(float widthIncrease) { if (displayedWidthIncrease != widthIncrease) { displayedWidthIncrease = widthIncrease; updatePaddingsAndSizeForWidthAnimation(); invalidate(); // Report width changed to the parent group. if (getParent() instanceof MaterialButtonGroup) { ((MaterialButtonGroup) getParent()) .onButtonWidthChanged(this, (int) displayedWidthIncrease); } } } void setDisplayedWidthDecrease(int widthDecrease) { displayedWidthDecrease = min(widthDecrease, allowedWidthDecrease); updatePaddingsAndSizeForWidthAnimation(); invalidate(); } /** * Sets whether to enable the optical center feature. * * @param opticalCenterEnabled whether to enable optical centering. * @see #isOpticalCenterEnabled() */ public void setOpticalCenterEnabled(boolean opticalCenterEnabled) { if (this.opticalCenterEnabled != opticalCenterEnabled) { this.opticalCenterEnabled = opticalCenterEnabled; if (opticalCenterEnabled) { materialButtonHelper.setCornerSizeChangeListener( (diffX) -> { int opticalCenterShift = (int) (diffX * OPTICAL_CENTER_RATIO); if (this.opticalCenterShift != opticalCenterShift) { this.opticalCenterShift = opticalCenterShift; updatePaddingsAndSizeForWidthAnimation(); invalidate(); } }); } else { materialButtonHelper.setCornerSizeChangeListener(null); } // Perform the optical center shift calculation using a post, as the calculation depends on // the button being fully laid out. post( () -> { opticalCenterShift = getOpticalCenterShift(); updatePaddingsAndSizeForWidthAnimation(); invalidate(); }); } } /** * Returns whether the optical center feature is enabled. * * @see #setOpticalCenterEnabled(boolean) */ public boolean isOpticalCenterEnabled() { return opticalCenterEnabled; } private void updatePaddingsAndSizeForWidthAnimation() { int widthChange = (int) (displayedWidthIncrease - displayedWidthDecrease); int paddingStartChange = widthChange / 2 + opticalCenterShift; getLayoutParams().width = (int) (originalWidth + widthChange); setPaddingRelative( originalPaddingStart + paddingStartChange, getPaddingTop(), originalPaddingEnd + widthChange - paddingStartChange, getPaddingBottom()); } private int getOpticalCenterShift() { if (opticalCenterEnabled && isInHorizontalButtonGroup) { MaterialShapeDrawable materialShapeDrawable = materialButtonHelper.getMaterialShapeDrawable(); if (materialShapeDrawable != null) { return (int) (materialShapeDrawable.getCornerSizeDiffX() * OPTICAL_CENTER_RATIO); } } return 0; } // ******************* Properties ******************* private static final FloatPropertyCompat WIDTH_INCREASE = new FloatPropertyCompat("widthIncrease") { @Override public float getValue(MaterialButton button) { return button.getDisplayedWidthIncrease(); } @Override public void setValue(MaterialButton button, float value) { button.setDisplayedWidthIncrease(value); } }; static class SavedState extends AbsSavedState { boolean checked; public SavedState(Parcelable superState) { super(superState); } public SavedState(@NonNull Parcel source, ClassLoader loader) { super(source, loader); if (loader == null) { loader = getClass().getClassLoader(); } readFromParcel(source); } @Override public void writeToParcel(@NonNull Parcel out, int flags) { super.writeToParcel(out, flags); out.writeInt(checked ? 1 : 0); } private void readFromParcel(@NonNull Parcel in) { checked = in.readInt() == 1; } public static final Creator CREATOR = new ClassLoaderCreator() { @NonNull @Override public SavedState createFromParcel(@NonNull Parcel in, ClassLoader loader) { return new SavedState(in, loader); } @NonNull @Override public SavedState createFromParcel(@NonNull Parcel in) { return new SavedState(in, null); } @NonNull @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }; } }