cketcham 7c72808f96 Fix FAB vertical offset bug when BAB is hidden
Because the FAB is anchored to the top of the BAB, the animation which hides the BAB also moves down the FAB. This removes the extra animation for the FAB. Because the two animations were running at the same time, the distance between the FAB and the BAB was being calculated incorrectly. This was causing the BAB to think the FAB was higher than it was which resulted in the cutout being drawn too high.

PiperOrigin-RevId: 233844627
2019-02-24 14:13:07 -05:00

948 lines
35 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.internal.ThemeEnforcement.createThemedContext;
import static com.google.android.material.shape.MaterialShapeDrawable.SHADOW_COMPAT_MODE_ALWAYS;
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.animation.TransformationListener;
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.resources.MaterialResources;
import com.google.android.material.shape.EdgeTreatment;
import com.google.android.material.shape.MaterialShapeDrawable;
import com.google.android.material.shape.ShapeAppearanceModel;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.coordinatorlayout.widget.CoordinatorLayout.AttachedBehavior;
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.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 java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
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>We are currently waiting for better attribute support in ColorStateLists to land in the
* support library. https://android-review.googlesource.com/757091. Because we can't fully support
* color theming on all API levels for the BottomAppBar, to use the default theme, we are requiring
* you to opt in for now.
*
* <p>As a workaround to enable correct color theming in your app for API < 23, in addition to
* setting the colorOnSurface attribute in your theme, redefine {@code
* mtrl_on_surface_emphasis_medium} to match with the correct opacity. For example, if you set
* {@code colorOnSurface} in your theme to red (#FF0000). You should redefine {@code
* mtrl_on_surface_emphasis_medium} to be #99FF0000. This sets the color value to be the correct
* color and opacity to match the correct color theming that will be applied on API level 23 and up.
* When the bugs are fixed in the support library you can remove these color definitions.
*
* <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 int fabVerticalOffset;
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;
/**
* Listens to the FABs hide or show animation to kick off an animation on BottomAppBar that reacts
* to the change.
*/
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. */
TransformationListener<FloatingActionButton> fabTransformationListener =
new TransformationListener<FloatingActionButton>() {
@Override
public void onScaleChanged(FloatingActionButton fab) {
materialShapeDrawable.setInterpolation(
fab.getVisibility() == View.VISIBLE ? fab.getScaleY() : 0);
}
@Override
public void onTranslationChanged(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(Context context) {
this(context, null, 0);
}
public BottomAppBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, R.attr.bottomAppBarStyle);
}
public BottomAppBar(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);
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.setShadowVerticalOffset(elevation / 2);
materialShapeDrawable.setPaintStyle(Style.FILL);
setElevation(elevation);
DrawableCompat.setTintList(materialShapeDrawable, backgroundTint);
ViewCompat.setBackground(this, materialShapeDrawable);
}
/**
* 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();
}
}
/**
* 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;
}
@Override
public void setElevation(float elevation) {
materialShapeDrawable.setElevation(elevation);
}
/**
* 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}.
*/
void setFabDiameter(@Px int diameter) {
if (diameter != getTopEdgeTreatment().getFabDiameter()) {
getTopEdgeTreatment().setFabDiameter(diameter);
materialShapeDrawable.invalidateSelf();
}
}
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(FloatingActionButton fab) {
fab.setTranslationX(getFabTranslationX(targetMode));
fab.show(new OnVisibilityChangedListener() {
@Override
public void onShown(FloatingActionButton fab) {
dispatchAnimationEnd();
}
});
}
});
}
private void createFabTranslationXAnimation(
@FabAlignmentMode int targetMode, 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,
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 -fabVerticalOffset;
}
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(
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(
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();
}
}
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());
}
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.addTransformationListener(fabTransformationListener);
}
@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 CoordinatorLayout.Behavior<BottomAppBar> getBehavior() {
if (behavior == null) {
behavior = new Behavior();
}
return behavior;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
// 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> {
private final Rect fabContentRect;
public Behavior() {
fabContentRect = new Rect();
}
public Behavior(Context context, AttributeSet attrs) {
super(context, attrs);
fabContentRect = new Rect();
}
@Override
public boolean onLayoutChild(
CoordinatorLayout parent, BottomAppBar child, int layoutDirection) {
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);
// Ensure the FAB is correctly linked to this BAB so the animations can run correctly
child.addFabAnimationListeners(fab);
// Set the correct cutout diameter
fab.getMeasuredContentRect(fabContentRect);
child.setFabDiameter(fabContentRect.height());
// 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);
fabLayoutParams.bottomMargin = Math.max(0, bottomMargin - bottomShadowPadding);
}
}
// 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);
}
}
@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(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>() {
@Override
public SavedState createFromParcel(Parcel in, ClassLoader loader) {
return new SavedState(in, loader);
}
@Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in, null);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}