diff --git a/catalog/java/io/material/catalog/bottomappbar/BottomAppBarMainDemoFragment.java b/catalog/java/io/material/catalog/bottomappbar/BottomAppBarMainDemoFragment.java
index e66fbc429bc..9f83f6ee67a 100644
--- a/catalog/java/io/material/catalog/bottomappbar/BottomAppBarMainDemoFragment.java
+++ b/catalog/java/io/material/catalog/bottomappbar/BottomAppBarMainDemoFragment.java
@@ -152,10 +152,9 @@ private void setUpDemoControls(@NonNull View view) {
}
centerButton.setOnClickListener(
- v -> {
- bar.setFabAlignmentModeAndReplaceMenu(
- BottomAppBar.FAB_ALIGNMENT_MODE_CENTER, R.menu.demo_primary);
- });
+ v ->
+ bar.setFabAlignmentModeAndReplaceMenu(
+ BottomAppBar.FAB_ALIGNMENT_MODE_CENTER, R.menu.demo_primary));
endButton.setOnClickListener(
v ->
bar.setFabAlignmentModeAndReplaceMenu(
@@ -176,6 +175,21 @@ private void setUpDemoControls(@NonNull View view) {
slideButton.setOnClickListener(
v -> bar.setFabAnimationMode(BottomAppBar.FAB_ANIMATION_MODE_SLIDE));
+ // Set up FAB anchor mode toggle buttons.
+ MaterialButton embedButton = view.findViewById(R.id.fab_anchor_mode_button_embed);
+ MaterialButton cradleButton = view.findViewById(R.id.fab_anchor_mode_button_cradle);
+
+ if (bar.getFabAnchorMode() == BottomAppBar.FAB_ANCHOR_MODE_EMBED) {
+ embedButton.setChecked(true);
+ } else {
+ cradleButton.setChecked(true);
+ }
+
+ embedButton.setOnClickListener(
+ v -> bar.setFabAnchorMode(BottomAppBar.FAB_ANCHOR_MODE_EMBED));
+ cradleButton.setOnClickListener(
+ v -> bar.setFabAnchorMode(BottomAppBar.FAB_ANCHOR_MODE_CRADLE));
+
// Set up hide on scroll switch.
MaterialSwitch barScrollSwitch = view.findViewById(R.id.bar_scroll_switch);
barScrollSwitch.setChecked(bar.getHideOnScroll());
diff --git a/catalog/java/io/material/catalog/bottomappbar/res/layout/cat_bottomappbar_content.xml b/catalog/java/io/material/catalog/bottomappbar/res/layout/cat_bottomappbar_content.xml
index c88175ac865..79542c4bccc 100644
--- a/catalog/java/io/material/catalog/bottomappbar/res/layout/cat_bottomappbar_content.xml
+++ b/catalog/java/io/material/catalog/bottomappbar/res/layout/cat_bottomappbar_content.xml
@@ -108,6 +108,29 @@
android:text="@string/cat_bottomappbar_fab_hide" />
+
+
+
+
+
+
Fab Alignment Mode
Fab Animation ModeFab Visibility Mode
+ Fab Anchor Mode
+ Embed
+ Cradle
diff --git a/docs/components/BottomAppBar.md b/docs/components/BottomAppBar.md
index 1d7f78379a8..c6e8218c870 100644
--- a/docs/components/BottomAppBar.md
+++ b/docs/components/BottomAppBar.md
@@ -234,6 +234,7 @@ Element | Attribute | Related
-------------------------------- | ---------------------------------- | ---------------------------------------------------------------------- | -------------
**Alignment mode** | `app:fabAlignmentMode` | `setFabAlignmentMode` `getFabAlignmentMode` | `center`
**Animation mode** | `app:fabAnimationMode` | `setFabAnimationMode` `getFabAnimationMode` | `slide`
+**Anchor mode** | `app:fabAnchorMode` | `setFabAnchorMode` `getFabAnchorMode` | `cradle`
**Cradle margin** | `app:fabCradleMargin` | `setFabCradleMargin` `getFabCradleMargin` | `6dp`
**Cradle rounded corner radius** | `app:fabCradleRoundedCornerRadius` | `setFabCradleRoundedCornerRadius` `getFabCradleRoundedCornerRadius` | `4dp`
**Cradle vertical offset** | `app:fabCradleVerticalOffset` | `setCradleVerticalOffset` `getCradleVerticalOffset` | `12dp`
diff --git a/lib/java/com/google/android/material/bottomappbar/BottomAppBar.java b/lib/java/com/google/android/material/bottomappbar/BottomAppBar.java
index e9a79e8750b..46abfe84ae5 100644
--- a/lib/java/com/google/android/material/bottomappbar/BottomAppBar.java
+++ b/lib/java/com/google/android/material/bottomappbar/BottomAppBar.java
@@ -18,6 +18,7 @@
import com.google.android.material.R;
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static com.google.android.material.shape.MaterialShapeDrawable.SHADOW_COMPAT_MODE_ALWAYS;
import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap;
@@ -47,6 +48,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
+import androidx.annotation.RestrictTo;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.coordinatorlayout.widget.CoordinatorLayout.AttachedBehavior;
import androidx.core.graphics.drawable.DrawableCompat;
@@ -104,6 +106,7 @@
*
* @attr ref com.google.android.material.R.styleable#BottomAppBar_backgroundTint
* @attr ref com.google.android.material.R.styleable#BottomAppBar_fabAlignmentMode
+ * @attr ref com.google.android.material.R.styleable#BottomAppBar_fabAnchorMode
* @attr ref com.google.android.material.R.styleable#BottomAppBar_fabAnimationMode
* @attr ref com.google.android.material.R.styleable#BottomAppBar_fabCradleMargin
* @attr ref com.google.android.material.R.styleable#BottomAppBar_fabCradleRoundedCornerRadius
@@ -132,6 +135,22 @@ public class BottomAppBar extends Toolbar implements AttachedBehavior {
@Retention(RetentionPolicy.SOURCE)
public @interface FabAlignmentMode {}
+ /** The FAB is embedded inside the BottomAppBar. */
+ public static final int FAB_ANCHOR_MODE_EMBED = 0;
+ /** The FAB is cradled at the top of the BottomAppBar. */
+ public static final int FAB_ANCHOR_MODE_CRADLE = 1;
+
+ /**
+ * The fabAnchorMode determines the placement of the FAB within the BottomAppBar. The FAB can be
+ * cradled at the top of the BottomAppBar, or embedded within it.
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef({FAB_ANCHOR_MODE_EMBED, FAB_ANCHOR_MODE_CRADLE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FabAnchorMode {}
+
public static final int FAB_ANIMATION_MODE_SCALE = 0;
public static final int FAB_ANIMATION_MODE_SLIDE = 1;
@@ -153,6 +172,7 @@ public class BottomAppBar extends Toolbar implements AttachedBehavior {
@Nullable private Animator menuAnimator;
@FabAlignmentMode private int fabAlignmentMode;
@FabAnimationMode private int fabAnimationMode;
+ @FabAnchorMode private int fabAnchorMode;
private boolean hideOnScroll;
private final boolean paddingBottomSystemWindowInsets;
private final boolean paddingLeftSystemWindowInsets;
@@ -220,11 +240,16 @@ public void onAnimationStart(Animator animation) {
@Override
public void onScaleChanged(@NonNull FloatingActionButton fab) {
materialShapeDrawable.setInterpolation(
- fab.getVisibility() == View.VISIBLE ? fab.getScaleY() : 0);
+ fab.getVisibility() == View.VISIBLE && fabAnchorMode == FAB_ANCHOR_MODE_CRADLE
+ ? fab.getScaleY()
+ : 0);
}
@Override
public void onTranslationChanged(@NonNull FloatingActionButton fab) {
+ if (fabAnchorMode != FAB_ANCHOR_MODE_CRADLE) {
+ return;
+ }
float horizontalOffset = fab.getTranslationX();
if (getTopEdgeTreatment().getHorizontalOffset() != horizontalOffset) {
getTopEdgeTreatment().setHorizontalOffset(horizontalOffset);
@@ -276,6 +301,7 @@ public BottomAppBar(@NonNull Context context, @Nullable AttributeSet attrs, int
a.getInt(R.styleable.BottomAppBar_fabAlignmentMode, FAB_ALIGNMENT_MODE_CENTER);
fabAnimationMode =
a.getInt(R.styleable.BottomAppBar_fabAnimationMode, FAB_ANIMATION_MODE_SCALE);
+ fabAnchorMode = a.getInt(R.styleable.BottomAppBar_fabAnchorMode, FAB_ANCHOR_MODE_CRADLE);
hideOnScroll = a.getBoolean(R.styleable.BottomAppBar_hideOnScroll, false);
// Reading out if we are handling bottom padding, so we can apply it to the FAB.
paddingBottomSystemWindowInsets =
@@ -335,7 +361,7 @@ public WindowInsetsCompat onApplyWindowInsets(
if (leftInsetsChanged || rightInsetsChanged) {
cancelAnimations();
- setCutoutState();
+ setCutoutStateAndTranslateFab();
setActionMenuViewPosition();
}
@@ -405,8 +431,43 @@ public void setFabAlignmentModeAndReplaceMenu(
}
/**
- * Returns the current fabAlignmentMode, either {@link #FAB_ANIMATION_MODE_SCALE} or {@link
- * #FAB_ANIMATION_MODE_SLIDE}.
+ * Returns the current {@code fabAnchorMode}, either {@link #FAB_ANCHOR_MODE_CRADLE} or {@link
+ * #FAB_ANCHOR_MODE_EMBED}.
+ */
+ @FabAnchorMode
+ public int getFabAnchorMode() {
+ return fabAnchorMode;
+ }
+
+ /**
+ * Sets the current {@code fabAnchorMode}.
+ *
+ * @param fabAnchorMode the desired fabAnchorMode, either {@link #FAB_ANCHOR_MODE_CRADLE} or
+ * {@link #FAB_ANCHOR_MODE_EMBED}.
+ */
+ public void setFabAnchorMode(@FabAnchorMode int fabAnchorMode) {
+ this.fabAnchorMode = fabAnchorMode;
+ setCutoutStateAndTranslateFab();
+ View fab = findDependentView();
+ if (fab != null) {
+ updateFabAnchorGravity(this, fab);
+ fab.requestLayout();
+ materialShapeDrawable.invalidateSelf();
+ }
+ }
+
+ private static void updateFabAnchorGravity(BottomAppBar bar, View fab) {
+ CoordinatorLayout.LayoutParams fabLayoutParams =
+ (CoordinatorLayout.LayoutParams) fab.getLayoutParams();
+ fabLayoutParams.anchorGravity = Gravity.CENTER;
+ if (bar.fabAnchorMode == FAB_ANCHOR_MODE_CRADLE) {
+ fabLayoutParams.anchorGravity |= Gravity.TOP;
+ }
+ }
+
+ /**
+ * Returns the current {@code fabAnimationMode}, either {@link #FAB_ANIMATION_MODE_SCALE} or
+ * {@link #FAB_ANIMATION_MODE_SLIDE}.
*/
@FabAnimationMode
public int getFabAnimationMode() {
@@ -414,8 +475,7 @@ public int getFabAnimationMode() {
}
/**
- * Sets the current fabAlignmentMode. Determines which animation will be played when the fab is
- * animated from from one {@link FabAlignmentMode} to another.
+ * Sets the current {@code fabAnimationMode}.
*
* @param fabAnimationMode the desired fabAlignmentMode, either {@link #FAB_ALIGNMENT_MODE_CENTER}
* or {@link #FAB_ALIGNMENT_MODE_END}.
@@ -441,7 +501,10 @@ public float getFabCradleMargin() {
}
/**
- * Sets the cradle margin for the fab cutout. This is the space between the fab and the cutout.
+ * Sets the cradle margin for the fab cutout.
+ *
+ * This is the space between the fab and the cutout. If
+ * the fab anchor mode is not cradled, this will not be respected.
*/
public void setFabCradleMargin(@Dimension float cradleMargin) {
if (cradleMargin != getFabCradleMargin()) {
@@ -450,13 +513,19 @@ public void setFabCradleMargin(@Dimension float cradleMargin) {
}
}
- /** Returns the rounded corner radius for the cutout. A value of 0 will be a sharp edge. */
+ /**
+ * Returns the rounded corner radius for the cutout if it exists. A value of 0 will be a
+ * sharp edge.
+ */
@Dimension
public float getFabCradleRoundedCornerRadius() {
return getTopEdgeTreatment().getFabCradleRoundedCornerRadius();
}
- /** Sets the rounded corner radius for the fab cutout. A value of 0 will be a sharp edge. */
+ /**
+ * Sets the rounded corner radius for the fab cutout. A value of 0 will be a sharp edge.
+ * This will not be visible until there is a cradle.
+ */
public void setFabCradleRoundedCornerRadius(@Dimension float roundedCornerRadius) {
if (roundedCornerRadius != getFabCradleRoundedCornerRadius()) {
getTopEdgeTreatment().setFabCradleRoundedCornerRadius(roundedCornerRadius);
@@ -477,12 +546,13 @@ public float getCradleVerticalOffset() {
* Sets the vertical offset, in pixels, of the {@link FloatingActionButton} being cradled. An
* offset of 0 indicates the vertical center of the {@link FloatingActionButton} is positioned on
* the top edge.
+ * This will not be visible until there is a cradle.
*/
public void setCradleVerticalOffset(@Dimension float verticalOffset) {
if (verticalOffset != getCradleVerticalOffset()) {
getTopEdgeTreatment().setCradleVerticalOffset(verticalOffset);
materialShapeDrawable.invalidateSelf();
- setCutoutState();
+ setCutoutStateAndTranslateFab();
}
}
@@ -629,7 +699,7 @@ private void dispatchAnimationEnd() {
/**
* Sets the fab diameter. This will be called automatically by the {@link BottomAppBar.Behavior}
- * if the fab is anchored to this {@link BottomAppBar}.
+ * if the fab is anchored to this {@link BottomAppBar}..
*/
boolean setFabDiameter(@Px int diameter) {
if (diameter != getTopEdgeTreatment().getFabDiameter()) {
@@ -871,7 +941,10 @@ public void onAnimationEnd(Animator animation) {
}
private float getFabTranslationY() {
- return -getTopEdgeTreatment().getCradleVerticalOffset();
+ if (fabAnchorMode == FAB_ANCHOR_MODE_CRADLE) {
+ return -getTopEdgeTreatment().getCradleVerticalOffset();
+ }
+ return 0;
}
private float getFabTranslationX(@FabAlignmentMode int fabAlignmentMode) {
@@ -1000,7 +1073,7 @@ protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
cancelAnimations();
- setCutoutState();
+ setCutoutStateAndTranslateFab();
}
// Always ensure the MenuView is in the correct position after a layout.
@@ -1013,11 +1086,14 @@ private BottomAppBarTopEdgeTreatment getTopEdgeTreatment() {
materialShapeDrawable.getShapeAppearanceModel().getTopEdge();
}
- private void setCutoutState() {
+ private void setCutoutStateAndTranslateFab() {
// Layout all elements related to the positioning of the fab.
getTopEdgeTreatment().setHorizontalOffset(getFabTranslationX());
+ materialShapeDrawable.setInterpolation(
+ fabAttached && isFabVisibleOrWillBeShown() && fabAnchorMode == FAB_ANCHOR_MODE_CRADLE
+ ? 1
+ : 0);
View fab = findDependentView();
- materialShapeDrawable.setInterpolation(fabAttached && isFabVisibleOrWillBeShown() ? 1 : 0);
if (fab != null) {
fab.setTranslationY(getFabTranslationY());
fab.setTranslationX(getFabTranslationX());
@@ -1164,10 +1240,12 @@ public void onLayoutChange(
// 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() - height) / 2;
- int bottomMargin =
- child
- .getResources()
- .getDimensionPixelOffset(R.dimen.mtrl_bottomappbar_fab_bottom_margin);
+ int bottomMargin = 0;
+ if (child.fabAnchorMode == FAB_ANCHOR_MODE_CRADLE) {
+ bottomMargin = child
+ .getResources()
+ .getDimensionPixelOffset(R.dimen.mtrl_bottomappbar_fab_bottom_margin);
+ }
// Should be moved above the bottom insets with space ignoring any shadow padding.
int minBottomMargin = bottomMargin - bottomShadowPadding;
fabLayoutParams.bottomMargin = child.getBottomInset() + minBottomMargin;
@@ -1201,12 +1279,12 @@ public boolean onLayoutChild(
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;
+ updateFabAnchorGravity(child, dependentView);
// Keep track of the original bottom margin for the fab. We will manage the margin if
// nothing was set.
+ CoordinatorLayout.LayoutParams fabLayoutParams =
+ (CoordinatorLayout.LayoutParams) dependentView.getLayoutParams();
originalBottomMargin = fabLayoutParams.bottomMargin;
if (dependentView instanceof FloatingActionButton) {
@@ -1230,7 +1308,7 @@ public boolean onLayoutChild(
}
// Move the fab to the correct position
- child.setCutoutState();
+ child.setCutoutStateAndTranslateFab();
}
// Now let the CoordinatorLayout lay out the BAB
diff --git a/lib/java/com/google/android/material/bottomappbar/res-public/values/public.xml b/lib/java/com/google/android/material/bottomappbar/res-public/values/public.xml
index 9424144c76a..e6bfad40f4a 100644
--- a/lib/java/com/google/android/material/bottomappbar/res-public/values/public.xml
+++ b/lib/java/com/google/android/material/bottomappbar/res-public/values/public.xml
@@ -18,6 +18,7 @@
+
diff --git a/lib/java/com/google/android/material/bottomappbar/res/values/attrs.xml b/lib/java/com/google/android/material/bottomappbar/res/values/attrs.xml
index fcbb8cf4589..51e1ce42d84 100644
--- a/lib/java/com/google/android/material/bottomappbar/res/values/attrs.xml
+++ b/lib/java/com/google/android/material/bottomappbar/res/values/attrs.xml
@@ -30,6 +30,13 @@
+
+
+
+
+
+
+
diff --git a/lib/java/com/google/android/material/bottomappbar/res/values/styles.xml b/lib/java/com/google/android/material/bottomappbar/res/values/styles.xml
index dd5368de8a9..58a9f5925c4 100644
--- a/lib/java/com/google/android/material/bottomappbar/res/values/styles.xml
+++ b/lib/java/com/google/android/material/bottomappbar/res/values/styles.xml
@@ -53,6 +53,7 @@