mirror of
https://github.com/material-components/material-components-android.git
synced 2026-01-19 19:41:35 +08:00
845 lines
29 KiB
Java
845 lines
29 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 android.animation.Animator;
|
|
import android.animation.AnimatorListenerAdapter;
|
|
import android.animation.AnimatorSet;
|
|
import android.animation.ObjectAnimator;
|
|
import android.animation.ValueAnimator;
|
|
import android.animation.ValueAnimator.AnimatorUpdateListener;
|
|
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 android.support.annotation.Dimension;
|
|
import android.support.annotation.IntDef;
|
|
import android.support.annotation.MenuRes;
|
|
import android.support.annotation.NonNull;
|
|
import android.support.annotation.Nullable;
|
|
import android.support.annotation.Px;
|
|
import com.google.android.material.animation.AnimationUtils;
|
|
import com.google.android.material.behavior.HideBottomViewOnScrollBehavior;
|
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
|
import com.google.android.material.internal.ThemeEnforcement;
|
|
import com.google.android.material.resources.MaterialResources;
|
|
import com.google.android.material.shape.MaterialShapeDrawable;
|
|
import com.google.android.material.shape.ShapePathModel;
|
|
import android.support.design.widget.CoordinatorLayout;
|
|
import android.support.design.widget.CoordinatorLayout.AttachedBehavior;
|
|
import android.support.v4.graphics.drawable.DrawableCompat;
|
|
import android.support.v4.view.AbsSavedState;
|
|
import android.support.v4.view.ViewCompat;
|
|
import android.support.v4.view.ViewCompat.NestedScrollType;
|
|
import android.support.v4.view.ViewCompat.ScrollAxis;
|
|
import android.support.v7.widget.ActionMenuView;
|
|
import android.support.v7.widget.Toolbar;
|
|
import android.util.AttributeSet;
|
|
import android.view.Gravity;
|
|
import android.view.View;
|
|
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>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}.
|
|
*
|
|
* @attr ref com.google.android.material.bottomappbar.R.styleable#BottomAppBar_backgroundTint
|
|
* @attr ref com.google.android.material.bottomappbar.R.styleable#BottomAppBar_fabAlignmentMode
|
|
* @attr ref com.google.android.material.bottomappbar.R.styleable#BottomAppBar_fabCradleMargin
|
|
* @attr ref
|
|
* com.google.android.material.bottomappbar.R.styleable#BottomAppBar_fabCradleRoundedCornerRadius
|
|
* @attr ref com.google.android.material.bottomappbar.R.styleable#BottomAppBar_fabCradleVerticalOffset
|
|
* @attr ref com.google.android.material.bottomappbar.R.styleable#BottomAppBar_hideOnScroll
|
|
*/
|
|
public class BottomAppBar extends Toolbar implements AttachedBehavior {
|
|
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 {}
|
|
|
|
private final int fabOffsetEndMode;
|
|
private final MaterialShapeDrawable materialShapeDrawable;
|
|
private final BottomAppBarTopEdgeTreatment topEdgeTreatment;
|
|
|
|
@Nullable private Animator attachAnimator;
|
|
@Nullable private Animator modeAnimator;
|
|
@Nullable private Animator menuAnimator;
|
|
@FabAlignmentMode private int fabAlignmentMode;
|
|
private boolean hideOnScroll;
|
|
|
|
/**
|
|
* 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;
|
|
|
|
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(context, attrs, defStyleAttr);
|
|
|
|
TypedArray a =
|
|
ThemeEnforcement.obtainStyledAttributes(
|
|
context,
|
|
attrs,
|
|
R.styleable.BottomAppBar,
|
|
defStyleAttr,
|
|
R.style.Widget_MaterialComponents_BottomAppBar);
|
|
|
|
ColorStateList backgroundTint =
|
|
MaterialResources.getColorStateList(context, a, R.styleable.BottomAppBar_backgroundTint);
|
|
|
|
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);
|
|
hideOnScroll = a.getBoolean(R.styleable.BottomAppBar_hideOnScroll, false);
|
|
|
|
a.recycle();
|
|
|
|
fabOffsetEndMode =
|
|
getResources().getDimensionPixelOffset(R.dimen.mtrl_bottomappbar_fabOffsetEndMode);
|
|
|
|
topEdgeTreatment =
|
|
new BottomAppBarTopEdgeTreatment(fabCradleMargin, fabCornerRadius, fabVerticalOffset);
|
|
ShapePathModel appBarModel = new ShapePathModel();
|
|
appBarModel.setTopEdge(topEdgeTreatment);
|
|
materialShapeDrawable = new MaterialShapeDrawable(appBarModel);
|
|
materialShapeDrawable.setShadowEnabled(true);
|
|
materialShapeDrawable.setPaintStyle(Style.FILL);
|
|
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;
|
|
}
|
|
|
|
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 topEdgeTreatment.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()) {
|
|
topEdgeTreatment.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 topEdgeTreatment.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()) {
|
|
topEdgeTreatment.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 topEdgeTreatment.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()) {
|
|
topEdgeTreatment.setCradleVerticalOffset(verticalOffset);
|
|
materialShapeDrawable.invalidateSelf();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if the {@link BottomAppBar} should hide when a {@link
|
|
* android.support.v4.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
|
|
* android.support.v4.view.NestedScrollingChild} is scrolled. This is handled by {@link
|
|
* BottomAppBar.Behavior}.
|
|
*/
|
|
public void setHideOnScroll(boolean hide) {
|
|
hideOnScroll = hide;
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* 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 != topEdgeTreatment.getFabDiameter()) {
|
|
topEdgeTreatment.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<>();
|
|
|
|
createCradleTranslationAnimation(targetMode, animators);
|
|
createFabTranslationXAnimation(targetMode, animators);
|
|
|
|
AnimatorSet set = new AnimatorSet();
|
|
set.playTogether(animators);
|
|
modeAnimator = set;
|
|
modeAnimator.addListener(
|
|
new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
modeAnimator = null;
|
|
}
|
|
});
|
|
modeAnimator.start();
|
|
}
|
|
|
|
private void createCradleTranslationAnimation(
|
|
@FabAlignmentMode int targetMode, List<Animator> animators) {
|
|
if (!fabAttached) {
|
|
return;
|
|
}
|
|
|
|
ValueAnimator animator =
|
|
ValueAnimator.ofFloat(
|
|
topEdgeTreatment.getHorizontalOffset(), getFabTranslationX(targetMode));
|
|
|
|
animator.addUpdateListener(
|
|
new AnimatorUpdateListener() {
|
|
@Override
|
|
public void onAnimationUpdate(ValueAnimator animation) {
|
|
topEdgeTreatment.setHorizontalOffset((Float) animation.getAnimatedValue());
|
|
materialShapeDrawable.invalidateSelf();
|
|
}
|
|
});
|
|
animator.setDuration(ANIMATION_DURATION);
|
|
animators.add(animator);
|
|
}
|
|
|
|
@Nullable
|
|
private FloatingActionButton findDependentFab() {
|
|
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) {
|
|
return (FloatingActionButton) v;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private boolean isVisibleFab() {
|
|
FloatingActionButton fab = findDependentFab();
|
|
return fab != null && fab.isOrWillBeShown();
|
|
}
|
|
|
|
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 (!isVisibleFab()) {
|
|
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 onAnimationEnd(Animator animation) {
|
|
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);
|
|
|
|
if ((fabAttached || (targetAttached && isVisibleFab()))
|
|
&& (fabAlignmentMode == FAB_ALIGNMENT_MODE_END || targetMode == FAB_ALIGNMENT_MODE_END)) {
|
|
// 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 void maybeAnimateAttachChange(boolean targetAttached) {
|
|
if (!ViewCompat.isLaidOut(this)) {
|
|
return;
|
|
}
|
|
|
|
if (attachAnimator != null) {
|
|
attachAnimator.cancel();
|
|
}
|
|
|
|
List<Animator> animators = new ArrayList<>();
|
|
|
|
createCradleShapeAnimation(targetAttached && isVisibleFab(), animators);
|
|
createFabTranslationYAnimation(targetAttached, animators);
|
|
|
|
AnimatorSet set = new AnimatorSet();
|
|
set.playTogether(animators);
|
|
attachAnimator = set;
|
|
attachAnimator.addListener(
|
|
new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
attachAnimator = null;
|
|
}
|
|
});
|
|
attachAnimator.start();
|
|
}
|
|
|
|
private void createCradleShapeAnimation(boolean showCradle, List<Animator> animators) {
|
|
// If we are animating the fab in, set the correct horizontal offset
|
|
if (showCradle) {
|
|
topEdgeTreatment.setHorizontalOffset(getFabTranslationX());
|
|
}
|
|
|
|
ValueAnimator animator =
|
|
ValueAnimator.ofFloat(materialShapeDrawable.getInterpolation(), showCradle ? 1 : 0);
|
|
animator.addUpdateListener(
|
|
new AnimatorUpdateListener() {
|
|
@Override
|
|
public void onAnimationUpdate(ValueAnimator animation) {
|
|
materialShapeDrawable.setInterpolation((Float) animation.getAnimatedValue());
|
|
}
|
|
});
|
|
animator.setDuration(ANIMATION_DURATION);
|
|
animators.add(animator);
|
|
}
|
|
|
|
private void createFabTranslationYAnimation(boolean targetAttached, List<Animator> animators) {
|
|
FloatingActionButton fab = findDependentFab();
|
|
if (fab == null) {
|
|
return;
|
|
}
|
|
|
|
ObjectAnimator animator =
|
|
ObjectAnimator.ofFloat(fab, "translationY", getFabTranslationY(targetAttached));
|
|
animator.setDuration(ANIMATION_DURATION);
|
|
animators.add(animator);
|
|
}
|
|
|
|
private float getFabTranslationY(boolean targetAttached) {
|
|
FloatingActionButton fab = findDependentFab();
|
|
if (fab == null) {
|
|
return 0;
|
|
}
|
|
|
|
// Get the content rect to calculate the amount of padding added with shadow.
|
|
Rect fabContentRect = new Rect();
|
|
fab.getContentRect(fabContentRect);
|
|
|
|
float fabHeight = fabContentRect.height();
|
|
if (fabHeight == 0) {
|
|
// If the fab hasn't been laid out yet, lets look at the measured height.
|
|
fabHeight = fab.getMeasuredHeight();
|
|
}
|
|
float fabBottomShadow = fab.getHeight() - fabContentRect.bottom;
|
|
float fabVerticalShadowPadding = fab.getHeight() - fabContentRect.height();
|
|
|
|
float attached = -getCradleVerticalOffset() + fabHeight / 2 + fabBottomShadow;
|
|
float detached = fabVerticalShadowPadding - fab.getPaddingBottom();
|
|
|
|
return -getMeasuredHeight() + (targetAttached ? attached : detached);
|
|
}
|
|
|
|
private float getFabTranslationY() {
|
|
return getFabTranslationY(fabAttached);
|
|
}
|
|
|
|
private int 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) {
|
|
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;
|
|
actionMenuView.setTranslationX(
|
|
fabAlignmentMode == FAB_ALIGNMENT_MODE_END && fabAttached ? offset : 0);
|
|
}
|
|
|
|
private void cancelAnimations() {
|
|
if (attachAnimator != null) {
|
|
attachAnimator.cancel();
|
|
}
|
|
if (menuAnimator != null) {
|
|
menuAnimator.cancel();
|
|
}
|
|
if (modeAnimator != null) {
|
|
modeAnimator.cancel();
|
|
}
|
|
}
|
|
|
|
private boolean isAnimationRunning() {
|
|
return (attachAnimator != null && attachAnimator.isRunning())
|
|
|| (menuAnimator != null && menuAnimator.isRunning())
|
|
|| (modeAnimator != null && modeAnimator.isRunning());
|
|
}
|
|
|
|
@Override
|
|
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
|
super.onLayout(changed, l, t, r, b);
|
|
|
|
// Stop any animations that might be trying to move things around.
|
|
cancelAnimations();
|
|
|
|
setCutoutState();
|
|
}
|
|
|
|
private void setCutoutState() {
|
|
// Layout all elements related to the positioning of the fab.
|
|
topEdgeTreatment.setHorizontalOffset(getFabTranslationX());
|
|
FloatingActionButton fab = findDependentFab();
|
|
materialShapeDrawable.setInterpolation(fabAttached && isVisibleFab() ? 1 : 0);
|
|
if (fab != null) {
|
|
fab.setTranslationY(getFabTranslationY());
|
|
fab.setTranslationX(getFabTranslationX());
|
|
}
|
|
ActionMenuView actionMenuView = getActionMenuView();
|
|
if (actionMenuView != null) {
|
|
actionMenuView.setAlpha(1.0f);
|
|
if (!isVisibleFab()) {
|
|
translateActionMenuView(actionMenuView, FAB_ALIGNMENT_MODE_CENTER, false);
|
|
} else {
|
|
translateActionMenuView(actionMenuView, fabAlignmentMode, fabAttached);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
maybeAnimateAttachChange(fabAttached);
|
|
maybeAnimateMenuView(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) {
|
|
removeFabAnimationListeners(fab);
|
|
fab.addOnHideAnimationListener(fabAnimationListener);
|
|
fab.addOnShowAnimationListener(fabAnimationListener);
|
|
}
|
|
|
|
private void removeFabAnimationListeners(@NonNull FloatingActionButton fab) {
|
|
fab.removeOnHideAnimationListener(fabAnimationListener);
|
|
fab.removeOnShowAnimationListener(fabAnimationListener);
|
|
}
|
|
|
|
@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() {
|
|
return new Behavior();
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
|
|
/** Default constructor for instantiating this Behavior. */
|
|
public Behavior() {
|
|
fabContentRect = new Rect();
|
|
}
|
|
|
|
/**
|
|
* Default constructor for inflating this Behavior from layout.
|
|
*
|
|
* @param context The {@link Context}.
|
|
* @param attrs The {@link AttributeSet}.
|
|
*/
|
|
public Behavior(Context context, AttributeSet attrs) {
|
|
super(context, attrs);
|
|
fabContentRect = new Rect();
|
|
}
|
|
|
|
private boolean updateFabPositionAndVisibility(FloatingActionButton fab, BottomAppBar child) {
|
|
// Set the initial position of the FloatingActionButton with the BottomAppBar vertical offset.
|
|
CoordinatorLayout.LayoutParams fabLayoutParams =
|
|
(CoordinatorLayout.LayoutParams) fab.getLayoutParams();
|
|
fabLayoutParams.anchorGravity = Gravity.CENTER;
|
|
|
|
// Ensure the FAB is correctly linked to this BAB so the animations can run correctly
|
|
child.addFabAnimationListeners(fab);
|
|
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean onLayoutChild(
|
|
CoordinatorLayout parent, BottomAppBar child, int layoutDirection) {
|
|
FloatingActionButton fab = child.findDependentFab();
|
|
if (fab != null) {
|
|
updateFabPositionAndVisibility(fab, child);
|
|
fab.getMeasuredContentRect(fabContentRect);
|
|
child.setFabDiameter(fabContentRect.height());
|
|
}
|
|
|
|
// If an animation is running, it should update the cutout to match the FAB, so don't do
|
|
// anything here.
|
|
if (!child.isAnimationRunning()) {
|
|
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 void slideUp(BottomAppBar child) {
|
|
super.slideUp(child);
|
|
FloatingActionButton fab = child.findDependentFab();
|
|
if (fab != null) {
|
|
fab.clearAnimation();
|
|
fab.animate()
|
|
.translationY(child.getFabTranslationY())
|
|
.setInterpolator(AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR)
|
|
.setDuration(ENTER_ANIMATION_DURATION);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void slideDown(BottomAppBar child) {
|
|
super.slideDown(child);
|
|
FloatingActionButton fab = child.findDependentFab();
|
|
if (fab != null) {
|
|
fab.getContentRect(fabContentRect);
|
|
float fabShadowPadding = fab.getMeasuredHeight() - fabContentRect.height();
|
|
|
|
fab.clearAnimation();
|
|
fab.animate()
|
|
.translationY(-fab.getPaddingBottom() + fabShadowPadding)
|
|
.setInterpolator(AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR)
|
|
.setDuration(EXIT_ANIMATION_DURATION);
|
|
}
|
|
}
|
|
}
|
|
|
|
@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];
|
|
}
|
|
};
|
|
}
|
|
}
|