[go: nahoru, domu]

Adds a Material3 SmallCenteredTopAppBar support.

- SmallCenteredTopAppBar implementation that horizontally center the top
app bar title.
- EnterAlwaysScrollBehavior that can be attached to the app bar and have
it collapsed when scrolling up, and immediately expand when scrolling
down.
- Sample & tests.

Bug: 198144133
Test: Manual & AppBarTest
Relnote: "Adds Material 3 small-centered top app bar support with an enter-always scroll behavior."
Change-Id: I787a809602b41b9f3c0b859df48daa4c370165d5
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 7ae7581..05ecf1a 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -2,6 +2,7 @@
 package androidx.compose.material3 {
 
   public final class AppBarKt {
+    method @androidx.compose.runtime.Composable public static void SmallCenteredTopAppBar(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.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @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.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
   }
 
@@ -128,7 +129,9 @@
   }
 
   public final class TopAppBarDefaults {
+    method public androidx.compose.material3.TopAppBarScrollBehavior enterAlwaysScrollBehavior(optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll);
     method public androidx.compose.material3.TopAppBarScrollBehavior pinnedScrollBehavior(optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll);
+    method @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarColors smallCenteredTopAppBarColors(optional long containerColor, optional long scrolledContainerColor, optional long navigationIconColor, optional long titleColor, optional long actionIconsColor);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarColors smallTopAppBarColors(optional long containerColor, optional long scrolledContainerColor, optional long navigationIconColor, optional long titleColor, optional long actionIconsColor);
     field public static final androidx.compose.material3.TopAppBarDefaults INSTANCE;
   }
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index e5e7d9c..28ecd4a 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -2,6 +2,7 @@
 package androidx.compose.material3 {
 
   public final class AppBarKt {
+    method @androidx.compose.runtime.Composable public static void SmallCenteredTopAppBar(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.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @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.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
   }
 
@@ -143,7 +144,9 @@
   }
 
   public final class TopAppBarDefaults {
+    method public androidx.compose.material3.TopAppBarScrollBehavior enterAlwaysScrollBehavior(optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll);
     method public androidx.compose.material3.TopAppBarScrollBehavior pinnedScrollBehavior(optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll);
+    method @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarColors smallCenteredTopAppBarColors(optional long containerColor, optional long scrolledContainerColor, optional long navigationIconColor, optional long titleColor, optional long actionIconsColor);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarColors smallTopAppBarColors(optional long containerColor, optional long scrolledContainerColor, optional long navigationIconColor, optional long titleColor, optional long actionIconsColor);
     field public static final androidx.compose.material3.TopAppBarDefaults INSTANCE;
   }
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 7ae7581..05ecf1a 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -2,6 +2,7 @@
 package androidx.compose.material3 {
 
   public final class AppBarKt {
+    method @androidx.compose.runtime.Composable public static void SmallCenteredTopAppBar(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.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
     method @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.material3.TopAppBarColors colors, optional androidx.compose.material3.TopAppBarScrollBehavior? scrollBehavior);
   }
 
@@ -128,7 +129,9 @@
   }
 
   public final class TopAppBarDefaults {
+    method public androidx.compose.material3.TopAppBarScrollBehavior enterAlwaysScrollBehavior(optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll);
     method public androidx.compose.material3.TopAppBarScrollBehavior pinnedScrollBehavior(optional kotlin.jvm.functions.Function0<java.lang.Boolean> canScroll);
+    method @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarColors smallCenteredTopAppBarColors(optional long containerColor, optional long scrolledContainerColor, optional long navigationIconColor, optional long titleColor, optional long actionIconsColor);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.TopAppBarColors smallTopAppBarColors(optional long containerColor, optional long scrolledContainerColor, optional long navigationIconColor, optional long titleColor, optional long actionIconsColor);
     field public static final androidx.compose.material3.TopAppBarDefaults INSTANCE;
   }
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 976fa17..0457449 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
@@ -20,7 +20,9 @@
 
 import androidx.compose.material3.catalog.library.util.SampleSourceUrl
 import androidx.compose.material3.samples.ColorSchemeSample
+import androidx.compose.material3.samples.EnterAlwaysSmallTopAppBar
 import androidx.compose.material3.samples.PinnedSmallTopAppBar
+import androidx.compose.material3.samples.SimpleCenteredTopAppBar
 import androidx.compose.material3.samples.SimpleSmallTopAppBar
 import androidx.compose.runtime.Composable
 
@@ -52,8 +54,18 @@
             sourceUrl = TopAppBarExampleSourceUrl,
         ) { SimpleSmallTopAppBar() },
         Example(
+            name = ::SimpleCenteredTopAppBar.name,
+            description = TopAppBarExampleDescription,
+            sourceUrl = TopAppBarExampleSourceUrl,
+        ) { SimpleCenteredTopAppBar() },
+        Example(
             name = ::PinnedSmallTopAppBar.name,
             description = TopAppBarExampleDescription,
             sourceUrl = TopAppBarExampleSourceUrl,
         ) { PinnedSmallTopAppBar() },
+        Example(
+            name = ::EnterAlwaysSmallTopAppBar.name,
+            description = TopAppBarExampleDescription,
+            sourceUrl = TopAppBarExampleSourceUrl,
+        ) { EnterAlwaysSmallTopAppBar() },
     )
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 22be1a7..17aee6d 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
@@ -29,6 +29,7 @@
 import androidx.compose.material3.IconButton
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SmallCenteredTopAppBar
 import androidx.compose.material3.SmallTopAppBar
 import androidx.compose.material3.Text
 import androidx.compose.material3.TopAppBarDefaults
@@ -57,7 +58,49 @@
                     }
                 },
                 actions = {
-                    // RowScope here, so these icons will be placed horizontally
+                    IconButton( /* doSomething() */ }) {
+                        Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+                    }
+                }
+            )
+        },
+        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)
+                    )
+                }
+            }
+        }
+    )
+}
+
+/**
+ * A sample for a simple use of [SmallCenteredTopAppBar].
+ *
+ * The top app bar here does not react to any scroll events in the content under it.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Sampled
+@Composable
+fun SimpleCenteredTopAppBar() {
+    Scaffold(
+        topBar = {
+            SmallCenteredTopAppBar(
+                title = { Text("Centered TopAppBar") },
+                navigationIcon = {
+                    IconButton( /* doSomething() */ }) {
+                        Icon(Icons.Filled.Menu, contentDescription = "Localized description")
+                    }
+                },
+                actions = {
                     IconButton( /* doSomething() */ }) {
                         Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
                     }
@@ -132,3 +175,48 @@
         }
     )
 }
+
+/**
+ * A sample for a [SmallTopAppBar] that collapses when the content is scrolled up, and
+ * appears when the content scrolled down.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Sampled
+@Composable
+fun EnterAlwaysSmallTopAppBar() {
+    val scrollBehavior = remember { TopAppBarDefaults.enterAlwaysScrollBehavior() }
+    Scaffold(
+        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+        topBar = {
+            SmallTopAppBar(
+                title = { Text("Small TopAppBar") },
+                navigationIcon = {
+                    IconButton( /* doSomething() */ }) {
+                        Icon(Icons.Filled.Menu, contentDescription = "Localized description")
+                    }
+                },
+                actions = {
+                    IconButton( /* doSomething() */ }) {
+                        Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+                    }
+                },
+                scrollBehavior = scrollBehavior
+            )
+        },
+        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/AppBarTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/AppBarTest.kt
index 0ade19d..5411019 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
@@ -19,11 +19,13 @@
 import android.os.Build
 import androidx.compose.foundation.layout.Box
 import androidx.compose.material3.tokens.TopAppBarSmall
+import androidx.compose.material3.tokens.TopAppBarSmallCentered
 import androidx.compose.runtime.Composable
 import androidx.compose.testutils.assertContainsColor
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.painter.ColorPainter
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.test.assertHeightIsEqualTo
@@ -98,33 +100,7 @@
                 )
             }
         }
-
-        val appBarBounds = rule.onNodeWithTag(TopAppBarTestTag).getUnclippedBoundsInRoot()
-        val titleBounds = rule.onNodeWithTag(TitleTestTag).getUnclippedBoundsInRoot()
-        val appBarBottomEdgeY = appBarBounds.top + appBarBounds.height
-
-        rule.onNodeWithTag(NavigationIconTestTag)
-            // Navigation icon should be 4.dp from the start
-            .assertLeftPositionInRootIsEqualTo(AppBarStartAndEndPadding)
-            // Navigation icon should be centered within the height of the app bar.
-            .assertTopPositionInRootIsEqualTo(
-                appBarBottomEdgeY - AppBarTopAndBottomPadding - FakeIconSize
-            )
-
-        rule.onNodeWithTag(TitleTestTag)
-            // Title should be 56.dp from the start
-            // 4.dp padding for the whole app bar + 48.dp icon size + 4.dp title padding.
-            .assertLeftPositionInRootIsEqualTo(4.dp + FakeIconSize + 4.dp)
-            // Title should be vertically centered
-            .assertTopPositionInRootIsEqualTo((appBarBounds.height - titleBounds.height) / 2)
-
-        rule.onNodeWithTag(ActionsTestTag)
-            // Action should be placed at the end
-            .assertLeftPositionInRootIsEqualTo(expectedActionPosition(appBarBounds.width))
-            // Action should be 8.dp from the top
-            .assertTopPositionInRootIsEqualTo(
-                appBarBottomEdgeY - AppBarTopAndBottomPadding - FakeIconSize
-            )
+        assertSmallDefaultPositioning()
     }
 
     @Test
@@ -141,17 +117,7 @@
                 )
             }
         }
-
-        val appBarBounds = rule.onNodeWithTag(TopAppBarTestTag).getUnclippedBoundsInRoot()
-
-        rule.onNodeWithTag(TitleTestTag)
-            // Title should now be placed 16.dp from the start, as there is no navigation icon
-            // 4.dp padding for the whole app bar + 12.dp inset
-            .assertLeftPositionInRootIsEqualTo(4.dp + 12.dp)
-
-        rule.onNodeWithTag(ActionsTestTag)
-            // Action should still be placed at the end
-            .assertLeftPositionInRootIsEqualTo(expectedActionPosition(appBarBounds.width))
+        assertSmallPositioningWithoutNavigation()
     }
 
     @Test
@@ -247,12 +213,251 @@
         }
 
         // Simulate scrolled content.
-        scrollBehavior.contentOffset = -100f
+        rule.runOnIdle {
+            scrollBehavior.contentOffset = -100f
+        }
         rule.waitForIdle()
         rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
             .assertContainsColor(expectedScrolledContainerColor)
     }
 
+    @Test
+    fun smallTopAppBar_scrolledPositioning() {
+        val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
+        val scrollOffsetDp = 20.dp
+        var scrollOffsetPx = 0f
+
+        rule.setMaterialContent {
+            SmallTopAppBar(
+                modifier = Modifier.testTag(TopAppBarTestTag),
+                title = {
+                    Text("Title", Modifier.testTag(TitleTestTag))
+                    scrollOffsetPx = with(LocalDensity.current) { scrollOffsetDp.toPx() }
+                },
+                scrollBehavior = scrollBehavior
+            )
+        }
+
+        // Simulate scrolled content.
+        rule.runOnIdle {
+            scrollBehavior.offset = -scrollOffsetPx
+            scrollBehavior.contentOffset = -scrollOffsetPx
+        }
+        rule.waitForIdle()
+        rule.onNodeWithTag(TopAppBarTestTag)
+            .assertHeightIsEqualTo(TopAppBarSmall.SmallContainerHeight - scrollOffsetDp)
+    }
+
+    @Test
+    fun smallCenteredTopAppBar_expandsToScreen() {
+        rule.setMaterialContentForSizeAssertions {
+            SmallCenteredTopAppBar(title = { Text("Title") })
+        }
+            .assertHeightIsEqualTo(TopAppBarSmallCentered.SmallCenteredContainerHeight)
+            .assertWidthIsEqualTo(rule.rootWidth())
+    }
+
+    @Test
+    fun smallCenteredTopAppBar_withTitle() {
+        val title = "Title"
+        rule.setMaterialContent {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                SmallCenteredTopAppBar(title = { Text(title) })
+            }
+        }
+        rule.onNodeWithText(title).assertIsDisplayed()
+    }
+
+    @Test
+    fun smallCenteredTopAppBar_default_positioning() {
+        rule.setMaterialContent {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                SmallTopAppBar(
+                    navigationIcon = {
+                        FakeIcon(Modifier.testTag(NavigationIconTestTag))
+                    },
+                    title = {
+                        Text("Title", Modifier.testTag(TitleTestTag))
+                    },
+                    actions = {
+                        FakeIcon(Modifier.testTag(ActionsTestTag))
+                    }
+                )
+            }
+        }
+        assertSmallDefaultPositioning()
+    }
+
+    @Test
+    fun smallCenteredTopAppBar_noNavigationIcon_positioning() {
+        rule.setMaterialContent {
+            Box(Modifier.testTag(TopAppBarTestTag)) {
+                SmallTopAppBar(
+                    title = {
+                        Text("Title", Modifier.testTag(TitleTestTag))
+                    },
+                    actions = {
+                        FakeIcon(Modifier.testTag(ActionsTestTag))
+                    }
+                )
+            }
+        }
+        assertSmallPositioningWithoutNavigation()
+    }
+
+    @Test
+    fun smallCenteredTopAppBar_titleDefaultStyle() {
+        var textStyle: TextStyle? = null
+        var expectedTextStyle: TextStyle? = null
+        rule.setMaterialContent {
+            SmallCenteredTopAppBar(
+                title = {
+                    Text("Title")
+                    textStyle = LocalTextStyle.current
+                    expectedTextStyle =
+                        MaterialTheme.typography.fromToken(
+                            TopAppBarSmallCentered.SmallCenteredHeadlineFont
+                        )
+                }
+            )
+        }
+        assertThat(textStyle).isNotNull()
+        assertThat(textStyle).isEqualTo(expectedTextStyle)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun smallCenteredTopAppBar_contentColor() {
+        var titleColor: Color = Color.Unspecified
+        var navigationIconColor: Color = Color.Unspecified
+        var actionsColor: Color = Color.Unspecified
+        var expectedTitleColor: Color = Color.Unspecified
+        var expectedNavigationIconColor: Color = Color.Unspecified
+        var expectedActionsColor: Color = Color.Unspecified
+        var expectedContainerColor: Color = Color.Unspecified
+
+        rule.setMaterialContent {
+            SmallCenteredTopAppBar(
+                modifier = Modifier.testTag(TopAppBarTestTag),
+                navigationIcon = {
+                    FakeIcon(Modifier.testTag(NavigationIconTestTag))
+                    navigationIconColor = LocalContentColor.current
+                    expectedNavigationIconColor =
+                        TopAppBarDefaults.smallCenteredTopAppBarColors()
+                            .navigationIconColor(scrollFraction = 0f).value
+                    // scrollFraction = 0f to indicate no scroll.
+                    expectedContainerColor =
+                        TopAppBarDefaults.smallCenteredTopAppBarColors()
+                            .containerColor(scrollFraction = 0f).value
+                },
+                title = {
+                    Text("Title", Modifier.testTag(TitleTestTag))
+                    titleColor = LocalContentColor.current
+                    expectedTitleColor =
+                        TopAppBarDefaults.smallCenteredTopAppBarColors()
+                            .titleColor(scrollFraction = 0f).value
+                },
+                actions = {
+                    FakeIcon(Modifier.testTag(ActionsTestTag))
+                    actionsColor = LocalContentColor.current
+                    expectedActionsColor =
+                        TopAppBarDefaults.smallCenteredTopAppBarColors()
+                            .actionIconColor(scrollFraction = 0f).value
+                }
+            )
+        }
+        assertThat(navigationIconColor).isNotNull()
+        assertThat(titleColor).isNotNull()
+        assertThat(actionsColor).isNotNull()
+        assertThat(navigationIconColor).isEqualTo(expectedNavigationIconColor)
+        assertThat(titleColor).isEqualTo(expectedTitleColor)
+        assertThat(actionsColor).isEqualTo(expectedActionsColor)
+
+        rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
+            .assertContainsColor(expectedContainerColor)
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun smallCenteredTopAppBar_scrolledContentColor() {
+        var expectedScrolledContainerColor: Color = Color.Unspecified
+        val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
+
+        rule.setMaterialContent {
+            SmallCenteredTopAppBar(
+                modifier = Modifier.testTag(TopAppBarTestTag),
+                title = {
+                    Text("Title", Modifier.testTag(TitleTestTag))
+                    // scrollFraction = 1f to indicate a scroll.
+                    expectedScrolledContainerColor =
+                        TopAppBarDefaults.smallCenteredTopAppBarColors()
+                            .containerColor(scrollFraction = 1f).value
+                },
+                scrollBehavior = scrollBehavior
+            )
+        }
+
+        // Simulate scrolled content.
+        rule.runOnIdle {
+            scrollBehavior.contentOffset = -100f
+        }
+        rule.waitForIdle()
+        rule.onNodeWithTag(TopAppBarTestTag).captureToImage()
+            .assertContainsColor(expectedScrolledContainerColor)
+    }
+
+    /**
+     * Checks the app bar's components positioning when it's a [SmallTopAppBar], a
+     * [SmallCenteredTopAppBar], or a larger app bar that is scrolled up and collapsed into a small
+     * configuration and there is no navigation icon.
+     */
+    private fun assertSmallPositioningWithoutNavigation() {
+        val appBarBounds = rule.onNodeWithTag(TopAppBarTestTag).getUnclippedBoundsInRoot()
+
+        rule.onNodeWithTag(TitleTestTag)
+            // Title should now be placed 16.dp from the start, as there is no navigation icon
+            // 4.dp padding for the whole app bar + 12.dp inset
+            .assertLeftPositionInRootIsEqualTo(4.dp + 12.dp)
+
+        rule.onNodeWithTag(ActionsTestTag)
+            // Action should still be placed at the end
+            .assertLeftPositionInRootIsEqualTo(expectedActionPosition(appBarBounds.width))
+    }
+
+    /**
+     * Checks the app bar's components positioning when it's a [SmallTopAppBar], a
+     * [SmallCenteredTopAppBar], or a larger app bar that is scrolled up and collapsed into a small
+     * configuration.
+     */
+    private fun assertSmallDefaultPositioning() {
+        val appBarBounds = rule.onNodeWithTag(TopAppBarTestTag).getUnclippedBoundsInRoot()
+        val titleBounds = rule.onNodeWithTag(TitleTestTag).getUnclippedBoundsInRoot()
+        val appBarBottomEdgeY = appBarBounds.top + appBarBounds.height
+
+        rule.onNodeWithTag(NavigationIconTestTag)
+            // Navigation icon should be 4.dp from the start
+            .assertLeftPositionInRootIsEqualTo(AppBarStartAndEndPadding)
+            // Navigation icon should be centered within the height of the app bar.
+            .assertTopPositionInRootIsEqualTo(
+                appBarBottomEdgeY - AppBarTopAndBottomPadding - FakeIconSize
+            )
+
+        rule.onNodeWithTag(TitleTestTag)
+            // Title should be 56.dp from the start
+            // 4.dp padding for the whole app bar + 48.dp icon size + 4.dp title padding.
+            .assertLeftPositionInRootIsEqualTo(4.dp + FakeIconSize + 4.dp)
+            // Title should be vertically centered
+            .assertTopPositionInRootIsEqualTo((appBarBounds.height - titleBounds.height) / 2)
+
+        rule.onNodeWithTag(ActionsTestTag)
+            // Action should be placed at the end
+            .assertLeftPositionInRootIsEqualTo(expectedActionPosition(appBarBounds.width))
+            // Action should be 8.dp from the top
+            .assertTopPositionInRootIsEqualTo(
+                appBarBottomEdgeY - AppBarTopAndBottomPadding - FakeIconSize
+            )
+    }
+
     /**
      * An [IconButton] with an [Icon] inside for testing positions.
      *
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 8945dfc..b4ccdca 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
@@ -25,6 +25,7 @@
 import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.tokens.TopAppBarSmall
+import androidx.compose.material3.tokens.TopAppBarSmallCentered
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.SideEffect
@@ -62,6 +63,7 @@
  * A top 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.PinnedSmallTopAppBar
+ * @sample androidx.compose.material3.samples.EnterAlwaysSmallTopAppBar
  *
  * @param title the title to be displayed in the top app bar
  * @param modifier the [Modifier] to be applied to this top app bar
@@ -98,6 +100,52 @@
 }
 
 /**
+ * Material Design small top app bar with a header title that is horizontally aligned to the center.
+ *
+ * The top app bar displays information and actions relating to the current screen.
+ *
+ * This SmallCenteredTopAppBar has slots for a title, navigation icon, and actions.
+ *
+ * A small centered top 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.SimpleCenteredTopAppBar
+ *
+ * @param title the title to be displayed in the top app bar
+ * @param modifier the [Modifier] to be applied to this top app bar
+ * @param navigationIcon The navigation icon displayed at the start of the top app bar. This should
+ * typically be an [IconButton] or [IconToggleButton].
+ * @param actions the actions displayed at the end of the top app bar. This should typically be
+ * [IconButton]s. The default layout here is a [Row], so icons inside will be placed horizontally.
+ * @param colors a [TopAppBarColors] that will be used to resolve the colors used for this top app
+ * bar in different states. See [TopAppBarDefaults.smallCenteredTopAppBarColors].
+ * @param scrollBehavior a [TopAppBarScrollBehavior] which holds various offset values that will be
+ * applied by this top app bar to set up its height and colors. A scroll behavior is designed to
+ * work in conjunction with a scrolled content to change the top app bar appearance as the content
+ * scrolls. See [TopAppBarScrollBehavior.nestedScrollConnection].
+ */
+@Composable
+fun SmallCenteredTopAppBar(
+    title: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    navigationIcon: @Composable () -> Unit = {},
+    actions: @Composable RowScope.() -> Unit = {},
+    colors: TopAppBarColors = TopAppBarDefaults.smallCenteredTopAppBarColors(),
+    scrollBehavior: TopAppBarScrollBehavior? = null
+) {
+    SingleRowTopAppBar(
+        modifier = modifier,
+        title = title,
+        titleTextStyle =
+        MaterialTheme.typography.fromToken(TopAppBarSmall.SmallHeadlineFont),
+        centeredTitle = true,
+        navigationIcon = navigationIcon,
+        actions = actions,
+        colors = colors,
+        scrollBehavior = scrollBehavior
+    )
+}
+
+/**
  * A TopAppBarScrollBehavior defines how an app bar should behave when the content under it is
  * scrolled.
  *
@@ -247,14 +295,79 @@
     }
 
     /**
+     * Creates a [TopAppBarColors] for small-centered top app bars. The default implementation
+     * animates between the provided colors according to the Material specification.
+     *
+     * @param containerColor the container color
+     * @param scrolledContainerColor the container color when content is scrolled behind it
+     * @param navigationIconColor the content used color for the navigation icon
+     * @param titleColor the content color used for the title
+     * @param actionIconsColor the content color used for actions
+     * @return the resulting [TopAppBarColors] used for the top app bar
+     */
+    @Composable
+    fun smallCenteredTopAppBarColors(
+        containerColor: Color =
+            MaterialTheme.colorScheme.fromToken(TopAppBarSmallCentered.SmallCenteredContainerColor),
+        scrolledContainerColor: Color = MaterialTheme.colorScheme.applyTonalElevation(
+            backgroundColor = containerColor,
+            elevation = TopAppBarSmall.SmallOnScrollContainerElevation
+        ),
+        navigationIconColor: Color =
+            MaterialTheme.colorScheme.fromToken(
+                TopAppBarSmallCentered.SmallCenteredLeadingIconColor
+            ),
+        titleColor: Color =
+            MaterialTheme.colorScheme.fromToken(
+                TopAppBarSmallCentered.SmallCenteredHeadlineColor
+            ),
+        actionIconsColor: Color =
+            MaterialTheme.colorScheme.fromToken(
+                TopAppBarSmallCentered.SmallCenteredTrailingIconColor
+            )
+    ): TopAppBarColors {
+        return remember(
+            containerColor,
+            scrolledContainerColor,
+            navigationIconColor,
+            titleColor,
+            actionIconsColor
+        ) {
+            AnimatingTopAppBarColors(
+                containerColor,
+                scrolledContainerColor,
+                navigationIconColor,
+                titleColor,
+                actionIconsColor
+            )
+        }
+    }
+
+    /**
      * Returns a pinned [TopAppBarScrollBehavior] that tracks nested-scroll callbacks and
      * updates its [TopAppBarScrollBehavior.contentOffset] accordingly.
      *
-     * @param canScroll a callback used to determine whether scroll events are to be
-     * handled by this pinned [TopAppBarScrollBehavior]
+     * @param canScroll a callback used to determine whether scroll events are to be handled by this
+     * pinned [TopAppBarScrollBehavior]
      */
     fun pinnedScrollBehavior(canScroll: () -> Boolean = { true }): TopAppBarScrollBehavior =
         PinnedScrollBehavior(canScroll)
+
+    /**
+     * Returns a [TopAppBarScrollBehavior] that tracks nested-scroll callbacks and
+     * updates its [TopAppBarScrollBehavior.offset] and [TopAppBarScrollBehavior.contentOffset]
+     * accordingly.
+     *
+     * This scroll-connection updates the `offset` value immediately whenever the content is pulled
+     * up or down. A top-bar that is set up with this [TopAppBarScrollBehavior] managed by this
+     * [EnterAlwaysScrollBehavior] will immediately collapse when the scaffold's content is pulled
+     * up, and will immediately appear when the content is pulled down.
+     *
+     * @param canScroll a callback used to determine whether scroll events are to be handled by this
+     * [EnterAlwaysScrollBehavior]
+     */
+    fun enterAlwaysScrollBehavior(canScroll: () -> Boolean = { true }): TopAppBarScrollBehavior =
+        EnterAlwaysScrollBehavior(canScroll)
 }
 
 /**
@@ -321,15 +434,6 @@
     }
 }
 
-@Composable
-private fun ActionsInRow(content: @Composable (RowScope.() -> Unit)) {
-    Row(
-        horizontalArrangement = Arrangement.End,
-        verticalAlignment = Alignment.CenterVertically,
-        content = content
-    )
-}
-
 /**
  * The base [Layout] for all top app bars. This function lays out a top app bar navigation icon
  * (leading icon), a title (header), and action icons (trailing icons). Note that the navigation and
@@ -544,6 +648,68 @@
         }
 }
 
+/**
+ * A [TopAppBarScrollBehavior] that adjusts its properties to affect the height of the top app bar.
+ *
+ * A top-bar that is set up with this [TopAppBarScrollBehavior] will immediately collapse when the
+ * scaffold's content is pulled up, and will immediately appear when the content is pulled down.
+ *
+ * @param canScroll a callback that can be used to determine whether scroll events are to be
+ * handled by this [EnterAlwaysScrollBehavior]
+ */
+private class EnterAlwaysScrollBehavior(val canScroll: () -> Boolean = { true }) :
+    TopAppBarScrollBehavior {
+    override val scrollFraction: Float
+        get() = if (offsetLimit != 0f) {
+            1 - ((offsetLimit - contentOffset).coerceIn(
+                minimumValue = offsetLimit,
+                maximumValue = 0f
+            ) / offsetLimit)
+        } else {
+            0f
+        }
+    override var offsetLimit by mutableStateOf(-Float.MAX_VALUE)
+    override var offset by mutableStateOf(0f)
+    override var contentOffset by mutableStateOf(0f)
+    override var nestedScrollConnection =
+        object : NestedScrollConnection {
+            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+                if (!canScroll()) return Offset.Zero
+                val newOffset = (offset + available.y)
+                val coerced = newOffset.coerceIn(minimumValue = offsetLimit, maximumValue = 0f)
+                return if (newOffset == coerced) {
+                    // Nothing coerced, meaning we're in the middle of top-bar collapse or
+                    // expand.
+                    offset = coerced
+                    available
+                } else {
+                    Offset.Zero
+                }
+            }
+
+            override fun onPostScroll(
+                consumed: Offset,
+                available: Offset,
+                source: NestedScrollSource
+            ): Offset {
+                if (!canScroll()) return Offset.Zero
+                contentOffset += consumed.y
+                if (offset == 0f || offset == offsetLimit) {
+                    if (consumed.y == 0f && available.y > 0f) {
+                        // Reset the total offset to zero when scrolling all the way down.
+                        // This will eliminate some float precision inaccuracies.
+                        contentOffset = 0f
+                    }
+                }
+                offset = (offset + consumed.y).coerceIn(
+                    minimumValue = offsetLimit,
+                    maximumValue = 0f
+                )
+                return Offset.Zero
+            }
+        }
+}
+
 private val TopAppBarHorizontalPadding = 4.dp
 
 // A title inset when the App-Bar is a Medium or Large one. Also used to size a spacer when the