hunterstich 6f41625f57 [FAB] Restored public jumpDrawablesToCurrentState method
PiperOrigin-RevId: 735749567
2025-03-11 18:02:00 +00:00

1489 lines
49 KiB
Java

/*
* Copyright (C) 2015 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.floatingactionbutton;
import com.google.android.material.R;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static androidx.core.util.Preconditions.checkNotNull;
import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap;
import android.animation.Animator.AnimatorListener;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.PorterDuff.Mode;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Parcelable;
import androidx.appcompat.widget.AppCompatDrawableManager;
import androidx.appcompat.widget.AppCompatImageHelper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.AnimatorRes;
import androidx.annotation.ColorInt;
import androidx.annotation.DimenRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.IdRes;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.view.TintableBackgroundView;
import androidx.core.view.ViewCompat;
import androidx.core.widget.TintableImageSourceView;
import com.google.android.material.animation.MotionSpec;
import com.google.android.material.animation.TransformationCallback;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.expandable.ExpandableTransformationWidget;
import com.google.android.material.expandable.ExpandableWidgetHelper;
import com.google.android.material.floatingactionbutton.FloatingActionButtonImpl.InternalTransformationCallback;
import com.google.android.material.floatingactionbutton.FloatingActionButtonImpl.InternalVisibilityChangedListener;
import com.google.android.material.internal.DescendantOffsetUtils;
import com.google.android.material.internal.ThemeEnforcement;
import com.google.android.material.internal.ViewUtils;
import com.google.android.material.internal.VisibilityAwareImageButton;
import com.google.android.material.resources.MaterialResources;
import com.google.android.material.shadow.ShadowViewDelegate;
import com.google.android.material.shape.ShapeAppearanceModel;
import com.google.android.material.shape.Shapeable;
import com.google.android.material.stateful.ExtendableSavedState;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
/**
* Floating action buttons are used for a special type of promoted action. They are distinguished by
* a circled icon floating above the UI and have special motion behaviors related to morphing,
* launching, and the transferring anchor point.
*
* <p>Floating action buttons come in two sizes: the default and the mini. The size can be
* controlled with the {@code fabSize} attribute.
*
* <p>As this class descends from {@link ImageView}, you can control the icon which is displayed via
* {@link #setImageDrawable(Drawable)}.
*
* <p>The background color of this view defaults to the your theme's {@code colorSecondary}. If you
* wish to change this at runtime then you can do so via {@link
* #setBackgroundTintList(ColorStateList)}.
*
* <p>For more information, see the <a
* href="https://github.com/material-components/material-components-android/blob/master/docs/components/FloatingActionButton.md">component
* developer guidance</a> and <a
* href="https://material.io/components/floating-action-button/overview">design guidelines</a>.
*/
public class FloatingActionButton extends VisibilityAwareImageButton
implements TintableBackgroundView,
TintableImageSourceView,
ExpandableTransformationWidget,
Shapeable,
CoordinatorLayout.AttachedBehavior {
static final String ACCESSIBIILTY_FAB_ROLE =
"com.google.android.material.floatingactionbutton.FloatingActionButton";
private static final String LOG_TAG = "FloatingActionButton";
private static final String EXPANDABLE_WIDGET_HELPER_KEY = "expandableWidgetHelper";
private static final int DEF_STYLE_RES = R.style.Widget_Design_FloatingActionButton;
/** Callback to be invoked when the visibility of a FloatingActionButton changes. */
public abstract static class OnVisibilityChangedListener {
/**
* Called when a FloatingActionButton has been {@link #show(OnVisibilityChangedListener) shown}.
*
* @param fab the FloatingActionButton that was shown.
*/
public void onShown(FloatingActionButton fab) {}
/**
* Called when a FloatingActionButton has been {@link #hide(OnVisibilityChangedListener)
* hidden}.
*
* @param fab the FloatingActionButton that was hidden.
*/
public void onHidden(FloatingActionButton fab) {}
}
// These values must match those in the attrs declaration
/**
* The mini sized button, 40dp. Will always be smaller than {@link #SIZE_NORMAL}.
*
* @see #setSize(int)
*/
public static final int SIZE_MINI = 1;
/**
* The normal sized button, 56dp. Will always be larger than {@link #SIZE_MINI}.
*
* @see #setSize(int)
*/
public static final int SIZE_NORMAL = 0;
/**
* Size which will change based on the window size. For small sized windows (largest screen
* dimension < 470dp) this will select a mini sized button ({@link #SIZE_MINI}), and for larger
* sized windows it will select a normal sized button ({@link #SIZE_NORMAL}).
*
* @see #setSize(int)
*/
public static final int SIZE_AUTO = -1;
/**
* Indicates that the {@link FloatingActionButton} should not have a custom size, and instead that
* the size should be calculated based on the value set using {@link #setSize(int)} or the {@code
* fabSize} attribute. Instead of using this constant directly, you can call the {@link
* #clearCustomSize()} method.
*/
public static final int NO_CUSTOM_SIZE = 0;
/**
* The switch point for the largest screen edge where {@link #SIZE_AUTO} switches from mini to
* normal.
*/
private static final int AUTO_MINI_LARGEST_SCREEN_WIDTH = 470;
/** @hide */
@RestrictTo(LIBRARY_GROUP)
@Retention(RetentionPolicy.SOURCE)
@IntDef({SIZE_MINI, SIZE_NORMAL, SIZE_AUTO})
public @interface Size {}
@Nullable private ColorStateList backgroundTint;
@Nullable private PorterDuff.Mode backgroundTintMode;
@Nullable private ColorStateList imageTint;
@Nullable private PorterDuff.Mode imageMode;
@Nullable private ColorStateList rippleColor;
private int borderWidth;
private int size;
private int customSize;
private int imagePadding;
private int maxImageSize;
boolean compatPadding;
final Rect shadowPadding = new Rect();
private final Rect touchArea = new Rect();
@NonNull private final AppCompatImageHelper imageHelper;
@NonNull private final ExpandableWidgetHelper expandableWidgetHelper;
private FloatingActionButtonImpl impl;
public FloatingActionButton(@NonNull Context context) {
this(context, null);
}
public FloatingActionButton(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, R.attr.floatingActionButtonStyle);
}
@SuppressWarnings("nullness")
public FloatingActionButton(
@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(wrap(context, attrs, defStyleAttr, DEF_STYLE_RES), attrs, defStyleAttr);
// Ensure we are using the correctly themed context rather than the context that was passed in.
context = getContext();
TypedArray a =
ThemeEnforcement.obtainStyledAttributes(
context, attrs, R.styleable.FloatingActionButton, defStyleAttr, DEF_STYLE_RES);
backgroundTint =
MaterialResources.getColorStateList(
context, a, R.styleable.FloatingActionButton_backgroundTint);
backgroundTintMode =
ViewUtils.parseTintMode(
a.getInt(R.styleable.FloatingActionButton_backgroundTintMode, -1), null);
rippleColor =
MaterialResources.getColorStateList(
context, a, R.styleable.FloatingActionButton_rippleColor);
size = a.getInt(R.styleable.FloatingActionButton_fabSize, SIZE_AUTO);
customSize =
a.getDimensionPixelSize(R.styleable.FloatingActionButton_fabCustomSize, NO_CUSTOM_SIZE);
borderWidth = a.getDimensionPixelSize(R.styleable.FloatingActionButton_borderWidth, 0);
final float elevation = a.getDimension(R.styleable.FloatingActionButton_elevation, 0f);
final float hoveredFocusedTranslationZ =
a.getDimension(R.styleable.FloatingActionButton_hoveredFocusedTranslationZ, 0f);
final float pressedTranslationZ =
a.getDimension(R.styleable.FloatingActionButton_pressedTranslationZ, 0f);
compatPadding = a.getBoolean(R.styleable.FloatingActionButton_useCompatPadding, false);
int minTouchTargetSize =
getResources().getDimensionPixelSize(R.dimen.mtrl_fab_min_touch_target);
setMaxImageSize(a.getDimensionPixelSize(R.styleable.FloatingActionButton_maxImageSize, 0));
MotionSpec showMotionSpec =
MotionSpec.createFromAttribute(context, a, R.styleable.FloatingActionButton_showMotionSpec);
MotionSpec hideMotionSpec =
MotionSpec.createFromAttribute(context, a, R.styleable.FloatingActionButton_hideMotionSpec);
ShapeAppearanceModel shapeAppearance =
ShapeAppearanceModel.builder(
context, attrs, defStyleAttr, DEF_STYLE_RES, ShapeAppearanceModel.PILL)
.build();
boolean ensureMinTouchTargetSize =
a.getBoolean(R.styleable.FloatingActionButton_ensureMinTouchTargetSize, false);
setEnabled(a.getBoolean(R.styleable.FloatingActionButton_android_enabled, true));
a.recycle();
imageHelper = new AppCompatImageHelper(this);
imageHelper.loadFromAttributes(attrs, defStyleAttr);
expandableWidgetHelper = new ExpandableWidgetHelper(this);
getImpl().setShapeAppearance(shapeAppearance);
getImpl()
.initializeBackgroundDrawable(backgroundTint, backgroundTintMode, rippleColor, borderWidth);
getImpl().setMinTouchTargetSize(minTouchTargetSize);
getImpl().setElevation(elevation);
getImpl().setHoveredFocusedTranslationZ(hoveredFocusedTranslationZ);
getImpl().setPressedTranslationZ(pressedTranslationZ);
getImpl().setShowMotionSpec(showMotionSpec);
getImpl().setHideMotionSpec(hideMotionSpec);
getImpl().setEnsureMinTouchTargetSize(ensureMinTouchTargetSize);
setScaleType(ScaleType.MATRIX);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int preferredSize = getSizeDimension();
imagePadding = (preferredSize - maxImageSize) / 2;
getImpl().updatePadding();
final int w = View.resolveSize(preferredSize, widthMeasureSpec);
final int h = View.resolveSize(preferredSize, heightMeasureSpec);
// As we want to stay circular, we set both dimensions to be the
// smallest resolved dimension
final int d = Math.min(w, h);
// We add the shadow's padding to the measured dimension
setMeasuredDimension(
d + shadowPadding.left + shadowPadding.right,
d + shadowPadding.top + shadowPadding.bottom);
}
/**
* Returns the ripple color for this button.
*
* @return the ARGB color used for the ripple
* @see #setRippleColor(int)
* @deprecated Use {@link #getRippleColorStateList()} instead.
*/
@ColorInt
@Deprecated
public int getRippleColor() {
return rippleColor != null ? rippleColor.getDefaultColor() : 0;
}
/**
* Returns the ripple color for this button.
*
* @return the color state list used for the ripple
* @see #setRippleColor(ColorStateList)
*/
@Nullable
public ColorStateList getRippleColorStateList() {
return rippleColor;
}
/**
* Sets the ripple color for this button.
*
* <p>When running on devices with KitKat, we draw this color as a filled circle rather
* than a ripple.
*
* @param color ARGB color to use for the ripple
* @attr ref com.google.android.material.R.styleable#FloatingActionButton_rippleColor
* @see #getRippleColor()
*/
public void setRippleColor(@ColorInt int color) {
setRippleColor(ColorStateList.valueOf(color));
}
/**
* Sets the ripple color for this button.
*
* <p>When running on devices with KitKat, we draw this color as a filled circle rather
* than a ripple.
*
* @param color color state list to use for the ripple
* @attr ref com.google.android.material.R.styleable#FloatingActionButton_rippleColor
* @see #getRippleColor()
*/
public void setRippleColor(@Nullable ColorStateList color) {
if (rippleColor != color) {
rippleColor = color;
getImpl().setRippleColor(rippleColor);
}
}
@Override
@NonNull
public CoordinatorLayout.Behavior<FloatingActionButton> getBehavior() {
return new FloatingActionButton.Behavior();
}
/**
* Returns the tint applied to the background drawable, if specified.
*
* @return the tint applied to the background drawable
* @see #setBackgroundTintList(ColorStateList)
*/
@Nullable
@Override
public ColorStateList getBackgroundTintList() {
return backgroundTint;
}
/**
* Applies a tint to the background drawable. Does not modify the current tint mode, which is
* {@link PorterDuff.Mode#SRC_IN} by default.
*
* @param tint the tint to apply, may be {@code null} to clear tint
*/
@Override
public void setBackgroundTintList(@Nullable ColorStateList tint) {
if (backgroundTint != tint) {
backgroundTint = tint;
getImpl().setBackgroundTintList(tint);
}
}
/**
* Returns the blending mode used to apply the tint to the background drawable, if specified.
*
* @return the blending mode used to apply the tint to the background drawable
* @see #setBackgroundTintMode(PorterDuff.Mode)
*/
@Nullable
@Override
public PorterDuff.Mode getBackgroundTintMode() {
return backgroundTintMode;
}
/**
* Specifies the blending mode used to apply the tint specified by {@link
* #setBackgroundTintList(ColorStateList)}} to the background drawable. The default mode is {@link
* PorterDuff.Mode#SRC_IN}.
*
* @param tintMode the blending mode used to apply the tint, may be {@code null} to clear tint
*/
@Override
public void setBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) {
if (backgroundTintMode != tintMode) {
backgroundTintMode = tintMode;
getImpl().setBackgroundTintMode(tintMode);
}
}
/**
* Compat method to support {@link TintableBackgroundView}. Use {@link
* #setBackgroundTintList(ColorStateList)} directly instead.
*/
@Override
public void setSupportBackgroundTintList(@Nullable ColorStateList tint) {
setBackgroundTintList(tint);
}
/**
* Compat method to support {@link TintableBackgroundView}. Use {@link #getBackgroundTintList()}
* directly instead.
*/
@Nullable
@Override
public ColorStateList getSupportBackgroundTintList() {
return getBackgroundTintList();
}
/**
* Compat method to support {@link TintableBackgroundView}. Use {@link
* #setBackgroundTintMode(Mode)} directly instead.
*/
@Override
public void setSupportBackgroundTintMode(@Nullable Mode tintMode) {
setBackgroundTintMode(tintMode);
}
/**
* Compat method to support {@link TintableBackgroundView}. Use {@link #getBackgroundTintMode()}
* directly instead.
*/
@Nullable
@Override
public Mode getSupportBackgroundTintMode() {
return getBackgroundTintMode();
}
@Override
public void setSupportImageTintList(@Nullable ColorStateList tint) {
if (imageTint != tint) {
imageTint = tint;
onApplySupportImageTint();
}
}
@Nullable
@Override
public ColorStateList getSupportImageTintList() {
return imageTint;
}
@Override
public void setSupportImageTintMode(@Nullable Mode tintMode) {
if (imageMode != tintMode) {
imageMode = tintMode;
onApplySupportImageTint();
}
}
@Nullable
@Override
public Mode getSupportImageTintMode() {
return imageMode;
}
private void onApplySupportImageTint() {
Drawable drawable = getDrawable();
if (drawable == null) {
return;
}
if (imageTint == null) {
DrawableCompat.clearColorFilter(drawable);
return;
}
int color = imageTint.getColorForState(getDrawableState(), Color.TRANSPARENT);
Mode mode = imageMode;
if (mode == null) {
mode = Mode.SRC_IN;
}
drawable
.mutate()
.setColorFilter(AppCompatDrawableManager.getPorterDuffColorFilter(color, mode));
}
@Override
public void setBackgroundDrawable(Drawable background) {
Log.i(LOG_TAG, "Setting a custom background is not supported.");
}
@Override
public void setBackgroundResource(int resid) {
Log.i(LOG_TAG, "Setting a custom background is not supported.");
}
@Override
public void setBackgroundColor(int color) {
Log.i(LOG_TAG, "Setting a custom background is not supported.");
}
@Override
public void setImageResource(@DrawableRes int resId) {
// Intercept this call and instead retrieve the Drawable via the image helper
imageHelper.setImageResource(resId);
onApplySupportImageTint();
}
@Override
public void setImageDrawable(@Nullable Drawable drawable) {
if (getDrawable() != drawable) {
super.setImageDrawable(drawable);
getImpl().updateImageMatrixScale();
if (imageTint != null) {
onApplySupportImageTint();
}
}
}
/** Sets the {@link ShapeAppearanceModel} for this {@link FloatingActionButton}. */
@Override
public void setShapeAppearanceModel(@NonNull ShapeAppearanceModel shapeAppearance) {
getImpl().setShapeAppearance(shapeAppearance);
}
/** Returns the {@link ShapeAppearanceModel} for this {@link FloatingActionButton}. */
@Override
@NonNull
public ShapeAppearanceModel getShapeAppearanceModel() {
return checkNotNull(getImpl().getShapeAppearance());
}
/**
* Returns whether this fab will expand its bounds (if needed) to meet the minimum touch target
* size.
*
* @see #setEnsureMinTouchTargetSize(boolean)
* @attr ref com.google.android.material.R.styleable#FloatingActionButton_ensureMinTouchTargetSize
*/
public boolean shouldEnsureMinTouchTargetSize() {
return getImpl().getEnsureMinTouchTargetSize();
}
/**
* Sets whether this FloatingActionButton should expand its bounds (if needed) to meet the minimum
* touch target size.
*
* @attr ref com.google.android.material.R.styleable#FloatingActionButton_ensureMinTouchTargetSize
*/
public void setEnsureMinTouchTargetSize(boolean flag) {
if (flag != getImpl().getEnsureMinTouchTargetSize()) {
getImpl().setEnsureMinTouchTargetSize(flag);
requestLayout();
}
}
@Override
public void setVisibility(int visibility) {
super.setVisibility(visibility);
}
/**
* Sets the max image size for this button.
*
* @param imageSize maximum icon image size
* @attr ref com.google.android.material.R.styleable#FloatingActionButton_maxImageSize
*/
public void setMaxImageSize(int imageSize) {
maxImageSize = imageSize;
getImpl().setMaxImageSize(imageSize);
}
/**
* Shows the button.
*
* <p>This method will animate the button show if the view has already been laid out.
*/
public void show() {
show(null);
}
/**
* Shows the button.
*
* <p>This method will animate the button show if the view has already been laid out.
*
* @param listener the listener to notify when this view is shown
*/
public void show(@Nullable final OnVisibilityChangedListener listener) {
show(listener, true);
}
void show(@Nullable OnVisibilityChangedListener listener, boolean fromUser) {
getImpl().show(wrapOnVisibilityChangedListener(listener), fromUser);
}
public void addOnShowAnimationListener(@NonNull AnimatorListener listener) {
getImpl().addOnShowAnimationListener(listener);
}
public void removeOnShowAnimationListener(@NonNull AnimatorListener listener) {
getImpl().removeOnShowAnimationListener(listener);
}
/**
* Hides the button.
*
* <p>This method will animate the button hide if the view has already been laid out.
*/
public void hide() {
hide(null);
}
/**
* Hides the button.
*
* <p>This method will animate the button hide if the view has already been laid out.
*
* @param listener the listener to notify when this view is hidden
*/
public void hide(@Nullable OnVisibilityChangedListener listener) {
hide(listener, true);
}
void hide(@Nullable OnVisibilityChangedListener listener, boolean fromUser) {
getImpl().hide(wrapOnVisibilityChangedListener(listener), fromUser);
}
public void addOnHideAnimationListener(@NonNull AnimatorListener listener) {
getImpl().addOnHideAnimationListener(listener);
}
public void removeOnHideAnimationListener(@NonNull AnimatorListener listener) {
getImpl().removeOnHideAnimationListener(listener);
}
@Override
public boolean setExpanded(boolean expanded) {
return expandableWidgetHelper.setExpanded(expanded);
}
@Override
public boolean isExpanded() {
return expandableWidgetHelper.isExpanded();
}
@Override
public void setExpandedComponentIdHint(@IdRes int expandedComponentIdHint) {
expandableWidgetHelper.setExpandedComponentIdHint(expandedComponentIdHint);
}
@Override
public int getExpandedComponentIdHint() {
return expandableWidgetHelper.getExpandedComponentIdHint();
}
/**
* Set whether FloatingActionButton should add inner padding on platforms Lollipop and after, to
* ensure consistent dimensions on all platforms.
*
* @param useCompatPadding true if FloatingActionButton is adding inner padding on platforms
* Lollipop and after, to ensure consistent dimensions on all platforms.
* @attr ref com.google.android.material.R.styleable#FloatingActionButton_useCompatPadding
* @see #getUseCompatPadding()
*/
public void setUseCompatPadding(boolean useCompatPadding) {
if (compatPadding != useCompatPadding) {
compatPadding = useCompatPadding;
getImpl().onCompatShadowChanged();
}
}
/**
* Returns whether FloatingActionButton will add inner padding on platforms Lollipop and after.
*
* @return true if FloatingActionButton is adding inner padding on platforms Lollipop and after,
* to ensure consistent dimensions on all platforms.
* @attr ref com.google.android.material.R.styleable#FloatingActionButton_useCompatPadding
* @see #setUseCompatPadding(boolean)
*/
public boolean getUseCompatPadding() {
return compatPadding;
}
/**
* Sets the size of the button.
*
* <p>The options relate to the options available on the material design specification. {@link
* #SIZE_NORMAL} is larger than {@link #SIZE_MINI}. {@link #SIZE_AUTO} will choose an appropriate
* size based on the screen size.
*
* <p>Calling this method will turn off custom sizing (see {@link #setCustomSize(int)}) if it was
* previously on.
*
* @param size one of {@link #SIZE_NORMAL}, {@link #SIZE_MINI} or {@link #SIZE_AUTO}
* @attr ref com.google.android.material.R.styleable#FloatingActionButton_fabSize
*/
public void setSize(@Size int size) {
customSize = NO_CUSTOM_SIZE;
if (size != this.size) {
this.size = size;
requestLayout();
}
}
/**
* Returns the chosen size for this button.
*
* @return one of {@link #SIZE_NORMAL}, {@link #SIZE_MINI} or {@link #SIZE_AUTO}
* @see #setSize(int)
*/
@Size
public int getSize() {
return size;
}
@Nullable
private InternalVisibilityChangedListener wrapOnVisibilityChangedListener(
@Nullable final OnVisibilityChangedListener listener) {
if (listener == null) {
return null;
}
return new InternalVisibilityChangedListener() {
@Override
public void onShown() {
listener.onShown(FloatingActionButton.this);
}
@Override
public void onHidden() {
listener.onHidden(FloatingActionButton.this);
}
};
}
public boolean isOrWillBeHidden() {
return getImpl().isOrWillBeHidden();
}
public boolean isOrWillBeShown() {
return getImpl().isOrWillBeShown();
}
/**
* Sets the size of the button to be a custom value in pixels.
*
* <p>If you've set a custom size and would like to clear it, you can use the {@link
* #clearCustomSize()} method. If called, custom sizing will not be used and the size will be
* calculated based on the value set using {@link #setSize(int)} or the {@code fabSize} attribute.
*
* @param size preferred size in pixels, or {@link #NO_CUSTOM_SIZE}
* @attr ref com.google.android.material.R.styleable#FloatingActionButton_fabCustomSize
*/
public void setCustomSize(@Px int size) {
if (size < 0) {
throw new IllegalArgumentException("Custom size must be non-negative");
}
if (size != customSize) {
customSize = size;
requestLayout();
}
}
/**
* Returns the custom size for this {@link FloatingActionButton}.
*
* @return size in pixels, or {@link #NO_CUSTOM_SIZE}
*/
@Px
public int getCustomSize() {
return customSize;
}
/**
* Clears the custom size for this {@link FloatingActionButton}.
*
* <p>If called, custom sizing will not be used and the size will be calculated based on the value
* set using {@link #setSize(int)} or the {@code fabSize} attribute
*/
public void clearCustomSize() {
setCustomSize(NO_CUSTOM_SIZE);
}
int getSizeDimension() {
return getSizeDimension(size);
}
private int getSizeDimension(@Size final int size) {
if (customSize != NO_CUSTOM_SIZE) {
return customSize;
}
final Resources res = getResources();
switch (size) {
case SIZE_AUTO:
// If we're set to auto, grab the size from resources and refresh
final int width = res.getConfiguration().screenWidthDp;
final int height = res.getConfiguration().screenHeightDp;
return Math.max(width, height) < AUTO_MINI_LARGEST_SCREEN_WIDTH
? getSizeDimension(SIZE_MINI)
: getSizeDimension(SIZE_NORMAL);
case SIZE_MINI:
return res.getDimensionPixelSize(R.dimen.design_fab_size_mini);
case SIZE_NORMAL:
default:
return res.getDimensionPixelSize(R.dimen.design_fab_size_normal);
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
getImpl().onAttachedToWindow();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
getImpl().onDetachedFromWindow();
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
if (Build.VERSION.SDK_INT == VERSION_CODES.LOLLIPOP) {
getImpl().onDrawableStateChangedForLollipop();
}
}
@SuppressWarnings("RedundantOverride")
@Override
public void jumpDrawablesToCurrentState() {
super.jumpDrawablesToCurrentState();
}
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
if (superState == null) {
superState = new Bundle();
}
ExtendableSavedState state = new ExtendableSavedState(superState);
state.extendableStates.put(
EXPANDABLE_WIDGET_HELPER_KEY, expandableWidgetHelper.onSaveInstanceState());
return state;
}
@Override
@SuppressWarnings("nullness:argument")
// onRestoreInstanceState should accept nullable
protected void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof ExtendableSavedState)) {
super.onRestoreInstanceState(state);
return;
}
ExtendableSavedState ess = (ExtendableSavedState) state;
super.onRestoreInstanceState(ess.getSuperState());
expandableWidgetHelper.onRestoreInstanceState(
checkNotNull(ess.extendableStates.get(EXPANDABLE_WIDGET_HELPER_KEY)));
}
/**
* Return in {@code rect} the bounds of the actual floating action button content in view-local
* coordinates. This is defined as anything within any visible shadow.
*
* @return true if this view actually has been laid out and has a content rect, else false.
* @deprecated prefer {@link FloatingActionButton#getMeasuredContentRect} instead, so you don't
* need to handle the case where the view isn't laid out.
*/
@Deprecated
public boolean getContentRect(@NonNull Rect rect) {
if (isLaidOut()) {
rect.set(0, 0, getWidth(), getHeight());
offsetRectWithShadow(rect);
return true;
} else {
return false;
}
}
/**
* Return in {@code rect} the bounds of the actual floating action button content in view-local
* coordinates. This is defined as anything within any visible shadow.
*/
public void getMeasuredContentRect(@NonNull Rect rect) {
rect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
offsetRectWithShadow(rect);
}
private void getTouchTargetRect(@NonNull Rect rect) {
getMeasuredContentRect(rect);
int touchTargetPadding = impl.getTouchTargetPadding();
rect.inset(-touchTargetPadding, -touchTargetPadding);
}
private void offsetRectWithShadow(@NonNull Rect rect) {
rect.left += shadowPadding.left;
rect.top += shadowPadding.top;
rect.right -= shadowPadding.right;
rect.bottom -= shadowPadding.bottom;
}
/** Returns the FloatingActionButton's background, minus any compatible shadow implementation. */
@Nullable
public Drawable getContentBackground() {
return getImpl().getContentBackground();
}
@Override
public boolean onTouchEvent(@NonNull MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
// Skipping the gesture if it doesn't start in the FAB 'content' area
getTouchTargetRect(touchArea);
if (!touchArea.contains((int) ev.getX(), (int) ev.getY())) {
return false;
}
}
return super.onTouchEvent(ev);
}
@Override
public CharSequence getAccessibilityClassName() {
return ACCESSIBIILTY_FAB_ROLE;
}
/**
* Behavior designed for use with {@link FloatingActionButton} instances. Its main function is to
* move {@link FloatingActionButton} views so that any displayed {@link
* com.google.android.material.snackbar.Snackbar}s do not cover them.
*/
// TODO(b/76413401): remove this generic type after the widget migration is done
public static class Behavior extends BaseBehavior<FloatingActionButton> {
public Behavior() {
super();
}
public Behavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
/**
* Behavior designed for use with {@link FloatingActionButton} instances. Its main function is to
* move {@link FloatingActionButton} views so that any displayed {@link
* com.google.android.material.snackbar.Snackbar}s do not cover them.
*/
// TODO(b/76413401): remove this generic type after the widget migration is done
protected static class BaseBehavior<T extends FloatingActionButton>
extends CoordinatorLayout.Behavior<T> {
private static final boolean AUTO_HIDE_DEFAULT = true;
private Rect tmpRect;
private OnVisibilityChangedListener internalAutoHideListener;
private boolean autoHideEnabled;
public BaseBehavior() {
super();
autoHideEnabled = AUTO_HIDE_DEFAULT;
}
public BaseBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a =
context.obtainStyledAttributes(attrs, R.styleable.FloatingActionButton_Behavior_Layout);
autoHideEnabled =
a.getBoolean(
R.styleable.FloatingActionButton_Behavior_Layout_behavior_autoHide,
AUTO_HIDE_DEFAULT);
a.recycle();
}
/**
* Sets whether the associated FloatingActionButton automatically hides when there is not enough
* space to be displayed. This works with {@link AppBarLayout} and {@link BottomSheetBehavior}.
*
* @attr ref
* com.google.android.material.R.styleable#FloatingActionButton_Behavior_Layout_behavior_autoHide
* @param autoHide true to enable automatic hiding
*/
public void setAutoHideEnabled(boolean autoHide) {
autoHideEnabled = autoHide;
}
/**
* Returns whether the associated FloatingActionButton automatically hides when there is not
* enough space to be displayed.
*
* @attr ref
* com.google.android.material.R.styleable#FloatingActionButton_Behavior_Layout_behavior_autoHide
* @return true if enabled
*/
public boolean isAutoHideEnabled() {
return autoHideEnabled;
}
@Override
public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams lp) {
if (lp.dodgeInsetEdges == Gravity.NO_GRAVITY) {
// If the developer hasn't set dodgeInsetEdges, lets set it to BOTTOM so that
// we dodge any Snackbars
lp.dodgeInsetEdges = Gravity.BOTTOM;
}
}
@Override
public boolean onDependentViewChanged(
CoordinatorLayout parent, @NonNull FloatingActionButton child, View dependency) {
if (dependency instanceof AppBarLayout) {
// If we're depending on an AppBarLayout we will show/hide it automatically
// if the FAB is anchored to the AppBarLayout
updateFabVisibilityForAppBarLayout(parent, (AppBarLayout) dependency, child);
} else if (isBottomSheet(dependency)) {
updateFabVisibilityForBottomSheet(dependency, child);
}
return false;
}
private static boolean isBottomSheet(@NonNull View view) {
final ViewGroup.LayoutParams lp = view.getLayoutParams();
if (lp instanceof CoordinatorLayout.LayoutParams) {
return ((CoordinatorLayout.LayoutParams) lp).getBehavior() instanceof BottomSheetBehavior;
}
return false;
}
@VisibleForTesting
public void setInternalAutoHideListener(OnVisibilityChangedListener listener) {
internalAutoHideListener = listener;
}
// dereference of possibly-null reference lp
@SuppressWarnings("nullness:dereference.of.nullable")
private boolean ignoreUpdateVisibility(
@NonNull View dependency, @NonNull FloatingActionButton child) {
final CoordinatorLayout.LayoutParams lp =
(CoordinatorLayout.LayoutParams) child.getLayoutParams();
if (!autoHideEnabled) {
return true;
}
if (lp.getAnchorId() != dependency.getId()) {
// The anchor ID doesn't match the dependency, so we won't automatically
// show/hide the FAB
return true;
}
//noinspection RedundantIfStatement
if (child.getUserSetVisibility() != VISIBLE) {
// The view isn't set to be visible so skip changing its visibility
return true;
}
return false;
}
private boolean updateFabVisibilityForAppBarLayout(
CoordinatorLayout parent,
@NonNull AppBarLayout appBarLayout,
@NonNull FloatingActionButton child) {
if (ignoreUpdateVisibility(appBarLayout, child)) {
return false;
}
if (tmpRect == null) {
tmpRect = new Rect();
}
// First, let's get the visible rect of the dependency
final Rect rect = tmpRect;
DescendantOffsetUtils.getDescendantRect(parent, appBarLayout, rect);
if (rect.bottom <= appBarLayout.getMinimumHeightForVisibleOverlappingContent()) {
// If the anchor's bottom is below the seam, we'll animate our FAB out
child.hide(internalAutoHideListener, false);
} else {
// Else, we'll animate our FAB back in
child.show(internalAutoHideListener, false);
}
return true;
}
// dereference of possibly-null reference lp
@SuppressWarnings("nullness:dereference.of.nullable")
private boolean updateFabVisibilityForBottomSheet(
@NonNull View bottomSheet, @NonNull FloatingActionButton child) {
if (ignoreUpdateVisibility(bottomSheet, child)) {
return false;
}
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
if (bottomSheet.getTop() < child.getHeight() / 2 + lp.topMargin) {
child.hide(internalAutoHideListener, false);
} else {
child.show(internalAutoHideListener, false);
}
return true;
}
@Override
public boolean onLayoutChild(
@NonNull CoordinatorLayout parent,
@NonNull FloatingActionButton child,
int layoutDirection) {
// First, let's make sure that the visibility of the FAB is consistent
final List<View> dependencies = parent.getDependencies(child);
for (int i = 0, count = dependencies.size(); i < count; i++) {
final View dependency = dependencies.get(i);
if (dependency instanceof AppBarLayout) {
if (updateFabVisibilityForAppBarLayout(parent, (AppBarLayout) dependency, child)) {
break;
}
} else if (isBottomSheet(dependency)) {
if (updateFabVisibilityForBottomSheet(dependency, child)) {
break;
}
}
}
// Now let the CoordinatorLayout lay out the FAB
parent.onLayoutChild(child, layoutDirection);
// Now offset it if needed
offsetIfNeeded(parent, child);
return true;
}
@Override
public boolean getInsetDodgeRect(
@NonNull CoordinatorLayout parent,
@NonNull FloatingActionButton child,
@NonNull Rect rect) {
// Since we offset so that any internal shadow padding isn't shown, we need to make
// sure that the shadow isn't used for any dodge inset calculations
final Rect shadowPadding = child.shadowPadding;
rect.set(
child.getLeft() + shadowPadding.left,
child.getTop() + shadowPadding.top,
child.getRight() - shadowPadding.right,
child.getBottom() - shadowPadding.bottom);
return true;
}
/**
* Pre-Lollipop we use padding so that the shadow has enough space to be drawn. This method
* offsets our layout position so that we're positioned correctly if we're on one of our
* parent's edges.
*/
// dereference of possibly-null reference lp
@SuppressWarnings("nullness:dereference.of.nullable")
private void offsetIfNeeded(
@NonNull CoordinatorLayout parent, @NonNull FloatingActionButton fab) {
final Rect padding = fab.shadowPadding;
if (padding.centerX() > 0 && padding.centerY() > 0) {
final CoordinatorLayout.LayoutParams lp =
(CoordinatorLayout.LayoutParams) fab.getLayoutParams();
int offsetTB = 0;
int offsetLR = 0;
if (fab.getRight() >= parent.getWidth() - lp.rightMargin) {
// If we're on the right edge, shift it the right
offsetLR = padding.right;
} else if (fab.getLeft() <= lp.leftMargin) {
// If we're on the left edge, shift it the left
offsetLR = -padding.left;
}
if (fab.getBottom() >= parent.getHeight() - lp.bottomMargin) {
// If we're on the bottom edge, shift it down
offsetTB = padding.bottom;
} else if (fab.getTop() <= lp.topMargin) {
// If we're on the top edge, shift it up
offsetTB = -padding.top;
}
if (offsetTB != 0) {
ViewCompat.offsetTopAndBottom(fab, offsetTB);
}
if (offsetLR != 0) {
ViewCompat.offsetLeftAndRight(fab, offsetLR);
}
}
}
}
@Override
public void setElevation(float elevation) {
super.setElevation(elevation);
getImpl().updateShapeElevation(elevation);
}
/**
* Returns the backward compatible elevation of the FloatingActionButton.
*
* @return the backward compatible elevation in pixels.
* @attr ref com.google.android.material.R.styleable#FloatingActionButton_elevation
* @see #setCompatElevation(float)
*/
public float getCompatElevation() {
return getImpl().getElevation();
}
/**
* Updates the backward compatible elevation of the FloatingActionButton.
*
* @param elevation The backward compatible elevation in pixels.
* @attr ref com.google.android.material.R.styleable#FloatingActionButton_elevation
* @see #getCompatElevation()
* @see #setUseCompatPadding(boolean)
*/
public void setCompatElevation(float elevation) {
getImpl().setElevation(elevation);
}
/**
* Updates the backward compatible elevation of the FloatingActionButton.
*
* @param id The resource id of the backward compatible elevation.
* @attr ref com.google.android.material.R.styleable#FloatingActionButton_elevation
* @see #getCompatElevation()
* @see #setUseCompatPadding(boolean)
*/
public void setCompatElevationResource(@DimenRes int id) {
setCompatElevation(getResources().getDimension(id));
}
/**
* Returns the backward compatible hovered/focused translationZ of the FloatingActionButton.
*
* @return the backward compatible hovered/focused translationZ in pixels.
* @attr ref
* com.google.android.material.R.styleable#FloatingActionButton_hoveredFocusedTranslationZ
* @see #setCompatHoveredFocusedTranslationZ(float)
*/
public float getCompatHoveredFocusedTranslationZ() {
return getImpl().getHoveredFocusedTranslationZ();
}
/**
* Updates the backward compatible hovered/focused translationZ of the FloatingActionButton.
*
* @param translationZ The backward compatible hovered/focused translationZ in pixels.
* @attr ref
* com.google.android.material.R.styleable#FloatingActionButton_hoveredFocusedTranslationZ
* @see #getCompatHoveredFocusedTranslationZ()
* @see #setUseCompatPadding(boolean)
*/
public void setCompatHoveredFocusedTranslationZ(float translationZ) {
getImpl().setHoveredFocusedTranslationZ(translationZ);
}
/**
* Updates the backward compatible hovered/focused translationZ of the FloatingActionButton.
*
* @param id The resource id of the backward compatible hovered/focused translationZ.
* @attr ref
* com.google.android.material.R.styleable#FloatingActionButton_hoveredFocusedTranslationZ
* @see #getCompatHoveredFocusedTranslationZ()
* @see #setUseCompatPadding(boolean)
*/
public void setCompatHoveredFocusedTranslationZResource(@DimenRes int id) {
setCompatHoveredFocusedTranslationZ(getResources().getDimension(id));
}
/**
* Returns the backward compatible pressed translationZ of the FloatingActionButton.
*
* @return the backward compatible pressed translationZ in pixels.
* @attr ref com.google.android.material.R.styleable#FloatingActionButton_pressedTranslationZ
* @see #setCompatPressedTranslationZ(float)
*/
public float getCompatPressedTranslationZ() {
return getImpl().getPressedTranslationZ();
}
/**
* Updates the backward compatible pressed translationZ of the FloatingActionButton.
*
* @param translationZ The backward compatible pressed translationZ in pixels.
* @attr ref com.google.android.material.R.styleable#FloatingActionButton_pressedTranslationZ
* @see #getCompatPressedTranslationZ()
* @see #setUseCompatPadding(boolean)
*/
public void setCompatPressedTranslationZ(float translationZ) {
getImpl().setPressedTranslationZ(translationZ);
}
/**
* Updates the backward compatible pressed translationZ of the FloatingActionButton.
*
* @param id The resource id of the backward compatible pressed translationZ.
* @attr ref com.google.android.material.R.styleable#FloatingActionButton_pressedTranslationZ
* @see #getCompatPressedTranslationZ()
* @see #setUseCompatPadding(boolean)
*/
public void setCompatPressedTranslationZResource(@DimenRes int id) {
setCompatPressedTranslationZ(getResources().getDimension(id));
}
/** Returns the motion spec for the show animation. */
@Nullable
public MotionSpec getShowMotionSpec() {
return getImpl().getShowMotionSpec();
}
/**
* Updates the motion spec for the show animation.
*
* @attr ref com.google.android.material.R.styleable#FloatingActionButton_showMotionSpec
*/
public void setShowMotionSpec(@Nullable MotionSpec spec) {
getImpl().setShowMotionSpec(spec);
}
/**
* Updates the motion spec for the show animation.
*
* @attr ref com.google.android.material.R.styleable#FloatingActionButton_showMotionSpec
*/
public void setShowMotionSpecResource(@AnimatorRes int id) {
setShowMotionSpec(MotionSpec.createFromResource(getContext(), id));
}
/** Returns the motion spec for the hide animation. */
@Nullable
public MotionSpec getHideMotionSpec() {
return getImpl().getHideMotionSpec();
}
/**
* Updates the motion spec for the hide animation.
*
* @attr ref com.google.android.material.R.styleable#FloatingActionButton_hideMotionSpec
*/
public void setHideMotionSpec(@Nullable MotionSpec spec) {
getImpl().setHideMotionSpec(spec);
}
/**
* Updates the motion spec for the hide animation.
*
* @attr ref com.google.android.material.R.styleable#FloatingActionButton_hideMotionSpec
*/
public void setHideMotionSpecResource(@AnimatorRes int id) {
setHideMotionSpec(MotionSpec.createFromResource(getContext(), id));
}
/** Add a {@link TransformationCallback} which can watch for changes to this view. */
public void addTransformationCallback(
@NonNull TransformationCallback<? extends FloatingActionButton> listener) {
getImpl().addTransformationCallback(new TransformationCallbackWrapper<>(listener));
}
/**
* Remove the {@link TransformationCallback} from this view. It will no longer receive updates
* when this view is transformed.
*/
public void removeTransformationCallback(
@NonNull TransformationCallback<? extends FloatingActionButton> listener) {
getImpl().removeTransformationCallback(new TransformationCallbackWrapper<>(listener));
}
class TransformationCallbackWrapper<T extends FloatingActionButton>
implements InternalTransformationCallback {
@NonNull private final TransformationCallback<T> listener;
TransformationCallbackWrapper(@NonNull TransformationCallback<T> listener) {
this.listener = listener;
}
@Override
public void onTranslationChanged() {
listener.onTranslationChanged((T) FloatingActionButton.this);
}
@Override
public void onScaleChanged() {
listener.onScaleChanged((T) FloatingActionButton.this);
}
@Override
public boolean equals(@Nullable Object obj) {
return obj instanceof TransformationCallbackWrapper
&& ((TransformationCallbackWrapper) obj).listener.equals(listener);
}
@Override
public int hashCode() {
return listener.hashCode();
}
}
@Override
public void setTranslationX(float translationX) {
super.setTranslationX(translationX);
getImpl().onTranslationChanged();
}
@Override
public void setTranslationY(float translationY) {
super.setTranslationY(translationY);
getImpl().onTranslationChanged();
}
@Override
public void setTranslationZ(float translationZ) {
super.setTranslationZ(translationZ);
getImpl().onTranslationChanged();
}
@Override
public void setScaleX(float scaleX) {
super.setScaleX(scaleX);
getImpl().onScaleChanged();
}
@Override
public void setScaleY(float scaleY) {
super.setScaleY(scaleY);
getImpl().onScaleChanged();
}
/**
* Set whether padding to show compat shadows should be added.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@VisibleForTesting
public void setShadowPaddingEnabled(boolean shadowPaddingEnabled) {
getImpl().setShadowPaddingEnabled(shadowPaddingEnabled);
}
private FloatingActionButtonImpl getImpl() {
if (impl == null) {
impl = new FloatingActionButtonImpl(this, new ShadowDelegateImpl());
}
return impl;
}
private class ShadowDelegateImpl implements ShadowViewDelegate {
ShadowDelegateImpl() {}
@Override
public float getRadius() {
return getSizeDimension() / 2f;
}
@Override
public void setShadowPadding(int left, int top, int right, int bottom) {
shadowPadding.set(left, top, right, bottom);
setPadding(
left + imagePadding, top + imagePadding, right + imagePadding, bottom + imagePadding);
}
@Override
public void setBackgroundDrawable(@Nullable Drawable background) {
if (background != null) {
FloatingActionButton.super.setBackgroundDrawable(background);
}
}
@Override
public boolean isCompatPaddingEnabled() {
return compatPadding;
}
}
}