marianomartin 0d4515cb46 Internal cleanup
PiperOrigin-RevId: 265971745
2019-08-29 14:59:20 -04:00

1044 lines
37 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 com.google.android.material.shape.MaterialShapeDrawable.SHADOW_COMPAT_MODE_ALWAYS;
import static com.google.android.material.internal.ThemeEnforcement.createThemedContext;
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.Paint.Style;
import android.graphics.Rect;
import android.os.Parcel;
import android.os.Parcelable;
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 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 androidx.core.graphics.drawable.DrawableCompat;
import androidx.customview.view.AbsSavedState;
import androidx.core.view.ViewCompat;
import androidx.core.view.ViewCompat.NestedScrollType;
import androidx.core.view.ViewCompat.ScrollAxis;
import androidx.core.view.WindowInsetsCompat;
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.coordinatorlayout.widget.CoordinatorLayout;
import androidx.coordinatorlayout.widget.CoordinatorLayout.AttachedBehavior;
import com.google.android.material.animation.TransformationCallback;
import com.google.android.material.behavior.HideBottomViewOnScrollBehavior;
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.resources.MaterialResources;
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}.
*
* @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_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
*/
public class BottomAppBar extends Toolbar implements AttachedBehavior {
private static final int DEF_STYLE_RES = R.style.Widget_MaterialComponents_BottomAppBar;
private static final long ANIMATION_DURATION = 300;
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 {}
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 {}
private final int fabOffsetEndMode;
private final MaterialShapeDrawable materialShapeDrawable = new MaterialShapeDrawable();
@Nullable private Animator modeAnimator;
@Nullable private Animator menuAnimator;
@FabAlignmentMode private int fabAlignmentMode;
@FabAnimationMode private int fabAnimationMode;
private boolean hideOnScroll;
/** Keeps track of the number of currently running animations. */
private int animatingModeChangeCounter = 0;
private ArrayList<AnimationListener> animationListeners;
/** 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;
/**
* 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) {
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 ? fab.getScaleY() : 0);
}
@Override
public void onTranslationChanged(@NonNull FloatingActionButton fab) {
float horizontalOffset = fab.getTranslationX();
if (getTopEdgeTreatment().getHorizontalOffset() != horizontalOffset) {
getTopEdgeTreatment().setHorizontalOffset(horizontalOffset);
materialShapeDrawable.invalidateSelf();
}
float verticalOffset = -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, 0);
}
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(createThemedContext(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);
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);
hideOnScroll = a.getBoolean(R.styleable.BottomAppBar_hideOnScroll, false);
a.recycle();
fabOffsetEndMode =
getResources().getDimensionPixelOffset(R.dimen.mtrl_bottomappbar_fabOffsetEndMode);
EdgeTreatment topEdgeTreatment =
new BottomAppBarTopEdgeTreatment(fabCradleMargin, fabCornerRadius, fabVerticalOffset);
ShapeAppearanceModel appBarModel = materialShapeDrawable.getShapeAppearanceModel();
appBarModel.setTopEdge(topEdgeTreatment);
materialShapeDrawable.setShadowCompatibilityMode(SHADOW_COMPAT_MODE_ALWAYS);
materialShapeDrawable.setPaintStyle(Style.FILL);
materialShapeDrawable.initializeElevationOverlay(context);
setElevation(elevation);
DrawableCompat.setTintList(materialShapeDrawable, backgroundTint);
ViewCompat.setBackground(this, materialShapeDrawable);
ViewUtils.doOnApplyWindowInsets(
this,
new ViewUtils.OnApplyWindowInsetsListener() {
@NonNull
@Override
public WindowInsetsCompat onApplyWindowInsets(
View view,
@NonNull WindowInsetsCompat insets,
@NonNull RelativePadding initialPadding) {
bottomInset = insets.getSystemWindowInsetBottom();
initialPadding.bottom += insets.getSystemWindowInsetBottom();
initialPadding.applyToView(view);
return insets;
}
});
}
/**
* 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 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) {
maybeAnimateModeChange(fabAlignmentMode);
maybeAnimateMenuView(fabAlignmentMode, fabAttached);
this.fabAlignmentMode = fabAlignmentMode;
}
/**
* Returns the current fabAlignmentMode, either {@link #FAB_ANIMATION_MODE_SCALE} or {@link
* #FAB_ANIMATION_MODE_SLIDE}.
*/
@FabAnimationMode
public int getFabAnimationMode() {
return fabAnimationMode;
}
/**
* Sets the current fabAlignmentMode. Determines which animation will be played when the fab is
* animated from from one {@link FabAlignmentMode} to another.
*
* @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;
}
public void setBackgroundTint(@Nullable ColorStateList backgroundTint) {
DrawableCompat.setTintList(materialShapeDrawable, 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.
*/
public void setFabCradleMargin(@Dimension float cradleMargin) {
if (cradleMargin != getFabCradleMargin()) {
getTopEdgeTreatment().setFabCradleMargin(cradleMargin);
materialShapeDrawable.invalidateSelf();
}
}
/** Returns the rounded corner radius for the cutout. 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. */
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.
*/
public void setCradleVerticalOffset(@Dimension float verticalOffset) {
if (verticalOffset != getCradleVerticalOffset()) {
getTopEdgeTreatment().setCradleVerticalOffset(verticalOffset);
materialShapeDrawable.invalidateSelf();
setCutoutState();
}
}
/**
* 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() {
getBehavior().slideDown(this);
}
/** Animates the {@link BottomAppBar} so it is shown on the screen. */
public void performShow() {
getBehavior().slideUp(this);
}
@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) {
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;
}
private void maybeAnimateModeChange(@FabAlignmentMode int targetMode) {
if (fabAlignmentMode == targetMode || !ViewCompat.isLaidOut(this)) {
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);
modeAnimator = set;
modeAnimator.addListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
dispatchAnimationStart();
}
@Override
public void onAnimationEnd(Animator animation) {
dispatchAnimationEnd();
}
});
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(ANIMATION_DURATION);
animators.add(animator);
}
private void maybeAnimateMenuView(@FabAlignmentMode int targetMode, boolean newFabAttached) {
if (!ViewCompat.isLaidOut(this)) {
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();
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;
}
Animator fadeIn = ObjectAnimator.ofFloat(actionMenuView, "alpha", 1);
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.addListener(
new AnimatorListenerAdapter() {
public boolean cancelled;
@Override
public void onAnimationCancel(Animator animation) {
cancelled = true;
}
@Override
public void onAnimationEnd(Animator animation) {
if (!cancelled) {
translateActionMenuView(actionMenuView, targetMode, targetAttached);
}
}
});
AnimatorSet set = new AnimatorSet();
set.setDuration(ANIMATION_DURATION / 2);
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() {
return -getTopEdgeTreatment().getCradleVerticalOffset();
}
private float getFabTranslationX(@FabAlignmentMode int fabAlignmentMode) {
boolean isRtl = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL;
return fabAlignmentMode == FAB_ALIGNMENT_MODE_END
? (getMeasuredWidth() / 2 - fabOffsetEndMode) * (isRtl ? -1 : 1)
: 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;
}
/**
* 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 ActionMenuView actionMenuView,
@FabAlignmentMode int fabAlignmentMode,
boolean fabAttached) {
actionMenuView.setTranslationX(
getActionMenuViewTranslationX(actionMenuView, fabAlignmentMode, fabAttached));
}
/**
* 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) {
int toolbarLeftContentEnd = 0;
boolean isRtl = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL;
// 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 Toolbar.LayoutParams
&& (((Toolbar.LayoutParams) view.getLayoutParams()).gravity
& Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK)
== Gravity.START;
if (isAlignedToStart) {
toolbarLeftContentEnd =
Math.max(toolbarLeftContentEnd, isRtl ? view.getLeft() : view.getRight());
}
}
int end = isRtl ? actionMenuView.getRight() : actionMenuView.getLeft();
int offset = toolbarLeftContentEnd - end;
return fabAlignmentMode == FAB_ALIGNMENT_MODE_END && fabAttached ? offset : 0;
}
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();
setCutoutState();
}
// Always ensure the MenuView is in the correct position after a layout.
setActionMenuViewPosition();
}
@NonNull
private BottomAppBarTopEdgeTreatment getTopEdgeTreatment() {
return (BottomAppBarTopEdgeTreatment)
materialShapeDrawable.getShapeAppearanceModel().getTopEdge();
}
private void setCutoutState() {
// Layout all elements related to the positioning of the fab.
getTopEdgeTreatment().setHorizontalOffset(getFabTranslationX());
View fab = findDependentView();
materialShapeDrawable.setInterpolation(fabAttached && isFabVisibleOrWillBeShown() ? 1 : 0);
if (fab != null) {
fab.setTranslationY(getFabTranslationY());
fab.setTranslationX(getFabTranslationX());
}
}
private void setActionMenuViewPosition() {
ActionMenuView actionMenuView = getActionMenuView();
if (actionMenuView != 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;
}
@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 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.removeOnLayoutChangeListener(this);
return;
}
FloatingActionButton fab = ((FloatingActionButton) v);
fab.getMeasuredContentRect(fabContentRect);
int height = fabContentRect.height();
// Set the cutout diameter based on the height of the fab.
if (!child.setFabDiameter(height)) {
// The size of the fab didn't change so return early.
return;
}
CoordinatorLayout.LayoutParams fabLayoutParams =
(CoordinatorLayout.LayoutParams) v.getLayoutParams();
// Set the bottomMargin of the fab if it is 0dp. This adds space below the fab if the
// BottomAppBar is hidden.
if (fabLayoutParams.bottomMargin == 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.
int bottomShadowPadding = (fab.getMeasuredHeight() - fabContentRect.height()) / 2;
int bottomMargin =
child
.getResources()
.getDimensionPixelOffset(R.dimen.mtrl_bottomappbar_fab_bottom_margin);
// At least be tall enough to be above the bottom insets, otherwise set some space
// ignoring any shadow padding.
fabLayoutParams.bottomMargin =
Math.max(child.getBottomInset(), bottomMargin - bottomShadowPadding);
}
}
};
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 && !ViewCompat.isLaidOut(dependentView)) {
// Set the initial position of the FloatingActionButton with the BottomAppBar vertical
// offset.
CoordinatorLayout.LayoutParams fabLayoutParams =
(CoordinatorLayout.LayoutParams) dependentView.getLayoutParams();
fabLayoutParams.anchorGravity = Gravity.CENTER | Gravity.TOP;
if (dependentView instanceof FloatingActionButton) {
FloatingActionButton fab = ((FloatingActionButton) dependentView);
// Always update the BAB if the fab is laid out.
fab.addOnLayoutChangeListener(fabLayoutListener);
// Ensure the FAB is correctly linked to this BAB so the animations can run correctly
child.addFabAnimationListeners(fab);
}
// Move the fab to the correct position
child.setCutoutState();
}
// 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];
}
};
}
}