pubiqq bc2fdeadaf [Internal] Prefer platform methods to compat ones
Resolves https://github.com/material-components/material-components-android/pull/4532

GIT_ORIGIN_REV_ID=180dec736f2521579e483317a4d537629d8f247b
PiperOrigin-RevId: 715494344
2025-01-15 18:06:07 +00:00

1538 lines
56 KiB
Java

/*
* Copyright 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.bottomappbar;
import com.google.android.material.R;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static com.google.android.material.shape.MaterialShapeDrawable.SHADOW_COMPAT_MODE_NEVER;
import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.graphics.RectF;
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.widget.ActionMenuView;
import androidx.appcompat.widget.Toolbar;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.ColorInt;
import androidx.annotation.Dimension;
import androidx.annotation.IntDef;
import androidx.annotation.MenuRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.annotation.RestrictTo;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.coordinatorlayout.widget.CoordinatorLayout.AttachedBehavior;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.view.ViewCompat.NestedScrollType;
import androidx.core.view.ViewCompat.ScrollAxis;
import androidx.core.view.WindowInsetsCompat;
import androidx.customview.view.AbsSavedState;
import com.google.android.material.animation.AnimationUtils;
import com.google.android.material.animation.TransformationCallback;
import com.google.android.material.behavior.HideBottomViewOnScrollBehavior;
import com.google.android.material.behavior.HideBottomViewOnScrollBehavior.OnScrollStateChangedListener;
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.floatingactionbutton.FloatingActionButton.OnVisibilityChangedListener;
import com.google.android.material.internal.ThemeEnforcement;
import com.google.android.material.internal.ViewUtils;
import com.google.android.material.internal.ViewUtils.RelativePadding;
import com.google.android.material.motion.MotionUtils;
import com.google.android.material.resources.MaterialResources;
import com.google.android.material.shape.EdgeTreatment;
import com.google.android.material.shape.MaterialShapeDrawable;
import com.google.android.material.shape.MaterialShapeUtils;
import com.google.android.material.shape.ShapeAppearanceModel;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
/**
* The Bottom App Bar is an extension of Toolbar that supports a shaped background that "cradles" an
* attached {@link FloatingActionButton}. A FAB is anchored to {@link BottomAppBar} by calling
* {@link CoordinatorLayout.LayoutParams#setAnchorId(int)}, or by setting {@code app:layout_anchor}
* on the FAB in xml.
*
* <p>Note: The Material Design Guidelines caution against using an {@link
* ExtendedFloatingActionButton} with a {@link BottomAppBar}, so there is limited support for that
* use case. {@link ExtendedFloatingActionButton} can be anchored to the {@link BottomAppBar}, but
* currently animations and the cutout are not supported.
*
* <p>There are two modes which determine where the FAB is shown relative to the {@link
* BottomAppBar}. {@link #FAB_ALIGNMENT_MODE_CENTER} mode is the primary mode with the FAB is
* centered. {@link #FAB_ALIGNMENT_MODE_END} is the secondary mode with the FAB on the side.
*
* <p>Do not use the {@code android:background} attribute or call {@code BottomAppBar.setBackground}
* because the BottomAppBar manages its background internally. Instead use {@code
* app:backgroundTint}.
*
* <p>To enable color theming for menu items you will also need to set the {@code
* materialThemeOverlay} attribute to a ThemeOverlay which sets the {@code colorControlNormal}
* attribute to the correct color. For example, if the background of the BottomAppBar is {@code
* colorSurface}, as it is in the default style, you should set {@code materialThemeOverlay} to
* {@code @style/ThemeOverlay.MaterialComponents.BottomAppBar.Surface}.
*
* <p>For more information, see the <a
* href="https://github.com/material-components/material-components-android/blob/master/docs/components/BottomAppBar.md">component
* developer guidance</a> and <a
* href="https://material.io/components/bottom-app-bar/overview">design guidelines</a>.
*
* @attr ref com.google.android.material.R.styleable#BottomAppBar_backgroundTint
* @attr ref com.google.android.material.R.styleable#BottomAppBar_fabAlignmentMode
* @attr ref com.google.android.material.R.styleable#BottomAppBar_fabAnchorMode
* @attr ref com.google.android.material.R.styleable#BottomAppBar_fabAnimationMode
* @attr ref com.google.android.material.R.styleable#BottomAppBar_fabCradleMargin
* @attr ref com.google.android.material.R.styleable#BottomAppBar_fabCradleRoundedCornerRadius
* @attr ref com.google.android.material.R.styleable#BottomAppBar_fabCradleVerticalOffset
* @attr ref com.google.android.material.R.styleable#BottomAppBar_hideOnScroll
* @attr ref com.google.android.material.R.styleable#BottomAppBar_paddingBottomSystemWindowInsets
*/
public class BottomAppBar extends Toolbar implements AttachedBehavior {
private static final int DEF_STYLE_RES = R.style.Widget_MaterialComponents_BottomAppBar;
private static final int FAB_ALIGNMENT_ANIM_DURATION_DEFAULT = 300;
private static final int FAB_ALIGNMENT_ANIM_DURATION_ATTR = R.attr.motionDurationLong2;
private static final int FAB_ALIGNMENT_ANIM_EASING_ATTR =
R.attr.motionEasingEmphasizedInterpolator;
private static final float FAB_ALIGNMENT_ANIM_EASING_MIDPOINT = .2F;
public static final int FAB_ALIGNMENT_MODE_CENTER = 0;
public static final int FAB_ALIGNMENT_MODE_END = 1;
/**
* The fabAlignmentMode determines the horizontal positioning of the cradle and the FAB which can
* be centered or aligned to the end.
*/
@IntDef({FAB_ALIGNMENT_MODE_CENTER, FAB_ALIGNMENT_MODE_END})
@Retention(RetentionPolicy.SOURCE)
public @interface FabAlignmentMode {}
/** The FAB is embedded inside the BottomAppBar. */
public static final int FAB_ANCHOR_MODE_EMBED = 0;
/** The FAB is cradled at the top of the BottomAppBar. */
public static final int FAB_ANCHOR_MODE_CRADLE = 1;
/**
* The fabAnchorMode determines the placement of the FAB within the BottomAppBar. The FAB can be
* cradled at the top of the BottomAppBar, or embedded within it.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@IntDef({FAB_ANCHOR_MODE_EMBED, FAB_ANCHOR_MODE_CRADLE})
@Retention(RetentionPolicy.SOURCE)
public @interface FabAnchorMode {}
public static final int FAB_ANIMATION_MODE_SCALE = 0;
public static final int FAB_ANIMATION_MODE_SLIDE = 1;
/**
* The fabAnimationMode determines the animation used to move the FAB between different alignment
* modes. Can be either scale, or slide. Scale mode will scale the fab down to a point and then
* scale it back in at it's new position. Slide mode will slide the fab from one position to the
* other.
*/
@IntDef({FAB_ANIMATION_MODE_SCALE, FAB_ANIMATION_MODE_SLIDE})
@Retention(RetentionPolicy.SOURCE)
public @interface FabAnimationMode {}
/** The menu items are aligned automatically to avoid the FAB. */
public static final int MENU_ALIGNMENT_MODE_AUTO = 0;
/** The menu items are aligned to the start. */
public static final int MENU_ALIGNMENT_MODE_START = 1;
/**
* The menuAlignmentMode determines the alignment of the menu items in the BottomAppBar.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@IntDef({MENU_ALIGNMENT_MODE_AUTO, MENU_ALIGNMENT_MODE_START})
@Retention(RetentionPolicy.SOURCE)
public @interface MenuAlignmentMode {}
@Nullable private Integer navigationIconTint;
private final MaterialShapeDrawable materialShapeDrawable = new MaterialShapeDrawable();
@Nullable private Animator modeAnimator;
@Nullable private Animator menuAnimator;
@FabAlignmentMode private int fabAlignmentMode;
@FabAnimationMode private int fabAnimationMode;
@FabAnchorMode private int fabAnchorMode;
/** No end margin for the FAB. */
private static final int NO_FAB_END_MARGIN = -1;
private final int fabOffsetEndMode;
@Px private int fabAlignmentModeEndMargin;
@MenuAlignmentMode private int menuAlignmentMode;
private final boolean removeEmbeddedFabElevation;
private boolean hideOnScroll;
private final boolean paddingBottomSystemWindowInsets;
private final boolean paddingLeftSystemWindowInsets;
private final boolean paddingRightSystemWindowInsets;
/** Keeps track of the number of currently running animations. */
private int animatingModeChangeCounter = 0;
private ArrayList<AnimationListener> animationListeners;
/**
* Track whether or not a new menu will be inflated along with a FAB alignment change. The
* inflation of the resource is deferred until an appropriate time during the FAB alignment/menu
* animation before being set and clearing this pending resource.
*/
private static final int NO_MENU_RES_ID = 0;
@MenuRes private int pendingMenuResId = NO_MENU_RES_ID;
private boolean menuAnimatingWithFabAlignmentMode = false;
/** Callback to be invoked when the BottomAppBar is animating. */
interface AnimationListener {
void onAnimationStart(BottomAppBar bar);
void onAnimationEnd(BottomAppBar bar);
}
/**
* If the {@link FloatingActionButton} is actually cradled in the {@link BottomAppBar} or if the
* {@link FloatingActionButton} is detached which will happen when the {@link
* FloatingActionButton} is not visible, or when the {@link BottomAppBar} is scrolled off the
* screen.
*/
private boolean fabAttached = true;
private Behavior behavior;
private int bottomInset;
private int rightInset;
private int leftInset;
/**
* Listens to the FABs hide or show animation to kick off an animation on BottomAppBar that reacts
* to the change.
*/
@NonNull
AnimatorListenerAdapter fabAnimationListener =
new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
// If the FAB has begun to animate as a result of the FAB alignment mode changing, the FAB
// alignment animation will handle coordination of menu animation and this should be
// skipped. If the FAB has begun to animate as a result of hiding/showing the FAB, the
// menu animation should be cancelled and restarted.
if (!menuAnimatingWithFabAlignmentMode) {
maybeAnimateMenuView(fabAlignmentMode, fabAttached);
}
}
};
/** Listens to any transformations applied to the FAB so the cutout can react. */
@NonNull
TransformationCallback<FloatingActionButton> fabTransformationCallback =
new TransformationCallback<FloatingActionButton>() {
@Override
public void onScaleChanged(@NonNull FloatingActionButton fab) {
materialShapeDrawable.setInterpolation(
fab.getVisibility() == View.VISIBLE && fabAnchorMode == FAB_ANCHOR_MODE_CRADLE
? fab.getScaleY()
: 0);
}
@Override
public void onTranslationChanged(@NonNull FloatingActionButton fab) {
if (fabAnchorMode != FAB_ANCHOR_MODE_CRADLE) {
return;
}
float horizontalOffset = fab.getTranslationX();
if (getTopEdgeTreatment().getHorizontalOffset() != horizontalOffset) {
getTopEdgeTreatment().setHorizontalOffset(horizontalOffset);
materialShapeDrawable.invalidateSelf();
}
// If the translation of the fab has changed, update the vertical offset.
float verticalOffset = Math.max(0, -fab.getTranslationY());
if (getTopEdgeTreatment().getCradleVerticalOffset() != verticalOffset) {
getTopEdgeTreatment().setCradleVerticalOffset(verticalOffset);
materialShapeDrawable.invalidateSelf();
}
materialShapeDrawable.setInterpolation(
fab.getVisibility() == View.VISIBLE ? fab.getScaleY() : 0);
}
};
public BottomAppBar(@NonNull Context context) {
this(context, null);
}
public BottomAppBar(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, R.attr.bottomAppBarStyle);
}
public BottomAppBar(@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.BottomAppBar, defStyleAttr, DEF_STYLE_RES);
ColorStateList backgroundTint =
MaterialResources.getColorStateList(context, a, R.styleable.BottomAppBar_backgroundTint);
if (a.hasValue(R.styleable.BottomAppBar_navigationIconTint)) {
setNavigationIconTint(a.getColor(R.styleable.BottomAppBar_navigationIconTint, -1));
}
int elevation = a.getDimensionPixelSize(R.styleable.BottomAppBar_elevation, 0);
float fabCradleMargin = a.getDimensionPixelOffset(R.styleable.BottomAppBar_fabCradleMargin, 0);
float fabCornerRadius =
a.getDimensionPixelOffset(R.styleable.BottomAppBar_fabCradleRoundedCornerRadius, 0);
float fabVerticalOffset =
a.getDimensionPixelOffset(R.styleable.BottomAppBar_fabCradleVerticalOffset, 0);
fabAlignmentMode =
a.getInt(R.styleable.BottomAppBar_fabAlignmentMode, FAB_ALIGNMENT_MODE_CENTER);
fabAnimationMode =
a.getInt(R.styleable.BottomAppBar_fabAnimationMode, FAB_ANIMATION_MODE_SCALE);
fabAnchorMode = a.getInt(R.styleable.BottomAppBar_fabAnchorMode, FAB_ANCHOR_MODE_CRADLE);
removeEmbeddedFabElevation =
a.getBoolean(R.styleable.BottomAppBar_removeEmbeddedFabElevation, true);
menuAlignmentMode =
a.getInt(R.styleable.BottomAppBar_menuAlignmentMode, MENU_ALIGNMENT_MODE_AUTO);
hideOnScroll = a.getBoolean(R.styleable.BottomAppBar_hideOnScroll, false);
// Reading out if we are handling bottom padding, so we can apply it to the FAB.
paddingBottomSystemWindowInsets =
a.getBoolean(R.styleable.BottomAppBar_paddingBottomSystemWindowInsets, false);
paddingLeftSystemWindowInsets =
a.getBoolean(R.styleable.BottomAppBar_paddingLeftSystemWindowInsets, false);
paddingRightSystemWindowInsets =
a.getBoolean(R.styleable.BottomAppBar_paddingRightSystemWindowInsets, false);
fabAlignmentModeEndMargin =
a.getDimensionPixelOffset(
R.styleable.BottomAppBar_fabAlignmentModeEndMargin, NO_FAB_END_MARGIN);
boolean addElevationShadow = a.getBoolean(R.styleable.BottomAppBar_addElevationShadow, true);
a.recycle();
fabOffsetEndMode =
getResources().getDimensionPixelOffset(R.dimen.mtrl_bottomappbar_fabOffsetEndMode);
EdgeTreatment topEdgeTreatment =
new BottomAppBarTopEdgeTreatment(fabCradleMargin, fabCornerRadius, fabVerticalOffset);
ShapeAppearanceModel shapeAppearanceModel =
ShapeAppearanceModel.builder().setTopEdge(topEdgeTreatment).build();
materialShapeDrawable.setShapeAppearanceModel(shapeAppearanceModel);
if (addElevationShadow) {
materialShapeDrawable.setShadowCompatibilityMode(
MaterialShapeDrawable.SHADOW_COMPAT_MODE_ALWAYS);
} else {
materialShapeDrawable.setShadowCompatibilityMode(SHADOW_COMPAT_MODE_NEVER);
if (VERSION.SDK_INT >= VERSION_CODES.P) {
setOutlineAmbientShadowColor(Color.TRANSPARENT);
setOutlineSpotShadowColor(Color.TRANSPARENT);
}
}
materialShapeDrawable.setPaintStyle(Style.FILL);
materialShapeDrawable.initializeElevationOverlay(context);
materialShapeDrawable.setTintList(backgroundTint);
setElevation(elevation);
setBackground(materialShapeDrawable);
ViewUtils.doOnApplyWindowInsets(
this,
attrs,
defStyleAttr,
DEF_STYLE_RES,
new ViewUtils.OnApplyWindowInsetsListener() {
@NonNull
@Override
public WindowInsetsCompat onApplyWindowInsets(
View view,
@NonNull WindowInsetsCompat insets,
@NonNull RelativePadding initialPadding) {
// Just read the insets here. doOnApplyWindowInsets will apply the padding under the
// hood.
boolean leftInsetsChanged = false;
boolean rightInsetsChanged = false;
if (paddingBottomSystemWindowInsets) {
bottomInset = insets.getSystemWindowInsetBottom();
}
if (paddingLeftSystemWindowInsets) {
leftInsetsChanged = leftInset != insets.getSystemWindowInsetLeft();
leftInset = insets.getSystemWindowInsetLeft();
}
if (paddingRightSystemWindowInsets) {
rightInsetsChanged = rightInset != insets.getSystemWindowInsetRight();
rightInset = insets.getSystemWindowInsetRight();
}
// We may need to change the position of the cutout or the action menu if the side
// insets have changed.
if (leftInsetsChanged || rightInsetsChanged) {
cancelAnimations();
setCutoutStateAndTranslateFab();
setActionMenuViewPosition();
}
return insets;
}
});
}
@Override
public void setNavigationIcon(@Nullable Drawable drawable) {
super.setNavigationIcon(maybeTintNavigationIcon(drawable));
}
/**
* Sets the color of the toolbar's navigation icon.
*
* @see #setNavigationIcon
*/
public void setNavigationIconTint(@ColorInt int navigationIconTint) {
this.navigationIconTint = navigationIconTint;
Drawable navigationIcon = getNavigationIcon();
if (navigationIcon != null) {
// Causes navigation icon to be tinted if needed.
setNavigationIcon(navigationIcon);
}
}
/**
* Returns the current fabAlignmentMode, either {@link #FAB_ALIGNMENT_MODE_CENTER} or {@link
* #FAB_ALIGNMENT_MODE_END}.
*/
@FabAlignmentMode
public int getFabAlignmentMode() {
return fabAlignmentMode;
}
/**
* Sets the current {@code fabAlignmentMode}. An animated transition between current and desired
* modes will be played.
*
* @param fabAlignmentMode the desired fabAlignmentMode, either {@link #FAB_ALIGNMENT_MODE_CENTER}
* or {@link #FAB_ALIGNMENT_MODE_END}.
*/
public void setFabAlignmentMode(@FabAlignmentMode int fabAlignmentMode) {
setFabAlignmentModeAndReplaceMenu(fabAlignmentMode, NO_MENU_RES_ID);
}
/**
* Sets the current {@code fabAlignmentMode} and replaces the {@code BottomAppBar}'s menu
* resource. An animated transition between the current and desired mode will be played in
* coordination with a menu resource swap animation.
*
* @param fabAlignmentMode the desired fabAlignmentMode, either {@link #FAB_ALIGNMENT_MODE_CENTER}
* or {@link #FAB_ALIGNMENT_MODE_END}.
* @param newMenu the menu resource of a new menu to be inflated and swapped during the animation.
* Passing 0 for newMenu will not clear the menu but will skip all menu manipulation. If you'd
* like to animate the FAB's alignment and clear the menu at the same time, use {@code
* getMenu().clear()} and {@link #setFabAlignmentMode(int)}.
*/
public void setFabAlignmentModeAndReplaceMenu(
@FabAlignmentMode int fabAlignmentMode, @MenuRes int newMenu) {
this.pendingMenuResId = newMenu;
this.menuAnimatingWithFabAlignmentMode = true;
maybeAnimateMenuView(fabAlignmentMode, fabAttached);
maybeAnimateModeChange(fabAlignmentMode);
this.fabAlignmentMode = fabAlignmentMode;
}
/**
* Returns the current {@code fabAnchorMode}, either {@link #FAB_ANCHOR_MODE_CRADLE} or {@link
* #FAB_ANCHOR_MODE_EMBED}.
*/
@FabAnchorMode
public int getFabAnchorMode() {
return fabAnchorMode;
}
/**
* Sets the current {@code fabAnchorMode}.
*
* @param fabAnchorMode the desired fabAnchorMode, either {@link #FAB_ANCHOR_MODE_CRADLE} or
* {@link #FAB_ANCHOR_MODE_EMBED}.
*/
public void setFabAnchorMode(@FabAnchorMode int fabAnchorMode) {
this.fabAnchorMode = fabAnchorMode;
setCutoutStateAndTranslateFab();
View fab = findDependentView();
if (fab != null) {
updateFabAnchorGravity(this, fab);
fab.requestLayout();
materialShapeDrawable.invalidateSelf();
}
}
private static void updateFabAnchorGravity(BottomAppBar bar, View fab) {
CoordinatorLayout.LayoutParams fabLayoutParams =
(CoordinatorLayout.LayoutParams) fab.getLayoutParams();
fabLayoutParams.anchorGravity = Gravity.CENTER;
if (bar.fabAnchorMode == FAB_ANCHOR_MODE_CRADLE) {
fabLayoutParams.anchorGravity |= Gravity.TOP;
}
if (bar.fabAnchorMode == FAB_ANCHOR_MODE_EMBED) {
fabLayoutParams.anchorGravity |= Gravity.BOTTOM;
}
}
/**
* Returns the current {@code fabAnimationMode}, either {@link #FAB_ANIMATION_MODE_SCALE} or
* {@link #FAB_ANIMATION_MODE_SLIDE}.
*/
@FabAnimationMode
public int getFabAnimationMode() {
return fabAnimationMode;
}
/**
* Sets the current {@code fabAnimationMode}.
*
* @param fabAnimationMode the desired fabAlignmentMode, either {@link #FAB_ALIGNMENT_MODE_CENTER}
* or {@link #FAB_ALIGNMENT_MODE_END}.
*/
public void setFabAnimationMode(@FabAnimationMode int fabAnimationMode) {
this.fabAnimationMode = fabAnimationMode;
}
/**
* Sets the current {@code menuAlignmentMode}. Determines where the menu items in the BottomAppBar
* will be aligned.
*
* @param menuAlignmentMode the desired menuAlignmentMode, either {@link
* #MENU_ALIGNMENT_MODE_AUTO} or {@link #MENU_ALIGNMENT_MODE_START}.
*/
public void setMenuAlignmentMode(@MenuAlignmentMode int menuAlignmentMode) {
if (this.menuAlignmentMode != menuAlignmentMode) {
this.menuAlignmentMode = menuAlignmentMode;
ActionMenuView menu = getActionMenuView();
if (menu != null) {
translateActionMenuView(menu, fabAlignmentMode, isFabVisibleOrWillBeShown());
}
}
}
/**
* Returns the current menuAlignmentMode, either {@link #MENU_ALIGNMENT_MODE_AUTO} or {@link
* #MENU_ALIGNMENT_MODE_START}.
*/
@MenuAlignmentMode
public int getMenuAlignmentMode() {
return menuAlignmentMode;
}
public void setBackgroundTint(@Nullable ColorStateList backgroundTint) {
materialShapeDrawable.setTintList(backgroundTint);
}
@Nullable
public ColorStateList getBackgroundTint() {
return materialShapeDrawable.getTintList();
}
/**
* Returns the cradle margin for the fab cutout. This is the space between the fab and the cutout.
*/
public float getFabCradleMargin() {
return getTopEdgeTreatment().getFabCradleMargin();
}
/**
* Sets the cradle margin for the fab cutout.
*
* This is the space between the fab and the cutout. If
* the fab anchor mode is not cradled, this will not be respected.
*/
public void setFabCradleMargin(@Dimension float cradleMargin) {
if (cradleMargin != getFabCradleMargin()) {
getTopEdgeTreatment().setFabCradleMargin(cradleMargin);
materialShapeDrawable.invalidateSelf();
}
}
/**
* Returns the rounded corner radius for the cutout if it exists. A value of 0 will be a
* sharp edge.
*/
@Dimension
public float getFabCradleRoundedCornerRadius() {
return getTopEdgeTreatment().getFabCradleRoundedCornerRadius();
}
/**
* Sets the rounded corner radius for the fab cutout. A value of 0 will be a sharp edge.
* This will not be visible until there is a cradle.
*/
public void setFabCradleRoundedCornerRadius(@Dimension float roundedCornerRadius) {
if (roundedCornerRadius != getFabCradleRoundedCornerRadius()) {
getTopEdgeTreatment().setFabCradleRoundedCornerRadius(roundedCornerRadius);
materialShapeDrawable.invalidateSelf();
}
}
/**
* Returns the vertical offset for the fab cutout. An offset of 0 indicates the vertical center of
* the {@link FloatingActionButton} is positioned on the top edge.
*/
@Dimension
public float getCradleVerticalOffset() {
return getTopEdgeTreatment().getCradleVerticalOffset();
}
/**
* Sets the vertical offset, in pixels, of the {@link FloatingActionButton} being cradled. An
* offset of 0 indicates the vertical center of the {@link FloatingActionButton} is positioned on
* the top edge.
* This will not be visible until there is a cradle.
*/
public void setCradleVerticalOffset(@Dimension float verticalOffset) {
if (verticalOffset != getCradleVerticalOffset()) {
getTopEdgeTreatment().setCradleVerticalOffset(verticalOffset);
materialShapeDrawable.invalidateSelf();
setCutoutStateAndTranslateFab();
}
}
/**
* Returns the {@link FloatingActionButton} end margin pixel offset for the fab if it is set.
*
* <p>An end margin of -1 indicates that the default margin will be used.
*/
@Px
public int getFabAlignmentModeEndMargin() {
return fabAlignmentModeEndMargin;
}
/**
* Sets the end margin, in pixels, of the {@link FloatingActionButton}. This will only have an
* effect if the fab alignment mode is {@link #FAB_ALIGNMENT_MODE_END}.
*
* <p>An offset of -1 will use the default margin.
*/
public void setFabAlignmentModeEndMargin(@Px int margin) {
if (fabAlignmentModeEndMargin != margin) {
fabAlignmentModeEndMargin = margin;
setCutoutStateAndTranslateFab();
}
}
/**
* Returns true if the {@link BottomAppBar} should hide when a {@link
* androidx.core.view.NestedScrollingChild} is scrolled. This is handled by {@link
* BottomAppBar.Behavior}.
*/
public boolean getHideOnScroll() {
return hideOnScroll;
}
/**
* Sets if the {@link BottomAppBar} should hide when a {@link
* androidx.core.view.NestedScrollingChild} is scrolled. This is handled by {@link
* BottomAppBar.Behavior}.
*/
public void setHideOnScroll(boolean hide) {
hideOnScroll = hide;
}
/** Animates the {@link BottomAppBar} so it hides off the screen. */
public void performHide() {
performHide(/*animate=*/ true);
}
/**
* Hides the {@link BottomAppBar}.
*
* @param animate {@code false} to hide the {@link BottomAppBar} immediately without animation.
*/
public void performHide(boolean animate) {
getBehavior().slideDown(this, animate);
}
/** Animates the {@link BottomAppBar} so it is shown on the screen. */
public void performShow() {
performShow(/*animate=*/ true);
}
/**
* Shows the {@link BottomAppBar}.
*
* @param animate {@code false} to show the {@link BottomAppBar} immediately without animation.
*/
public void performShow(boolean animate) {
getBehavior().slideUp(this, animate);
}
/** Returns true if the {@link BottomAppBar} is scrolled down. */
public boolean isScrolledDown() {
return getBehavior().isScrolledDown();
}
/** Returns true if the {@link BottomAppBar} is scrolled up. */
public boolean isScrolledUp() {
return getBehavior().isScrolledUp();
}
/**
* Add a listener that will be called when the bottom app bar scroll state changes.
* See {@link OnScrollStateChangedListener}.
*
* <p>Components that add a listener should take care to remove it when finished via {@link
* #removeOnScrollStateChangedListener(OnScrollStateChangedListener)}.
*
* @param listener listener to add
*/
public void addOnScrollStateChangedListener(@NonNull OnScrollStateChangedListener listener) {
getBehavior().addOnScrollStateChangedListener(listener);
}
/**
* Remove a listener that was previously added via {@link
* #addOnScrollStateChangedListener(OnScrollStateChangedListener)}.
*
* @param listener listener to remove
*/
public void removeOnScrollStateChangedListener(
@NonNull OnScrollStateChangedListener listener) {
getBehavior().removeOnScrollStateChangedListener(listener);
}
/** Remove all previously added {@link OnScrollStateChangedListener}s. */
public void clearOnScrollStateChangedListeners() {
getBehavior().clearOnScrollStateChangedListeners();
}
@Override
public void setElevation(float elevation) {
materialShapeDrawable.setElevation(elevation);
// Make sure the shadow isn't shown if this view slides down with hideOnScroll.
int topShadowHeight =
materialShapeDrawable.getShadowRadius() - materialShapeDrawable.getShadowOffsetY();
getBehavior().setAdditionalHiddenOffsetY(this, topShadowHeight);
}
/**
* A convenience method to replace the contents of the BottomAppBar's menu.
*
* @param newMenu the desired new menu.
*/
public void replaceMenu(@MenuRes int newMenu) {
if (newMenu != NO_MENU_RES_ID) {
// Clear any pending menu changes if the menu being passed in happens to be pendingMenuResID.
pendingMenuResId = NO_MENU_RES_ID;
getMenu().clear();
inflateMenu(newMenu);
}
}
/** Add a listener to watch for animation changes to the BottomAppBar and FAB */
void addAnimationListener(@NonNull AnimationListener listener) {
if (animationListeners == null) {
animationListeners = new ArrayList<>();
}
animationListeners.add(listener);
}
void removeAnimationListener(@NonNull AnimationListener listener) {
if (animationListeners == null) {
return;
}
animationListeners.remove(listener);
}
private void dispatchAnimationStart() {
if (animatingModeChangeCounter++ == 0 && animationListeners != null) {
// Only dispatch the starting event if there are 0 running animations before this one starts.
for (AnimationListener listener : animationListeners) {
listener.onAnimationStart(this);
}
}
}
private void dispatchAnimationEnd() {
if (--animatingModeChangeCounter == 0 && animationListeners != null) {
// Only dispatch the ending event if there are 0 running animations after this one ends.
for (AnimationListener listener : animationListeners) {
listener.onAnimationEnd(this);
}
}
}
/**
* Sets the fab diameter. This will be called automatically by the {@link BottomAppBar.Behavior}
* if the fab is anchored to this {@link BottomAppBar}..
*/
boolean setFabDiameter(@Px int diameter) {
if (diameter != getTopEdgeTreatment().getFabDiameter()) {
getTopEdgeTreatment().setFabDiameter(diameter);
materialShapeDrawable.invalidateSelf();
return true;
}
return false;
}
void setFabCornerSize(@Dimension float radius) {
if (radius != getTopEdgeTreatment().getFabCornerRadius()) {
getTopEdgeTreatment().setFabCornerSize(radius);
materialShapeDrawable.invalidateSelf();
}
}
private void maybeAnimateModeChange(@FabAlignmentMode int targetMode) {
if (fabAlignmentMode == targetMode || !isLaidOut()) {
return;
}
if (modeAnimator != null) {
modeAnimator.cancel();
}
List<Animator> animators = new ArrayList<>();
if (fabAnimationMode == FAB_ANIMATION_MODE_SLIDE) {
createFabTranslationXAnimation(targetMode, animators);
} else {
createFabDefaultXAnimation(targetMode, animators);
}
AnimatorSet set = new AnimatorSet();
set.playTogether(animators);
set.setInterpolator(
MotionUtils.resolveThemeInterpolator(
getContext(), FAB_ALIGNMENT_ANIM_EASING_ATTR, AnimationUtils.LINEAR_INTERPOLATOR));
modeAnimator = set;
modeAnimator.addListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
dispatchAnimationStart();
}
@Override
public void onAnimationEnd(Animator animation) {
dispatchAnimationEnd();
modeAnimator = null;
}
});
modeAnimator.start();
}
@Nullable
private FloatingActionButton findDependentFab() {
View view = findDependentView();
return view instanceof FloatingActionButton ? (FloatingActionButton) view : null;
}
@Nullable
private View findDependentView() {
if (!(getParent() instanceof CoordinatorLayout)) {
// If we aren't in a CoordinatorLayout we won't have a dependent FAB.
return null;
}
List<View> dependents = ((CoordinatorLayout) getParent()).getDependents(this);
for (View v : dependents) {
if (v instanceof FloatingActionButton || v instanceof ExtendedFloatingActionButton) {
return v;
}
}
return null;
}
private boolean isFabVisibleOrWillBeShown() {
FloatingActionButton fab = findDependentFab();
return fab != null && fab.isOrWillBeShown();
}
/**
* Creates the default animation for moving a fab between alignment modes. Can be overridden by
* extending classes to create a custom animation. Animations that should be executed should be
* added to the animators list. The default animation defined here calls {@link
* FloatingActionButton#hide()} and {@link FloatingActionButton#show()} rather than using custom
* animations.
*/
protected void createFabDefaultXAnimation(
final @FabAlignmentMode int targetMode, List<Animator> animators) {
final FloatingActionButton fab = findDependentFab();
if (fab == null || fab.isOrWillBeHidden()) {
return;
}
dispatchAnimationStart();
fab.hide(
new OnVisibilityChangedListener() {
@Override
public void onHidden(@NonNull FloatingActionButton fab) {
fab.setTranslationX(getFabTranslationX(targetMode));
fab.show(
new OnVisibilityChangedListener() {
@Override
public void onShown(FloatingActionButton fab) {
dispatchAnimationEnd();
}
});
}
});
}
private void createFabTranslationXAnimation(
@FabAlignmentMode int targetMode, @NonNull List<Animator> animators) {
ObjectAnimator animator =
ObjectAnimator.ofFloat(findDependentFab(), "translationX", getFabTranslationX(targetMode));
animator.setDuration(getFabAlignmentAnimationDuration());
animators.add(animator);
}
private int getFabAlignmentAnimationDuration() {
return MotionUtils.resolveThemeDuration(
getContext(), FAB_ALIGNMENT_ANIM_DURATION_ATTR, FAB_ALIGNMENT_ANIM_DURATION_DEFAULT);
}
@Nullable
private Drawable maybeTintNavigationIcon(@Nullable Drawable navigationIcon) {
if (navigationIcon != null && navigationIconTint != null) {
Drawable wrappedNavigationIcon = DrawableCompat.wrap(navigationIcon.mutate());
wrappedNavigationIcon.setTint(navigationIconTint);
return wrappedNavigationIcon;
} else {
return navigationIcon;
}
}
private void maybeAnimateMenuView(@FabAlignmentMode int targetMode, boolean newFabAttached) {
if (!isLaidOut()) {
menuAnimatingWithFabAlignmentMode = false;
// If this method is called before the BottomAppBar is laid out and able to animate, make sure
// the desired menu is still set even if the animation is skipped.
replaceMenu(pendingMenuResId);
return;
}
if (menuAnimator != null) {
menuAnimator.cancel();
}
List<Animator> animators = new ArrayList<>();
// If there's no visible FAB, treat the animation like the FAB is going away.
if (!isFabVisibleOrWillBeShown()) {
targetMode = FAB_ALIGNMENT_MODE_CENTER;
newFabAttached = false;
}
createMenuViewTranslationAnimation(targetMode, newFabAttached, animators);
AnimatorSet set = new AnimatorSet();
set.playTogether(animators);
menuAnimator = set;
menuAnimator.addListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
dispatchAnimationStart();
}
@Override
public void onAnimationEnd(Animator animation) {
dispatchAnimationEnd();
menuAnimatingWithFabAlignmentMode = false;
menuAnimator = null;
}
});
menuAnimator.start();
}
private void createMenuViewTranslationAnimation(
@FabAlignmentMode final int targetMode,
final boolean targetAttached,
@NonNull List<Animator> animators) {
final ActionMenuView actionMenuView = getActionMenuView();
// Stop if there is no action menu view to animate
if (actionMenuView == null) {
return;
}
final float animationDuration = getFabAlignmentAnimationDuration();
Animator fadeIn = ObjectAnimator.ofFloat(actionMenuView, "alpha", 1);
fadeIn.setDuration((long) (animationDuration * (1F - FAB_ALIGNMENT_ANIM_EASING_MIDPOINT)));
float translationXDifference =
actionMenuView.getTranslationX()
- getActionMenuViewTranslationX(actionMenuView, targetMode, targetAttached);
// If the MenuView has moved at least a pixel we will need to animate it.
if (Math.abs(translationXDifference) > 1) {
// We need to fade the MenuView out and in because it's position is changing
Animator fadeOut = ObjectAnimator.ofFloat(actionMenuView, "alpha", 0);
fadeOut.setDuration((long) (animationDuration * FAB_ALIGNMENT_ANIM_EASING_MIDPOINT));
fadeOut.addListener(
new AnimatorListenerAdapter() {
public boolean cancelled;
@Override
public void onAnimationCancel(Animator animation) {
cancelled = true;
}
@Override
public void onAnimationEnd(Animator animation) {
if (!cancelled) {
boolean replaced = pendingMenuResId != NO_MENU_RES_ID;
replaceMenu(pendingMenuResId);
translateActionMenuView(actionMenuView, targetMode, targetAttached, replaced);
}
}
});
AnimatorSet set = new AnimatorSet();
set.playSequentially(fadeOut, fadeIn);
animators.add(set);
} else if (actionMenuView.getAlpha() < 1) {
// If the previous animation was cancelled in the middle and now we're deciding we don't need
// fade the MenuView away and back in, we need to ensure the MenuView is visible
animators.add(fadeIn);
}
}
private float getFabTranslationY() {
if (fabAnchorMode == FAB_ANCHOR_MODE_CRADLE) {
return -getTopEdgeTreatment().getCradleVerticalOffset();
}
View fab = findDependentView();
int translationY = 0;
if (fab != null) {
translationY = -(getMeasuredHeight() + getBottomInset() - fab.getMeasuredHeight()) / 2;
}
return translationY;
}
private float getFabTranslationX(@FabAlignmentMode int fabAlignmentMode) {
boolean isRtl = ViewUtils.isLayoutRtl(this);
if (fabAlignmentMode == FAB_ALIGNMENT_MODE_END) {
View fab = findDependentView();
int systemEndInset = isRtl ? leftInset : rightInset;
int totalEndInset = systemEndInset;
if (fabAlignmentModeEndMargin != NO_FAB_END_MARGIN && fab != null) {
totalEndInset += fab.getMeasuredWidth() / 2 + fabAlignmentModeEndMargin;
} else {
// If no fab end margin is specified, it follows the previous behaviour of
// translating by fabOffsetEndMode instead of a clear-cut margin.
// This will result in a different padding for different FAB sizes.
totalEndInset += fabOffsetEndMode;
}
return (getMeasuredWidth() / 2 - totalEndInset) * (isRtl ? -1 : 1);
} else {
return 0;
}
}
private float getFabTranslationX() {
return getFabTranslationX(fabAlignmentMode);
}
@Nullable
private ActionMenuView getActionMenuView() {
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
if (view instanceof ActionMenuView) {
return (ActionMenuView) view;
}
}
return null;
}
private void translateActionMenuView(
@NonNull final ActionMenuView actionMenuView,
@FabAlignmentMode final int fabAlignmentMode,
final boolean fabAttached) {
translateActionMenuView(actionMenuView, fabAlignmentMode, fabAttached, false);
}
/**
* Translates the ActionMenuView so that it is aligned correctly depending on the fabAlignmentMode
* and if the fab is attached. The view will be translated to the left when the fab is attached
* and on the end. Otherwise it will be in its normal position.
*
* @param actionMenuView the ActionMenuView to translate
* @param fabAlignmentMode the fabAlignmentMode used to determine the position of the
* ActionMenuView
* @param fabAttached whether the ActionMenuView should be moved
*/
private void translateActionMenuView(
@NonNull final ActionMenuView actionMenuView,
@FabAlignmentMode final int fabAlignmentMode,
final boolean fabAttached,
boolean shouldWaitForMenuReplacement) {
Runnable runnable =
new Runnable() {
@Override
public void run() {
actionMenuView.setTranslationX(
getActionMenuViewTranslationX(actionMenuView, fabAlignmentMode, fabAttached));
}
};
if (shouldWaitForMenuReplacement) {
// Wait to ensure the actionMenuView has had it's menu inflated and is able to correctly
// measure it's width before calculating and translating X.
actionMenuView.post(runnable);
} else {
runnable.run();
}
}
/**
* Returns the X translation to position the {@link ActionMenuView}. When {@code fabAlignmentMode}
* is equal to {@link #FAB_ALIGNMENT_MODE_END} and {@code fabAttached} is true, the {@link
* ActionMenuView} will be aligned to the end of the navigation icon, otherwise the {@link
* ActionMenuView} is not moved.
*/
protected int getActionMenuViewTranslationX(
@NonNull ActionMenuView actionMenuView,
@FabAlignmentMode int fabAlignmentMode,
boolean fabAttached) {
if (menuAlignmentMode != MENU_ALIGNMENT_MODE_START
&& (fabAlignmentMode != FAB_ALIGNMENT_MODE_END || !fabAttached)) {
return 0;
}
boolean isRtl = ViewUtils.isLayoutRtl(this);
int toolbarLeftContentEnd = isRtl ? getMeasuredWidth() : 0;
// Calculate the inner side of the Toolbar's Gravity.START contents.
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
boolean isAlignedToStart =
view.getLayoutParams() instanceof LayoutParams
&& (((LayoutParams) view.getLayoutParams()).gravity
& Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK)
== Gravity.START;
if (isAlignedToStart) {
toolbarLeftContentEnd =
isRtl
? Math.min(toolbarLeftContentEnd, view.getLeft())
: Math.max(toolbarLeftContentEnd, view.getRight());
}
}
int actionMenuViewStart = isRtl ? actionMenuView.getRight() : actionMenuView.getLeft();
int systemStartInset = isRtl ? rightInset : -leftInset;
// If there's no navigation icon, we want to add margin since we are translating the menu items
// to the start.
int marginStart = 0;
if (getNavigationIcon() == null) {
int horizontalMargin =
getResources().getDimensionPixelOffset(R.dimen.m3_bottomappbar_horizontal_padding);
marginStart = isRtl ? horizontalMargin : -horizontalMargin;
}
int end = actionMenuViewStart + systemStartInset + marginStart;
return toolbarLeftContentEnd - end;
}
private void cancelAnimations() {
if (menuAnimator != null) {
menuAnimator.cancel();
}
if (modeAnimator != null) {
modeAnimator.cancel();
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
// If the layout hasn't changed this means the position and size hasn't changed so we don't need
// to update the position of the cutout and we can continue any running animations. Otherwise,
// we should stop any animations that might be trying to move things around and reset the
// position of the cutout.
if (changed) {
cancelAnimations();
setCutoutStateAndTranslateFab();
// If the BAB layout has changed, we should alert the fab so that it can
// adjust its margins accordingly.
View dependentView = findDependentView();
if (dependentView != null && dependentView.isLaidOut()) {
dependentView.post(() -> dependentView.requestLayout());
}
}
// Always ensure the MenuView is in the correct position after a layout.
setActionMenuViewPosition();
}
@NonNull
private BottomAppBarTopEdgeTreatment getTopEdgeTreatment() {
return (BottomAppBarTopEdgeTreatment)
materialShapeDrawable.getShapeAppearanceModel().getTopEdge();
}
private void setCutoutStateAndTranslateFab() {
// Layout all elements related to the positioning of the fab.
getTopEdgeTreatment().setHorizontalOffset(getFabTranslationX());
materialShapeDrawable.setInterpolation(
fabAttached && isFabVisibleOrWillBeShown() && fabAnchorMode == FAB_ANCHOR_MODE_CRADLE
? 1
: 0);
View fab = findDependentView();
if (fab != null) {
fab.setTranslationY(getFabTranslationY());
fab.setTranslationX(getFabTranslationX());
}
}
private void setActionMenuViewPosition() {
ActionMenuView actionMenuView = getActionMenuView();
// If the menu is null there is no need to translate it. If the menu is currently being
// animated, the menuAnimator will take care of re-positioning the menu if necessary.
if (actionMenuView != null && menuAnimator == null) {
actionMenuView.setAlpha(1.0f);
if (!isFabVisibleOrWillBeShown()) {
translateActionMenuView(actionMenuView, FAB_ALIGNMENT_MODE_CENTER, false);
} else {
translateActionMenuView(actionMenuView, fabAlignmentMode, fabAttached);
}
}
}
/**
* Ensures that the FAB show and hide animations are linked to this BottomAppBar so it can react
* to changes in the FABs visibility.
*
* @param fab the FAB to link the animations with
*/
private void addFabAnimationListeners(@NonNull FloatingActionButton fab) {
fab.addOnHideAnimationListener(fabAnimationListener);
fab.addOnShowAnimationListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
fabAnimationListener.onAnimationStart(animation);
// Any time the fab is being shown make sure it is in the correct position.
FloatingActionButton fab = findDependentFab();
if (fab != null) {
fab.setTranslationX(getFabTranslationX());
}
}
});
fab.addTransformationCallback(fabTransformationCallback);
}
private int getBottomInset() {
return bottomInset;
}
private int getRightInset() {
return rightInset;
}
private int getLeftInset() {
return leftInset;
}
@Override
public void setTitle(CharSequence title) {
// Don't do anything. BottomAppBar can't have a title.
}
@Override
public void setSubtitle(CharSequence subtitle) {
// Don't do anything. BottomAppBar can't have a subtitle.
}
@NonNull
@Override
public Behavior getBehavior() {
if (behavior == null) {
behavior = new Behavior();
}
return behavior;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
MaterialShapeUtils.setParentAbsoluteElevation(this, materialShapeDrawable);
// Automatically don't clip children for the parent view of BottomAppBar. This allows the shadow
// to be drawn outside the bounds.
if (getParent() instanceof ViewGroup) {
((ViewGroup) getParent()).setClipChildren(false);
}
}
/**
* Behavior designed for use with {@link BottomAppBar} instances. Its main function is to link a
* dependent {@link FloatingActionButton} so that it can be shown docked in the cradle.
*/
public static class Behavior extends HideBottomViewOnScrollBehavior<BottomAppBar> {
@NonNull private final Rect fabContentRect;
private WeakReference<BottomAppBar> viewRef;
private int originalBottomMargin;
private final OnLayoutChangeListener fabLayoutListener =
new OnLayoutChangeListener() {
@Override
public void onLayoutChange(
View v,
int left,
int top,
int right,
int bottom,
int oldLeft,
int oldTop,
int oldRight,
int oldBottom) {
BottomAppBar child = viewRef.get();
// If the child BAB no longer exists, remove the listener.
if (child == null
|| !(v instanceof FloatingActionButton
|| v instanceof ExtendedFloatingActionButton)) {
v.removeOnLayoutChangeListener(this);
return;
}
int height = v.getHeight();
if (v instanceof FloatingActionButton) {
FloatingActionButton fab = ((FloatingActionButton) v);
fab.getMeasuredContentRect(fabContentRect);
height = fabContentRect.height();
// Set the cutout diameter based on the height of the fab.
child.setFabDiameter(height);
// Assume symmetrical corners
float cornerSize =
fab.getShapeAppearanceModel()
.getTopLeftCornerSize()
.getCornerSize(new RectF(fabContentRect));
child.setFabCornerSize(cornerSize);
}
CoordinatorLayout.LayoutParams fabLayoutParams =
(CoordinatorLayout.LayoutParams) v.getLayoutParams();
// Manage the bottomMargin of the fab if it wasn't explicitly set to something. This
// adds space below the fab if the BottomAppBar is hidden.
if (originalBottomMargin == 0) {
// Extra padding is added for the fake shadow on API < 21. Ensure we don't add too
// much space by removing that extra padding if the fab mode is cradle.
if (child.fabAnchorMode == FAB_ANCHOR_MODE_CRADLE) {
int bottomShadowPadding = (v.getMeasuredHeight() - height) / 2;
int bottomMargin =
child
.getResources()
.getDimensionPixelOffset(R.dimen.mtrl_bottomappbar_fab_bottom_margin);
// Should be moved above the bottom insets with space ignoring any shadow padding.
int minBottomMargin = bottomMargin - bottomShadowPadding;
fabLayoutParams.bottomMargin = child.getBottomInset() + minBottomMargin;
}
fabLayoutParams.leftMargin = child.getLeftInset();
fabLayoutParams.rightMargin = child.getRightInset();
boolean isRtl = ViewUtils.isLayoutRtl(v);
if (isRtl) {
fabLayoutParams.leftMargin += child.fabOffsetEndMode;
} else {
fabLayoutParams.rightMargin += child.fabOffsetEndMode;
}
}
child.setCutoutStateAndTranslateFab();
}
};
public Behavior() {
fabContentRect = new Rect();
}
public Behavior(Context context, AttributeSet attrs) {
super(context, attrs);
fabContentRect = new Rect();
}
@Override
public boolean onLayoutChild(
@NonNull CoordinatorLayout parent, @NonNull BottomAppBar child, int layoutDirection) {
viewRef = new WeakReference<>(child);
View dependentView = child.findDependentView();
if (dependentView != null && !dependentView.isLaidOut()) {
// Set the initial position of the FloatingActionButton with the BottomAppBar vertical
// offset.
updateFabAnchorGravity(child, dependentView);
// Keep track of the original bottom margin for the fab. We will manage the margin if
// nothing was set.
CoordinatorLayout.LayoutParams fabLayoutParams =
(CoordinatorLayout.LayoutParams) dependentView.getLayoutParams();
originalBottomMargin = fabLayoutParams.bottomMargin;
if (dependentView instanceof FloatingActionButton) {
FloatingActionButton fab = ((FloatingActionButton) dependentView);
if (child.fabAnchorMode == FAB_ANCHOR_MODE_EMBED && child.removeEmbeddedFabElevation) {
fab.setElevation(0);
fab.setCompatElevation(0);
}
// TODO (b/185233196): Update to use FABs default animator with motion theming.
// If there is no motion spec set on the anchored fab, set one which scales the fab to
// zero so the top edge cutout will be properly animated out when the fab is hidden.
if (fab.getShowMotionSpec() == null) {
fab.setShowMotionSpecResource(R.animator.mtrl_fab_show_motion_spec);
}
if (fab.getHideMotionSpec() == null) {
fab.setHideMotionSpecResource(R.animator.mtrl_fab_hide_motion_spec);
}
// Ensure the FAB is correctly linked to this BAB so the animations can run correctly
child.addFabAnimationListeners(fab);
}
// Always update the BAB if the fab/efab is laid out.
dependentView.addOnLayoutChangeListener(fabLayoutListener);
// Move the fab to the correct position
child.setCutoutStateAndTranslateFab();
}
// Now let the CoordinatorLayout lay out the BAB
parent.onLayoutChild(child, layoutDirection);
return super.onLayoutChild(parent, child, layoutDirection);
}
@Override
public boolean onStartNestedScroll(
@NonNull CoordinatorLayout coordinatorLayout,
@NonNull BottomAppBar child,
@NonNull View directTargetChild,
@NonNull View target,
@ScrollAxis int axes,
@NestedScrollType int type) {
// We will ask to start on nested scroll if the BottomAppBar is set to hide.
return child.getHideOnScroll()
&& super.onStartNestedScroll(
coordinatorLayout, child, directTargetChild, target, axes, type);
}
}
@NonNull
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState savedState = new SavedState(superState);
savedState.fabAlignmentMode = fabAlignmentMode;
savedState.fabAttached = fabAttached;
return savedState;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState savedState = (SavedState) state;
super.onRestoreInstanceState(savedState.getSuperState());
fabAlignmentMode = savedState.fabAlignmentMode;
fabAttached = savedState.fabAttached;
}
static class SavedState extends AbsSavedState {
@FabAlignmentMode int fabAlignmentMode;
boolean fabAttached;
public SavedState(Parcelable superState) {
super(superState);
}
public SavedState(@NonNull Parcel in, ClassLoader loader) {
super(in, loader);
fabAlignmentMode = in.readInt();
fabAttached = in.readInt() != 0;
}
@Override
public void writeToParcel(@NonNull Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(fabAlignmentMode);
out.writeInt(fabAttached ? 1 : 0);
}
public static final Creator<SavedState> CREATOR =
new ClassLoaderCreator<SavedState>() {
@NonNull
@Override
public SavedState createFromParcel(@NonNull Parcel in, ClassLoader loader) {
return new SavedState(in, loader);
}
@Nullable
@Override
public SavedState createFromParcel(@NonNull Parcel in) {
return new SavedState(in, null);
}
@NonNull
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}