/* * 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.RequiresApi; 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. * *
Floating action buttons come in two sizes: the default and the mini. The size can be * controlled with the {@code fabSize} attribute. * *
As this class descends from {@link ImageView}, you can control the icon which is displayed via * {@link #setImageDrawable(Drawable)}. * *
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)}. * *
For more information, see the component * developer guidance and design guidelines. */ public class FloatingActionButton extends VisibilityAwareImageButton implements TintableBackgroundView, TintableImageSourceView, ExpandableTransformationWidget, Shapeable, CoordinatorLayout.AttachedBehavior { 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. * *
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. * *
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 This method will animate the button show if the view has already been laid out.
*/
public void show() {
show(null);
}
/**
* Shows the button.
*
* 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.
*
* This method will animate the button hide if the view has already been laid out.
*/
public void hide() {
hide(null);
}
/**
* Hides the button.
*
* 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.
*
* 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.
*
* 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.
*
* 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}.
*
* 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();
getImpl().onDrawableStateChanged(getDrawableState());
}
@Override
public void jumpDrawablesToCurrentState() {
super.jumpDrawablesToCurrentState();
getImpl().jumpDrawableToCurrentState();
}
@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 (ViewCompat.isLaidOut(this)) {
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);
}
/**
* 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