[BottomAppBar] Add auto-hide scrolling behavior
Specs: https://m3.material.io/components/bottom-app-bar/guidelines#5becae2a-851e-49b6-b253-4eb994f36137
Test: AppBarScreenshotTest and AppBarTest updated
Relnote: "We added a new BottomAppBar that takes as parameter a BottomAppBarScrollBehavior in order to auto-hide it when content is scrolled. We also added FabPosition.EndOverlay allowing the FAB to overlay the bottom app bar in the scaffold instead of being anchored above it."
Bug: 287308719
Change-Id: Iecb47accb59cbf44a49d0099289ef89736a84f2b
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index d665b1b..c6ec5ce 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -29,13 +29,17 @@
}
public final class AppBarKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void BottomAppBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.BottomAppBarScrollBehavior? scrollBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void BottomAppBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void BottomAppBar(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void BottomAppBar(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.BottomAppBarScrollBehavior? scrollBehavior);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.BottomAppBarState BottomAppBarState(float initialHeightOffsetLimit, float initialHeightOffset, float initialContentOffset);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void CenterAlignedTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void MediumTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SmallTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.BottomAppBarState rememberBottomAppBarState(optional float initialHeightOffsetLimit, optional float initialHeightOffset, optional float initialContentOffset);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.TopAppBarState rememberTopAppBarState(optional float initialHeightOffsetLimit, optional float initialHeightOffset, optional float initialContentOffset);
}
@@ -66,6 +70,7 @@
}
public final class BottomAppBarDefaults {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.BottomAppBarScrollBehavior exitAlwaysScrollBehavior(optional androidx.compose.material3.BottomAppBarState state, optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec);
method @androidx.compose.runtime.Composable public long getBottomAppBarFabColor();
method @androidx.compose.runtime.Composable public long getContainerColor();
method public float getContainerElevation();
@@ -79,6 +84,39 @@
field public static final androidx.compose.material3.BottomAppBarDefaults INSTANCE;
}
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public interface BottomAppBarScrollBehavior {
+ method public androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? getFlingAnimationSpec();
+ method public androidx.compose.ui.input.nestedscroll.NestedScrollConnection getNestedScrollConnection();
+ method public androidx.compose.animation.core.AnimationSpec<java.lang.Float>? getSnapAnimationSpec();
+ method public androidx.compose.material3.BottomAppBarState getState();
+ method public boolean isPinned();
+ property public abstract androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec;
+ property public abstract boolean isPinned;
+ property public abstract androidx.compose.ui.input.nestedscroll.NestedScrollConnection nestedScrollConnection;
+ property public abstract androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec;
+ property public abstract androidx.compose.material3.BottomAppBarState state;
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public interface BottomAppBarState {
+ method public float getCollapsedFraction();
+ method public float getContentOffset();
+ method public float getHeightOffset();
+ method public float getHeightOffsetLimit();
+ method public void setContentOffset(float);
+ method public void setHeightOffset(float);
+ method public void setHeightOffsetLimit(float);
+ property public abstract float collapsedFraction;
+ property public abstract float contentOffset;
+ property public abstract float heightOffset;
+ property public abstract float heightOffsetLimit;
+ field public static final androidx.compose.material3.BottomAppBarState.Companion Companion;
+ }
+
+ public static final class BottomAppBarState.Companion {
+ method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.BottomAppBarState,?> getSaver();
+ property public final androidx.compose.runtime.saveable.Saver<androidx.compose.material3.BottomAppBarState,?> Saver;
+ }
+
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class BottomSheetDefaults {
method @androidx.compose.runtime.Composable public void DragHandle(optional androidx.compose.ui.Modifier modifier, optional float width, optional float height, optional androidx.compose.ui.graphics.Shape shape, optional long color);
method @androidx.compose.runtime.Composable public long getContainerColor();
@@ -677,9 +715,11 @@
public static final class FabPosition.Companion {
method public int getCenter();
method public int getEnd();
+ method public int getEndOverlay();
method public int getStart();
property public final int Center;
property public final int End;
+ property public final int EndOverlay;
property public final int Start;
}
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index d665b1b..c6ec5ce 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -29,13 +29,17 @@
}
public final class AppBarKt {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void BottomAppBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.BottomAppBarScrollBehavior? scrollBehavior, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void BottomAppBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void BottomAppBar(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void BottomAppBar(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional long containerColor, optional long contentColor, optional float tonalElevation, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.BottomAppBarScrollBehavior? scrollBehavior);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.BottomAppBarState BottomAppBarState(float initialHeightOffsetLimit, float initialHeightOffset, float initialContentOffset);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void CenterAlignedTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LargeTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void MediumTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SmallTopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TopAppBar(kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> navigationIcon, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> actions, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional androidx.compose.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.BottomAppBarState rememberBottomAppBarState(optional float initialHeightOffsetLimit, optional float initialHeightOffset, optional float initialContentOffset);
method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.TopAppBarState rememberTopAppBarState(optional float initialHeightOffsetLimit, optional float initialHeightOffset, optional float initialContentOffset);
}
@@ -66,6 +70,7 @@
}
public final class BottomAppBarDefaults {
+ method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public androidx.compose.material3.BottomAppBarScrollBehavior exitAlwaysScrollBehavior(optional androidx.compose.material3.BottomAppBarState state, optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec);
method @androidx.compose.runtime.Composable public long getBottomAppBarFabColor();
method @androidx.compose.runtime.Composable public long getContainerColor();
method public float getContainerElevation();
@@ -79,6 +84,39 @@
field public static final androidx.compose.material3.BottomAppBarDefaults INSTANCE;
}
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public interface BottomAppBarScrollBehavior {
+ method public androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? getFlingAnimationSpec();
+ method public androidx.compose.ui.input.nestedscroll.NestedScrollConnection getNestedScrollConnection();
+ method public androidx.compose.animation.core.AnimationSpec<java.lang.Float>? getSnapAnimationSpec();
+ method public androidx.compose.material3.BottomAppBarState getState();
+ method public boolean isPinned();
+ property public abstract androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float>? flingAnimationSpec;
+ property public abstract boolean isPinned;
+ property public abstract androidx.compose.ui.input.nestedscroll.NestedScrollConnection nestedScrollConnection;
+ property public abstract androidx.compose.animation.core.AnimationSpec<java.lang.Float>? snapAnimationSpec;
+ property public abstract androidx.compose.material3.BottomAppBarState state;
+ }
+
+ @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public interface BottomAppBarState {
+ method public float getCollapsedFraction();
+ method public float getContentOffset();
+ method public float getHeightOffset();
+ method public float getHeightOffsetLimit();
+ method public void setContentOffset(float);
+ method public void setHeightOffset(float);
+ method public void setHeightOffsetLimit(float);
+ property public abstract float collapsedFraction;
+ property public abstract float contentOffset;
+ property public abstract float heightOffset;
+ property public abstract float heightOffsetLimit;
+ field public static final androidx.compose.material3.BottomAppBarState.Companion Companion;
+ }
+
+ public static final class BottomAppBarState.Companion {
+ method public androidx.compose.runtime.saveable.Saver<androidx.compose.material3.BottomAppBarState,?> getSaver();
+ property public final androidx.compose.runtime.saveable.Saver<androidx.compose.material3.BottomAppBarState,?> Saver;
+ }
+
@SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public final class BottomSheetDefaults {
method @androidx.compose.runtime.Composable public void DragHandle(optional androidx.compose.ui.Modifier modifier, optional float width, optional float height, optional androidx.compose.ui.graphics.Shape shape, optional long color);
method @androidx.compose.runtime.Composable public long getContainerColor();
@@ -677,9 +715,11 @@
public static final class FabPosition.Companion {
method public int getCenter();
method public int getEnd();
+ method public int getEndOverlay();
method public int getStart();
property public final int Center;
property public final int End;
+ property public final int EndOverlay;
property public final int Start;
}
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index c6d720e..2e8c6a2 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -57,6 +57,7 @@
import androidx.compose.material3.samples.ElevatedFilterChipSample
import androidx.compose.material3.samples.ElevatedSuggestionChipSample
import androidx.compose.material3.samples.EnterAlwaysTopAppBar
+import androidx.compose.material3.samples.ExitAlwaysBottomAppBar
import androidx.compose.material3.samples.ExitUntilCollapsedLargeTopAppBar
import androidx.compose.material3.samples.ExitUntilCollapsedMediumTopAppBar
import androidx.compose.material3.samples.ExposedDropdownMenuSample
@@ -465,7 +466,12 @@
name = ::BottomAppBarWithFAB.name,
description = BottomAppBarsExampleDescription,
sourceUrl = BottomAppBarsExampleSourceUrl,
- ) { BottomAppBarWithFAB() }
+ ) { BottomAppBarWithFAB() },
+ Example(
+ name = ::ExitAlwaysBottomAppBar.name,
+ description = BottomAppBarsExampleDescription,
+ sourceUrl = BottomAppBarsExampleSourceUrl,
+ ) { ExitAlwaysBottomAppBar() }
)
private const val TopAppBarExampleDescription = "Top app bar examples"
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
index 6cbc0aa..a6538f0 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/AppBarSamples.kt
@@ -19,6 +19,7 @@
import androidx.annotation.Sampled
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
@@ -31,6 +32,7 @@
import androidx.compose.material3.BottomAppBarDefaults
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.Icon
@@ -418,11 +420,13 @@
@Sampled
@Composable
fun SimpleBottomAppBar() {
- BottomAppBar {
- IconButton(onClick = { /* doSomething() */ }) {
- Icon(Icons.Filled.Menu, contentDescription = "Localized description")
+ BottomAppBar(
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.Menu, contentDescription = "Localized description")
+ }
}
- }
+ )
}
@Preview
@@ -452,3 +456,59 @@
}
)
}
+
+/**
+ * A sample for a [BottomAppBar] that collapses when the content is scrolled up, and
+ * appears when the content scrolled down.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Sampled
+@Composable
+fun ExitAlwaysBottomAppBar() {
+ val scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
+ Scaffold(
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+ bottomBar = {
+ BottomAppBar(
+ actions = {
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.Check, contentDescription = "Localized description")
+ }
+ IconButton(onClick = { /* doSomething() */ }) {
+ Icon(Icons.Filled.Edit, contentDescription = "Localized description")
+ }
+ },
+ scrollBehavior = scrollBehavior
+ )
+ },
+ floatingActionButton = {
+ FloatingActionButton(
+ modifier = Modifier.offset(y = 4.dp),
+ onClick = { /* do something */ },
+ containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+ elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
+ ) {
+ Icon(Icons.Filled.Add, "Localized description")
+ }
+ },
+ floatingActionButtonPosition = FabPosition.EndOverlay,
+ content = { innerPadding ->
+ LazyColumn(
+ contentPadding = innerPadding,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ val list = (0..75).map { it.toString() }
+ items(count = list.size) {
+ Text(
+ text = list[it],
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ )
+ }
+ }
+ }
+ )
+}
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
index ddbb438..7b8d1a5 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarScreenshotTest.kt
@@ -24,6 +24,7 @@
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material3.BottomAppBarDefaults.bottomAppBarFabColor
import androidx.compose.material3.TopAppBarDefaults.enterAlwaysScrollBehavior
import androidx.compose.material3.tokens.TopAppBarSmallTokens
import androidx.compose.testutils.assertAgainstGolden
@@ -361,7 +362,7 @@
floatingActionButton = {
FloatingActionButton(
onClick = { /* do something */ },
- containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+ containerColor = bottomAppBarFabColor,
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
) {
Icon(Icons.Filled.Add, "Localized description")
@@ -393,7 +394,7 @@
floatingActionButton = {
FloatingActionButton(
onClick = { /* do something */ },
- containerColor = BottomAppBarDefaults.bottomAppBarFabColor,
+ containerColor = bottomAppBarFabColor,
elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
) {
Icon(Icons.Filled.Add, "Localized description")
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
index ef6ffe4..a913ceb 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
@@ -23,6 +23,7 @@
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
@@ -1183,6 +1184,105 @@
.assertTopPositionInRootIsEqualTo(12.dp)
}
+ @Test
+ fun bottomAppBar_exitAlways_scaffoldWithFAB_default_positioning() {
+ rule.setMaterialContent(lightColorScheme()) {
+ val scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
+ Scaffold(
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+ bottomBar = {
+ BottomAppBar(
+ modifier = Modifier.testTag(BottomAppBarTestTag),
+ scrollBehavior = scrollBehavior
+ ) {}
+ },
+ floatingActionButton = {
+ FloatingActionButton(
+ modifier = Modifier
+ .testTag("FAB")
+ .offset(y = 4.dp),
+ onClick = { /* do something */ },
+ ) {}
+ },
+ floatingActionButtonPosition = FabPosition.EndOverlay
+ ) {}
+ }
+
+ val appBarBounds = rule.onNodeWithTag(BottomAppBarTestTag).getUnclippedBoundsInRoot()
+ val fabBounds = rule.onNodeWithTag("FAB").getUnclippedBoundsInRoot()
+ rule.onNodeWithTag("FAB")
+ // FAB should be 16.dp from the end
+ .assertLeftPositionInRootIsEqualTo(appBarBounds.width - 16.dp - fabBounds.width)
+ // FAB should be 12.dp from the bottom
+ .assertTopPositionInRootIsEqualTo(rule.rootHeight() - 12.dp - fabBounds.height)
+ }
+
+ @Test
+ fun bottomAppBar_exitAlways_scaffoldWithFAB_scrolled_positioning() {
+ lateinit var scrollBehavior: BottomAppBarScrollBehavior
+ val scrollHeightOffsetDp = 20.dp
+ var scrollHeightOffsetPx = 0f
+
+ rule.setMaterialContent(lightColorScheme()) {
+ scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
+ scrollHeightOffsetPx = with(LocalDensity.current) { scrollHeightOffsetDp.toPx() }
+ Scaffold(
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+ bottomBar = {
+ BottomAppBar(
+ modifier = Modifier.testTag(BottomAppBarTestTag),
+ scrollBehavior = scrollBehavior
+ ) {}
+ },
+ floatingActionButton = {
+ FloatingActionButton(
+ modifier = Modifier
+ .testTag("FAB")
+ .offset(y = 4.dp),
+ onClick = { /* do something */ },
+ ) {}
+ },
+ floatingActionButtonPosition = FabPosition.EndOverlay
+ ) {}
+ }
+
+ // Simulate scrolled content.
+ rule.runOnIdle {
+ scrollBehavior.state.heightOffset = -scrollHeightOffsetPx
+ scrollBehavior.state.contentOffset = -scrollHeightOffsetPx
+ }
+ rule.waitForIdle()
+ rule.onNodeWithTag(BottomAppBarTestTag)
+ .assertHeightIsEqualTo(BottomAppBarTokens.ContainerHeight - scrollHeightOffsetDp)
+
+ val appBarBounds = rule.onNodeWithTag(BottomAppBarTestTag).getUnclippedBoundsInRoot()
+ val fabBounds = rule.onNodeWithTag("FAB").getUnclippedBoundsInRoot()
+ rule.onNodeWithTag("FAB")
+ // FAB should be 16.dp from the end
+ .assertLeftPositionInRootIsEqualTo(appBarBounds.width - 16.dp - fabBounds.width)
+ // FAB should be 12.dp from the bottom
+ .assertTopPositionInRootIsEqualTo(rule.rootHeight() - 12.dp - fabBounds.height)
+ }
+
+ @Test
+ fun bottomAppBar_exitAlways_allowHorizontalScroll() {
+ lateinit var state: LazyListState
+ rule.setMaterialContent(lightColorScheme()) {
+ state = rememberLazyListState()
+ MultiPageContent(BottomAppBarDefaults.exitAlwaysScrollBehavior(), state)
+ }
+
+ rule.onNodeWithTag(LazyListTag).performTouchInput { swipeLeft() }
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+ }
+
+ rule.onNodeWithTag(LazyListTag).performTouchInput { swipeRight() }
+ rule.runOnIdle {
+ assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+ }
+ }
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun MultiPageContent(scrollBehavior: TopAppBarScrollBehavior, state: LazyListState) {
@@ -1218,6 +1318,39 @@
}
}
+ @Composable
+ private fun MultiPageContent(scrollBehavior: BottomAppBarScrollBehavior, state: LazyListState) {
+ Scaffold(
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+ bottomBar = {
+ BottomAppBar(
+ modifier = Modifier.testTag(BottomAppBarTestTag),
+ scrollBehavior = scrollBehavior
+ ) {}
+ }
+ ) { contentPadding ->
+ LazyRow(
+ Modifier
+ .fillMaxSize()
+ .testTag(LazyListTag), state
+ ) {
+ items(2) { page ->
+ LazyColumn(
+ modifier = Modifier.fillParentMaxSize(),
+ contentPadding = contentPadding
+ ) {
+ items(50) {
+ Text(
+ modifier = Modifier.fillParentMaxWidth(),
+ text = "Item #$page x $it"
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+
/**
* Checks the app bar's components positioning when it's a [TopAppBar], a
* [CenterAlignedTopAppBar], or a larger app bar that is scrolled up and collapsed into a small
@@ -1583,7 +1716,8 @@
(TopAppBarSmallTokens.ContainerHeight - FakeIconSize) / 2
private val LazyListTag = "lazyList"
- private val TopAppBarTestTag = "bar"
+ private val TopAppBarTestTag = "topAppBar"
+ private val BottomAppBarTestTag = "bottomAppBar"
private val NavigationIconTestTag = "navigationIcon"
private val TitleTestTag = "title"
private val ActionsTestTag = "actions"
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
index cd79a42..7d8c9f6 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
@@ -73,6 +73,7 @@
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.LastBaseline
import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.clearAndSetSemantics
@@ -391,6 +392,7 @@
* @param contentPadding the padding applied to the content of this BottomAppBar
* @param windowInsets a window insets that app bar will respect.
*/
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BottomAppBar(
actions: @Composable RowScope.() -> Unit,
@@ -402,12 +404,76 @@
contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding,
windowInsets: WindowInsets = BottomAppBarDefaults.windowInsets,
) = BottomAppBar(
+ actions = actions,
+ modifier = modifier,
+ floatingActionButton = floatingActionButton,
+ containerColor = containerColor,
+ contentColor = contentColor,
+ tonalElevation = tonalElevation,
+ contentPadding = contentPadding,
+ windowInsets = windowInsets,
+ scrollBehavior = null
+)
+
+/**
+ * <a href="https://m3.material.io/components/bottom-app-bar/overview" class="external" target="_blank">Material Design bottom app bar</a>.
+ *
+ * A bottom app bar displays navigation and key actions at the bottom of mobile screens.
+ *
+ * ![Bottom app bar image](https://developer.android.com/images/reference/androidx/compose/material3/bottom-app-bar.png)
+ *
+ * @sample androidx.compose.material3.samples.SimpleBottomAppBar
+ *
+ * It can optionally display a [FloatingActionButton] embedded at the end of the BottomAppBar.
+ *
+ * @sample androidx.compose.material3.samples.BottomAppBarWithFAB
+ *
+ * A bottom app bar that uses a [scrollBehavior] to customize its nested scrolling behavior when
+ * working in conjunction with a scrolling content looks like:
+ *
+ * @sample androidx.compose.material3.samples.ExitAlwaysBottomAppBar
+ *
+ * Also see [NavigationBar].
+ *
+ * @param actions the icon content of this BottomAppBar. The default layout here is a [Row],
+ * so content inside will be placed horizontally.
+ * @param modifier the [Modifier] to be applied to this BottomAppBar
+ * @param floatingActionButton optional floating action button at the end of this BottomAppBar
+ * @param containerColor the color used for the background of this BottomAppBar. Use
+ * [Color.Transparent] to have no color.
+ * @param contentColor the preferred color for content inside this BottomAppBar. Defaults to either
+ * the matching content color for [containerColor], or to the current [LocalContentColor] if
+ * [containerColor] is not a color from the theme.
+ * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color
+ * overlay is applied on top of the container. A higher tonal elevation value will result in a
+ * darker color in light theme and lighter color in dark theme. See also: [Surface].
+ * @param contentPadding the padding applied to the content of this BottomAppBar
+ * @param windowInsets a window insets that app bar will respect.
+ * @param scrollBehavior a [BottomAppBarScrollBehavior] which holds various offset values that will
+ * be applied by this bottom app bar to set up its height. A scroll behavior is designed to
+ * work in conjunction with a scrolled content to change the bottom app bar appearance as the
+ * content scrolls. See [BottomAppBarScrollBehavior.nestedScrollConnection].
+ */
+@ExperimentalMaterial3Api
+@Composable
+fun BottomAppBar(
+ actions: @Composable RowScope.() -> Unit,
+ modifier: Modifier = Modifier,
+ floatingActionButton: @Composable (() -> Unit)? = null,
+ containerColor: Color = BottomAppBarDefaults.containerColor,
+ contentColor: Color = contentColorFor(containerColor),
+ tonalElevation: Dp = BottomAppBarDefaults.ContainerElevation,
+ contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding,
+ windowInsets: WindowInsets = BottomAppBarDefaults.windowInsets,
+ scrollBehavior: BottomAppBarScrollBehavior? = null,
+) = BottomAppBar(
modifier = modifier,
containerColor = containerColor,
contentColor = contentColor,
tonalElevation = tonalElevation,
windowInsets = windowInsets,
- contentPadding = contentPadding
+ contentPadding = contentPadding,
+ scrollBehavior = scrollBehavior
) {
Row(
modifier = Modifier.weight(1f),
@@ -455,6 +521,7 @@
* @param content the content of this BottomAppBar. The default layout here is a [Row],
* so content inside will be placed horizontally.
*/
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BottomAppBar(
modifier: Modifier = Modifier,
@@ -464,7 +531,81 @@
contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding,
windowInsets: WindowInsets = BottomAppBarDefaults.windowInsets,
content: @Composable RowScope.() -> Unit
+) = BottomAppBar(
+ modifier = modifier,
+ containerColor = containerColor,
+ contentColor = contentColor,
+ tonalElevation = tonalElevation,
+ contentPadding = contentPadding,
+ windowInsets = windowInsets,
+ scrollBehavior = null,
+ content = content
+)
+
+/**
+ * <a href="https://m3.material.io/components/bottom-app-bar/overview" class="external" target="_blank">Material Design bottom app bar</a>.
+ *
+ * A bottom app bar displays navigation and key actions at the bottom of mobile screens.
+ *
+ * ![Bottom app bar image](https://developer.android.com/images/reference/androidx/compose/material3/bottom-app-bar.png)
+ *
+ * If you are interested in displaying a [FloatingActionButton], consider using another overload.
+ *
+ * Also see [NavigationBar].
+ *
+ * @param modifier the [Modifier] to be applied to this BottomAppBar
+ * @param containerColor the color used for the background of this BottomAppBar. Use
+ * [Color.Transparent] to have no color.
+ * @param contentColor the preferred color for content inside this BottomAppBar. Defaults to either
+ * the matching content color for [containerColor], or to the current [LocalContentColor] if
+ * [containerColor] is not a color from the theme.
+ * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color
+ * overlay is applied on top of the container. A higher tonal elevation value will result in a
+ * darker color in light theme and lighter color in dark theme. See also: [Surface].
+ * @param contentPadding the padding applied to the content of this BottomAppBar
+ * @param windowInsets a window insets that app bar will respect.
+ * @param scrollBehavior a [BottomAppBarScrollBehavior] which holds various offset values that will
+ * be applied by this bottom app bar to set up its height. A scroll behavior is designed to
+ * work in conjunction with a scrolled content to change the bottom app bar appearance as the
+ * content scrolls. See [BottomAppBarScrollBehavior.nestedScrollConnection].
+ * @param content the content of this BottomAppBar. The default layout here is a [Row],
+ * so content inside will be placed horizontally.
+ */
+@ExperimentalMaterial3Api
+@Composable
+fun BottomAppBar(
+ modifier: Modifier = Modifier,
+ containerColor: Color = BottomAppBarDefaults.containerColor,
+ contentColor: Color = contentColorFor(containerColor),
+ tonalElevation: Dp = BottomAppBarDefaults.ContainerElevation,
+ contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding,
+ windowInsets: WindowInsets = BottomAppBarDefaults.windowInsets,
+ scrollBehavior: BottomAppBarScrollBehavior? = null,
+ content: @Composable RowScope.() -> Unit
) {
+ // Set up support for resizing the bottom app bar when vertically dragging the bar itself.
+ val appBarDragModifier = if (scrollBehavior != null && !scrollBehavior.isPinned) {
+ Modifier.draggable(
+ orientation = Orientation.Vertical,
+ state = rememberDraggableState { delta ->
+ scrollBehavior.state.heightOffset -= delta
+ },
+ onDragStopped = { velocity ->
+ settleAppBarBottom(
+ scrollBehavior.state,
+ velocity,
+ scrollBehavior.flingAnimationSpec,
+ scrollBehavior.snapAnimationSpec
+ )
+ }
+ )
+ } else {
+ Modifier
+ }
+
+ // Compose a Surface with a Row content.
+ // The height of the app bar is determined by subtracting the bar's height offset from the
+ // app bar's defined constant height value (i.e. the ContainerHeight token).
Surface(
color = containerColor,
contentColor = contentColor,
@@ -472,6 +613,19 @@
// TODO(b/209583788): Consider adding a shape parameter if updated design guidance allows
shape = BottomAppBarTokens.ContainerShape.value,
modifier = modifier
+ .layout { measurable, constraints ->
+ // Sets the app bar's height offset to collapse the entire bar's height when content
+ // is scrolled.
+ scrollBehavior?.state?.heightOffsetLimit =
+ -BottomAppBarTokens.ContainerHeight.toPx()
+
+ val placeable = measurable.measure(constraints)
+ val height = placeable.height + (scrollBehavior?.state?.heightOffset ?: 0f)
+ layout(placeable.width, height.roundToInt()) {
+ placeable.place(0, 0)
+ }
+ }
+ .then(appBarDragModifier)
) {
Row(
Modifier
@@ -979,6 +1133,50 @@
}
}
+/**
+ * A BottomAppBarScrollBehavior defines how a bottom app bar should behave when the content under
+ * it is scrolled.
+ *
+ * @see [BottomAppBarDefaults.exitAlwaysScrollBehavior]
+ */
+@ExperimentalMaterial3Api
+@Stable
+interface BottomAppBarScrollBehavior {
+
+ /**
+ * A [BottomAppBarState] that is attached to this behavior and is read and updated when
+ * scrolling happens.
+ */
+ val state: BottomAppBarState
+
+ /**
+ * Indicates whether the bottom app bar is pinned.
+ *
+ * A pinned app bar will stay fixed in place when content is scrolled and will not react to any
+ * drag gestures.
+ */
+ val isPinned: Boolean
+
+ /**
+ * An optional [AnimationSpec] that defines how the bottom app bar snaps to either fully
+ * collapsed or fully extended state when a fling or a drag scrolled it into an intermediate
+ * position.
+ */
+ val snapAnimationSpec: AnimationSpec<Float>?
+
+ /**
+ * An optional [DecayAnimationSpec] that defined how to fling the bottom app bar when the user
+ * flings the app bar itself, or the content below it.
+ */
+ val flingAnimationSpec: DecayAnimationSpec<Float>?
+
+ /**
+ * A [NestedScrollConnection] that should be attached to a [Modifier.nestedScroll] in order to
+ * keep track of the scroll events.
+ */
+ val nestedScrollConnection: NestedScrollConnection
+}
+
/** Contains default values used for the bottom app bar implementations. */
object BottomAppBarDefaults {
@@ -1012,6 +1210,287 @@
val bottomAppBarFabColor: Color
@Composable get() =
FabSecondaryTokens.ContainerColor.value
+
+ /**
+ * Returns a [BottomAppBarScrollBehavior]. A bottom app bar that is set up with this
+ * [BottomAppBarScrollBehavior] will immediately collapse when the content is pulled up, and
+ * will immediately appear when the content is pulled down.
+ *
+ * @param state the state object to be used to control or observe the bottom app bar's scroll
+ * state. See [rememberBottomAppBarState] for a state that is remembered across compositions.
+ * @param canScroll a callback used to determine whether scroll events are to be
+ * handled by this [ExitAlwaysScrollBehavior]
+ * @param snapAnimationSpec an optional [AnimationSpec] that defines how the bottom app bar
+ * snaps to either fully collapsed or fully extended state when a fling or a drag scrolled it
+ * into an intermediate position
+ * @param flingAnimationSpec an optional [DecayAnimationSpec] that defined how to fling the
+ * bottom app bar when the user flings the app bar itself, or the content below it
+ */
+ @ExperimentalMaterial3Api
+ @Composable
+ fun exitAlwaysScrollBehavior(
+ state: BottomAppBarState = rememberBottomAppBarState(),
+ canScroll: () -> Boolean = { true },
+ snapAnimationSpec: AnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMediumLow),
+ flingAnimationSpec: DecayAnimationSpec<Float>? = rememberSplineBasedDecay()
+ ): BottomAppBarScrollBehavior =
+ ExitAlwaysScrollBehavior(
+ state = state,
+ snapAnimationSpec = snapAnimationSpec,
+ flingAnimationSpec = flingAnimationSpec,
+ canScroll = canScroll
+ )
+}
+
+/**
+ * Creates a [BottomAppBarState] that is remembered across compositions.
+ *
+ * @param initialHeightOffsetLimit the initial value for [BottomAppBarState.heightOffsetLimit],
+ * which represents the pixel limit that a bottom app bar is allowed to collapse when the scrollable
+ * content is scrolled
+ * @param initialHeightOffset the initial value for [BottomAppBarState.heightOffset]. The initial
+ * offset height offset should be between zero and [initialHeightOffsetLimit].
+ * @param initialContentOffset the initial value for [BottomAppBarState.contentOffset]
+ */
+@ExperimentalMaterial3Api
+@Composable
+fun rememberBottomAppBarState(
+ initialHeightOffsetLimit: Float = -Float.MAX_VALUE,
+ initialHeightOffset: Float = 0f,
+ initialContentOffset: Float = 0f
+): BottomAppBarState {
+ return rememberSaveable(saver = BottomAppBarState.Saver) {
+ BottomAppBarState(
+ initialHeightOffsetLimit,
+ initialHeightOffset,
+ initialContentOffset
+ )
+ }
+}
+
+/**
+ * A state object that can be hoisted to control and observe the bottom app bar state. The state is
+ * read and updated by a [BottomAppBarScrollBehavior] implementation.
+ *
+ * In most cases, this state will be created via [rememberBottomAppBarState].
+ */
+@ExperimentalMaterial3Api
+interface BottomAppBarState {
+
+ /**
+ * The bottom app bar's height offset limit in pixels, which represents the limit that a bottom
+ * app bar is allowed to collapse to.
+ *
+ * Use this limit to coerce the [heightOffset] value when it's updated.
+ */
+ var heightOffsetLimit: Float
+
+ /**
+ * The bottom app bar's current height offset in pixels. This height offset is applied to the
+ * fixed height of the app bar to control the displayed height when content is being scrolled.
+ *
+ * Updates to the [heightOffset] value are coerced between zero and [heightOffsetLimit].
+ */
+ var heightOffset: Float
+
+ /**
+ * The total offset of the content scrolled under the bottom app bar.
+ *
+ * This value is updated by a [BottomAppBarScrollBehavior] whenever a nested scroll connection
+ * consumes scroll events. A common implementation would update the value to be the sum of all
+ * [NestedScrollConnection.onPostScroll] `consumed.y` values.
+ */
+ var contentOffset: Float
+
+ /**
+ * A value that represents the collapsed height percentage of the app bar.
+ *
+ * A `0.0` represents a fully expanded bar, and `1.0` represents a fully collapsed bar (computed
+ * as [heightOffset] / [heightOffsetLimit]).
+ */
+ val collapsedFraction: Float
+
+ companion object {
+ /**
+ * The default [Saver] implementation for [BottomAppBarState].
+ */
+ val Saver: Saver<BottomAppBarState, *> = listSaver(
+ save = { listOf(it.heightOffsetLimit, it.heightOffset, it.contentOffset) },
+ restore = {
+ BottomAppBarState(
+ initialHeightOffsetLimit = it[0],
+ initialHeightOffset = it[1],
+ initialContentOffset = it[2]
+ )
+ }
+ )
+ }
+}
+
+/**
+ * Creates a [BottomAppBarState].
+ *
+ * @param initialHeightOffsetLimit the initial value for [BottomAppBarState.heightOffsetLimit],
+ * which represents the pixel limit that a bottom app bar is allowed to collapse when the scrollable
+ * content is scrolled
+ * @param initialHeightOffset the initial value for [BottomAppBarState.heightOffset]. The initial
+ * offset height offset should be between zero and [initialHeightOffsetLimit].
+ * @param initialContentOffset the initial value for [BottomAppBarState.contentOffset]
+ */
+@ExperimentalMaterial3Api
+fun BottomAppBarState(
+ initialHeightOffsetLimit: Float,
+ initialHeightOffset: Float,
+ initialContentOffset: Float
+): BottomAppBarState = BottomAppBarStateImpl(
+ initialHeightOffsetLimit,
+ initialHeightOffset,
+ initialContentOffset
+)
+
+@ExperimentalMaterial3Api
+@Stable
+private class BottomAppBarStateImpl(
+ initialHeightOffsetLimit: Float,
+ initialHeightOffset: Float,
+ initialContentOffset: Float
+) : BottomAppBarState {
+
+ override var heightOffsetLimit by mutableFloatStateOf(initialHeightOffsetLimit)
+
+ override var heightOffset: Float
+ get() = _heightOffset.floatValue
+ set(newOffset) {
+ _heightOffset.floatValue = newOffset.coerceIn(
+ minimumValue = heightOffsetLimit,
+ maximumValue = 0f
+ )
+ }
+
+ override var contentOffset by mutableFloatStateOf(initialContentOffset)
+
+ override val collapsedFraction: Float
+ get() = if (heightOffsetLimit != 0f) {
+ heightOffset / heightOffsetLimit
+ } else {
+ 0f
+ }
+
+ private var _heightOffset = mutableFloatStateOf(initialHeightOffset)
+}
+
+/**
+ * A [BottomAppBarScrollBehavior] that adjusts its properties to affect the colors and height of a
+ * bottom app bar.
+ *
+ * A bottom app bar that is set up with this [BottomAppBarScrollBehavior] will immediately collapse
+ * when the nested content is pulled up, and will immediately appear when the content is pulled
+ * down.
+ *
+ * @param state a [BottomAppBarState]
+ * @param snapAnimationSpec an optional [AnimationSpec] that defines how the bottom app bar snaps to
+ * either fully collapsed or fully extended state when a fling or a drag scrolled it into an
+ * intermediate position
+ * @param flingAnimationSpec an optional [DecayAnimationSpec] that defined how to fling the bottom
+ * app bar when the user flings the app bar itself, or the content below it
+ * @param canScroll a callback used to determine whether scroll events are to be
+ * handled by this [ExitAlwaysScrollBehavior]
+ */
+@ExperimentalMaterial3Api
+private class ExitAlwaysScrollBehavior(
+ override val state: BottomAppBarState,
+ override val snapAnimationSpec: AnimationSpec<Float>?,
+ override val flingAnimationSpec: DecayAnimationSpec<Float>?,
+ val canScroll: () -> Boolean = { true }
+) : BottomAppBarScrollBehavior {
+ override val isPinned: Boolean = false
+ override var nestedScrollConnection =
+ object : NestedScrollConnection {
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ if (!canScroll()) return Offset.Zero
+ state.contentOffset += consumed.y
+ if (state.heightOffset == 0f || state.heightOffset == state.heightOffsetLimit) {
+ if (consumed.y == 0f && available.y > 0f) {
+ // Reset the total content offset to zero when scrolling all the way down.
+ // This will eliminate some float precision inaccuracies.
+ state.contentOffset = 0f
+ }
+ }
+ state.heightOffset = state.heightOffset + consumed.y
+ return Offset.Zero
+ }
+
+ override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+ val superConsumed = super.onPostFling(consumed, available)
+ return superConsumed + settleAppBarBottom(
+ state,
+ available.y,
+ flingAnimationSpec,
+ snapAnimationSpec
+ )
+ }
+ }
+}
+
+/**
+ * Settles the app bar by flinging, in case the given velocity is greater than zero, and snapping
+ * after the fling settles.
+ */
+@ExperimentalMaterial3Api
+private suspend fun settleAppBarBottom(
+ state: BottomAppBarState,
+ velocity: Float,
+ flingAnimationSpec: DecayAnimationSpec<Float>?,
+ snapAnimationSpec: AnimationSpec<Float>?
+): Velocity {
+ // Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar,
+ // and just return Zero Velocity.
+ // Note that we don't check for 0f due to float precision with the collapsedFraction
+ // calculation.
+ if (state.collapsedFraction < 0.01f || state.collapsedFraction == 1f) {
+ return Velocity.Zero
+ }
+ var remainingVelocity = velocity
+ // In case there is an initial velocity that was left after a previous user fling, animate to
+ // continue the motion to expand or collapse the app bar.
+ if (flingAnimationSpec != null && abs(velocity) > 1f) {
+ var lastValue = 0f
+ AnimationState(
+ initialValue = 0f,
+ initialVelocity = velocity,
+ )
+ .animateDecay(flingAnimationSpec) {
+ val delta = value - lastValue
+ val initialHeightOffset = state.heightOffset
+ state.heightOffset = initialHeightOffset + delta
+ val consumed = abs(initialHeightOffset - state.heightOffset)
+ lastValue = value
+ remainingVelocity = this.velocity
+ // avoid rounding errors and stop if anything is unconsumed
+ if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
+ }
+ }
+ // Snap if animation specs were provided.
+ if (snapAnimationSpec != null) {
+ if (state.heightOffset < 0 &&
+ state.heightOffset > state.heightOffsetLimit
+ ) {
+ AnimationState(initialValue = state.heightOffset).animateTo(
+ if (state.collapsedFraction < 0.5f) {
+ 0f
+ } else {
+ state.heightOffsetLimit
+ },
+ animationSpec = snapAnimationSpec
+ ) { state.heightOffset = value }
+ }
+ }
+
+ return Velocity(0f, remainingVelocity)
}
// Padding minus IconButton's min touch target expansion
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
index b346d44..fea69f4 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt
@@ -197,7 +197,7 @@
layoutWidth - FabSpacing.roundToPx() - fabWidth
}
}
- FabPosition.End -> {
+ FabPosition.End, FabPosition.EndOverlay -> {
if (layoutDirection == LayoutDirection.Ltr) {
layoutWidth - FabSpacing.roundToPx() - fabWidth
} else {
@@ -225,7 +225,7 @@
val bottomBarHeight = bottomBarPlaceables.fastMaxBy { it.height }?.height
val fabOffsetFromBottom = fabPlacement?.let {
- if (bottomBarHeight == null) {
+ if (bottomBarHeight == null || fabPosition == FabPosition.EndOverlay) {
it.height + FabSpacing.roundToPx() +
contentWindowInsets.getBottom(this@SubcomposeLayout)
} else {
@@ -329,13 +329,20 @@
* exists)
*/
val End = FabPosition(2)
+
+ /**
+ * Position FAB at the bottom of the screen at the end, overlaying the [NavigationBar] (if
+ * it exists)
+ */
+ val EndOverlay = FabPosition(3)
}
override fun toString(): String {
return when (this) {
Start -> "FabPosition.Start"
Center -> "FabPosition.Center"
- else -> "FabPosition.End"
+ End -> "FabPosition.End"
+ else -> "FabPosition.EndOverlay"
}
}
}