[go: nahoru, domu]

Refactors Scaffold to use SubcomposeLayout, avoiding frame delays in calculating the content padding / FAB cutout location

Also removes nullable type from Composable lambda parameters, these now just default to emptyContent()

Bug: b/157633857
Bug: b/158551084
Test: ScaffoldTest
Test: ScaffoldScreenshotTest
Relnote: "Removes nullable type from Scaffold lambda parameters, you can use emptyContent() to represent no content for a given parameter."
Change-Id: I2b3181de9314b8b0dd48c30e2f663cddc0d62448
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index 64a5efc..64540fc 100644
--- a/compose/material/material/api/current.txt
+++ b/compose/material/material/api/current.txt
@@ -497,7 +497,7 @@
   }
 
   public final class ScaffoldKt {
-    method @androidx.compose.runtime.Composable public static void Scaffold-6SxdeRQ(androidx.compose.ui.Modifier modifier = Modifier, androidx.compose.material.ScaffoldState scaffoldState = rememberScaffoldState(), kotlin.jvm.functions.Function0<kotlin.Unit>? topBar = null, kotlin.jvm.functions.Function0<kotlin.Unit>? bottomBar = null, kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarHostState,kotlin.Unit> snackbarHost = { SnackbarHost(it) }, kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton = null, androidx.compose.material.FabPosition floatingActionButtonPosition = androidx.compose.material.FabPosition.End, boolean isFloatingActionButtonDocked = false, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? drawerContent = null, androidx.compose.ui.graphics.Shape drawerShape = large, float drawerElevation = DrawerConstants.DefaultElevation, long drawerBackgroundColor = MaterialTheme.colors.surface, long drawerContentColor = contentColorFor(drawerBackgroundColor), long drawerScrimColor = DrawerConstants.defaultScrimColor, long backgroundColor = MaterialTheme.colors.background, long contentColor = contentColorFor(backgroundColor), kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> bodyContent);
+    method @androidx.compose.runtime.Composable public static void Scaffold-qcZBNiw(androidx.compose.ui.Modifier modifier = Modifier, androidx.compose.material.ScaffoldState scaffoldState = rememberScaffoldState(), kotlin.jvm.functions.Function0<kotlin.Unit> topBar = emptyContent(), kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar = emptyContent(), kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarHostState,kotlin.Unit> snackbarHost = { SnackbarHost(it) }, kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton = emptyContent(), androidx.compose.material.FabPosition floatingActionButtonPosition = androidx.compose.material.FabPosition.End, boolean isFloatingActionButtonDocked = false, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? drawerContent = null, androidx.compose.ui.graphics.Shape drawerShape = large, float drawerElevation = DrawerConstants.DefaultElevation, long drawerBackgroundColor = MaterialTheme.colors.surface, long drawerContentColor = contentColorFor(drawerBackgroundColor), long drawerScrimColor = DrawerConstants.defaultScrimColor, long backgroundColor = MaterialTheme.colors.background, long contentColor = contentColorFor(backgroundColor), kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> bodyContent);
     method @androidx.compose.runtime.Composable public static androidx.compose.material.ScaffoldState rememberScaffoldState(androidx.compose.material.DrawerState drawerState = rememberDrawerState(DrawerValue.Closed), androidx.compose.material.SnackbarHostState snackbarHostState = androidx.compose.material.SnackbarHostState(), boolean isDrawerGesturesEnabled = true);
   }
 
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index 64a5efc..64540fc 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -497,7 +497,7 @@
   }
 
   public final class ScaffoldKt {
-    method @androidx.compose.runtime.Composable public static void Scaffold-6SxdeRQ(androidx.compose.ui.Modifier modifier = Modifier, androidx.compose.material.ScaffoldState scaffoldState = rememberScaffoldState(), kotlin.jvm.functions.Function0<kotlin.Unit>? topBar = null, kotlin.jvm.functions.Function0<kotlin.Unit>? bottomBar = null, kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarHostState,kotlin.Unit> snackbarHost = { SnackbarHost(it) }, kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton = null, androidx.compose.material.FabPosition floatingActionButtonPosition = androidx.compose.material.FabPosition.End, boolean isFloatingActionButtonDocked = false, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? drawerContent = null, androidx.compose.ui.graphics.Shape drawerShape = large, float drawerElevation = DrawerConstants.DefaultElevation, long drawerBackgroundColor = MaterialTheme.colors.surface, long drawerContentColor = contentColorFor(drawerBackgroundColor), long drawerScrimColor = DrawerConstants.defaultScrimColor, long backgroundColor = MaterialTheme.colors.background, long contentColor = contentColorFor(backgroundColor), kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> bodyContent);
+    method @androidx.compose.runtime.Composable public static void Scaffold-qcZBNiw(androidx.compose.ui.Modifier modifier = Modifier, androidx.compose.material.ScaffoldState scaffoldState = rememberScaffoldState(), kotlin.jvm.functions.Function0<kotlin.Unit> topBar = emptyContent(), kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar = emptyContent(), kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarHostState,kotlin.Unit> snackbarHost = { SnackbarHost(it) }, kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton = emptyContent(), androidx.compose.material.FabPosition floatingActionButtonPosition = androidx.compose.material.FabPosition.End, boolean isFloatingActionButtonDocked = false, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? drawerContent = null, androidx.compose.ui.graphics.Shape drawerShape = large, float drawerElevation = DrawerConstants.DefaultElevation, long drawerBackgroundColor = MaterialTheme.colors.surface, long drawerContentColor = contentColorFor(drawerBackgroundColor), long drawerScrimColor = DrawerConstants.defaultScrimColor, long backgroundColor = MaterialTheme.colors.background, long contentColor = contentColorFor(backgroundColor), kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> bodyContent);
     method @androidx.compose.runtime.Composable public static androidx.compose.material.ScaffoldState rememberScaffoldState(androidx.compose.material.DrawerState drawerState = rememberDrawerState(DrawerValue.Closed), androidx.compose.material.SnackbarHostState snackbarHostState = androidx.compose.material.SnackbarHostState(), boolean isDrawerGesturesEnabled = true);
   }
 
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index 64a5efc..64540fc 100644
--- a/compose/material/material/api/restricted_current.txt
+++ b/compose/material/material/api/restricted_current.txt
@@ -497,7 +497,7 @@
   }
 
   public final class ScaffoldKt {
-    method @androidx.compose.runtime.Composable public static void Scaffold-6SxdeRQ(androidx.compose.ui.Modifier modifier = Modifier, androidx.compose.material.ScaffoldState scaffoldState = rememberScaffoldState(), kotlin.jvm.functions.Function0<kotlin.Unit>? topBar = null, kotlin.jvm.functions.Function0<kotlin.Unit>? bottomBar = null, kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarHostState,kotlin.Unit> snackbarHost = { SnackbarHost(it) }, kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton = null, androidx.compose.material.FabPosition floatingActionButtonPosition = androidx.compose.material.FabPosition.End, boolean isFloatingActionButtonDocked = false, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? drawerContent = null, androidx.compose.ui.graphics.Shape drawerShape = large, float drawerElevation = DrawerConstants.DefaultElevation, long drawerBackgroundColor = MaterialTheme.colors.surface, long drawerContentColor = contentColorFor(drawerBackgroundColor), long drawerScrimColor = DrawerConstants.defaultScrimColor, long backgroundColor = MaterialTheme.colors.background, long contentColor = contentColorFor(backgroundColor), kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> bodyContent);
+    method @androidx.compose.runtime.Composable public static void Scaffold-qcZBNiw(androidx.compose.ui.Modifier modifier = Modifier, androidx.compose.material.ScaffoldState scaffoldState = rememberScaffoldState(), kotlin.jvm.functions.Function0<kotlin.Unit> topBar = emptyContent(), kotlin.jvm.functions.Function0<kotlin.Unit> bottomBar = emptyContent(), kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarHostState,kotlin.Unit> snackbarHost = { SnackbarHost(it) }, kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton = emptyContent(), androidx.compose.material.FabPosition floatingActionButtonPosition = androidx.compose.material.FabPosition.End, boolean isFloatingActionButtonDocked = false, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? drawerContent = null, androidx.compose.ui.graphics.Shape drawerShape = large, float drawerElevation = DrawerConstants.DefaultElevation, long drawerBackgroundColor = MaterialTheme.colors.surface, long drawerContentColor = contentColorFor(drawerBackgroundColor), long drawerScrimColor = DrawerConstants.defaultScrimColor, long backgroundColor = MaterialTheme.colors.background, long contentColor = contentColorFor(backgroundColor), kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> bodyContent);
     method @androidx.compose.runtime.Composable public static androidx.compose.material.ScaffoldState rememberScaffoldState(androidx.compose.material.DrawerState drawerState = rememberDrawerState(DrawerValue.Closed), androidx.compose.material.SnackbarHostState snackbarHostState = androidx.compose.material.SnackbarHostState(), boolean isDrawerGesturesEnabled = true);
   }
 
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialDemos.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialDemos.kt
index d9508e7..c5f7dde 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialDemos.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialDemos.kt
@@ -29,6 +29,8 @@
 import androidx.compose.material.samples.BottomSheetScaffoldSample
 import androidx.compose.material.samples.ScaffoldWithBottomBarAndCutout
 import androidx.compose.material.samples.ScaffoldWithCoroutinesSnackbar
+import androidx.compose.material.samples.ScaffoldWithSimpleSnackbar
+import androidx.compose.material.samples.SimpleScaffoldWithTopBar
 
 val MaterialDemos = DemoCategory(
     "Material",
@@ -72,7 +74,14 @@
         ),
         ComposableDemo("Modal bottom sheet") { ModalBottomSheetSample() },
         ComposableDemo("Progress Indicators") { ProgressIndicatorDemo() },
-        ComposableDemo("Scaffold") { ScaffoldWithBottomBarAndCutout() },
+        DemoCategory(
+            "Scaffold",
+            listOf(
+                ComposableDemo("Scaffold with top bar") { SimpleScaffoldWithTopBar() },
+                ComposableDemo("Scaffold with docked FAB") { ScaffoldWithBottomBarAndCutout() },
+                ComposableDemo("Scaffold with snackbar") { ScaffoldWithSimpleSnackbar() }
+            )
+        ),
         ComposableDemo("Selection Controls") { SelectionControlsDemo() },
         ComposableDemo("Slider") { SliderDemo() },
         ComposableDemo("Snackbar") { ScaffoldWithCoroutinesSnackbar() },
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldScreenshotTest.kt
index f9f02c0..965e693 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldScreenshotTest.kt
@@ -616,18 +616,22 @@
     fabPosition: FabPosition = FabPosition.End,
     rtl: Boolean = false
 ) {
-    val topAppBar = (@Composable {
-        TopAppBar(title = { Text("Scaffold") })
-    }).takeIf { showTopAppBar }
+    val topAppBar = @Composable {
+        if (showTopAppBar) {
+            TopAppBar(title = { Text("Scaffold") })
+        }
+    }
 
-    val bottomAppBar = (@Composable {
-        val cutoutShape = if (fabCutout) CircleShape else null
-        BottomAppBar(cutoutShape = cutoutShape) {
-            IconButton( {
-                Icon(Icons.Filled.Menu)
+    val bottomAppBar = @Composable {
+        if (showBottomAppBar) {
+            val cutoutShape = if (fabCutout) CircleShape else null
+            BottomAppBar(cutoutShape = cutoutShape) {
+                IconButton( {
+                    Icon(Icons.Filled.Menu)
+                }
             }
         }
-    }).takeIf { showBottomAppBar }
+    }
 
     val snackbar = @Composable {
         if (showSnackbar) {
@@ -642,12 +646,14 @@
         }
     }
 
-    val fab = (@Composable {
-        FloatingActionButton(
-            icon = { Icon(Icons.Filled.Favorite) },
-            >
-        )
-    }).takeIf { showFab }
+    val fab = @Composable {
+        if (showFab) {
+            FloatingActionButton(
+                icon = { Icon(Icons.Filled.Favorite) },
+                >
+            )
+        }
+    }
 
     val layoutDirection = if (rtl) LayoutDirection.Rtl else LayoutDirection.Ltr
 
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt
index 9c02629..d544ff7 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt
@@ -17,12 +17,12 @@
 package androidx.compose.material
 
 import android.os.Build
-import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.Icon
 import androidx.compose.foundation.Text
 import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.preferredHeight
 import androidx.compose.foundation.layout.size
@@ -36,12 +36,14 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.layout.positionInParent
+import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.onGloballyPositioned
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.toSize
+import androidx.compose.ui.zIndex
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
 import androidx.ui.test.assertHeightIsEqualTo
@@ -162,7 +164,7 @@
                             contentPosition = positioned.positionInParent
                             contentSize = positioned.size
                         }
-                        .fillMaxWidth()
+                        .fillMaxSize()
                         .preferredHeight(50.dp)
                         .background(color = Color.Blue)
                 )
@@ -281,7 +283,7 @@
                     FloatingActionButton(
                         modifier = Modifier.onGloballyPositioned { positioned ->
                             fabSize = positioned.size
-                            fabPosition = positioned.localToGlobal(positioned.positionInParent)
+                            fabPosition = positioned.positionInRoot
                         },
                         >
                     ) {
@@ -291,16 +293,12 @@
                 floatingActionButtonPosition = FabPosition.Center,
                 isFloatingActionButtonDocked = true,
                 bottomBar = {
-                    Box(
+                    BottomAppBar(
                         Modifier
                             .onGloballyPositioned { positioned: LayoutCoordinates ->
-                                bottomBarPosition =
-                                    positioned.localToGlobal(positioned.positionInParent)
+                                bottomBarPosition = positioned.positionInRoot
                             }
-                            .fillMaxWidth()
-                            .preferredHeight(100.dp)
-                            .background(color = Color.Red)
-                    )
+                    ) {}
                 }
             ) {
                 Text("body")
@@ -321,7 +319,7 @@
                     FloatingActionButton(
                         modifier = Modifier.onGloballyPositioned { positioned ->
                             fabSize = positioned.size
-                            fabPosition = positioned.localToGlobal(positioned.positionInParent)
+                            fabPosition = positioned.positionInRoot
                         },
                         >
                     ) {
@@ -331,16 +329,12 @@
                 floatingActionButtonPosition = FabPosition.End,
                 isFloatingActionButtonDocked = true,
                 bottomBar = {
-                    Box(
+                    BottomAppBar(
                         Modifier
                             .onGloballyPositioned { positioned: LayoutCoordinates ->
-                                bottomBarPosition =
-                                    positioned.localToGlobal(positioned.positionInParent)
+                                bottomBarPosition = positioned.positionInRoot
                             }
-                            .fillMaxWidth()
-                            .preferredHeight(100.dp)
-                            .background(color = Color.Red)
-                    )
+                    ) {}
                 }
             ) {
                 Text("body")
@@ -365,6 +359,7 @@
                         Box(
                             Modifier.size(10.dp)
                                 .drawShadow(4.dp)
+                                .zIndex(4f)
                                 .background(color = Color.White)
                         )
                     }
@@ -391,11 +386,10 @@
     fun scaffold_geometry_fabSize() {
         var fabSize: IntSize = IntSize.Zero
         val showFab = mutableStateOf(true)
-        lateinit var scaffoldState: ScaffoldState
+        var fabPlacement: FabPlacement? = null
         rule.setContent {
-            scaffoldState = rememberScaffoldState()
-            val fab: @Composable (() -> Unit)? = if (showFab.value) {
-                @Composable {
+            val fab = @Composable {
+                if (showFab.value) {
                     FloatingActionButton(
                         modifier = Modifier.onGloballyPositioned { positioned ->
                             fabSize = positioned.size
@@ -405,107 +399,26 @@
                         Icon(Icons.Filled.Favorite)
                     }
                 }
-            } else {
-                null
             }
             Scaffold(
-                scaffoldState = scaffoldState,
                 floatingActionButton = fab,
-                floatingActionButtonPosition = FabPosition.End
+                floatingActionButtonPosition = FabPosition.End,
+                bottomBar = {
+                    fabPlacement = AmbientFabPlacement.current
+                }
             ) {
                 Text("body")
             }
         }
         rule.runOnIdle {
-            assertThat(scaffoldState.scaffoldGeometry.fabBounds?.size)
-                .isEqualTo(fabSize.toSize())
+            assertThat(fabPlacement?.width).isEqualTo(fabSize.width)
+            assertThat(fabPlacement?.height).isEqualTo(fabSize.height)
             showFab.value = false
         }
 
         rule.runOnIdle {
-            assertThat(scaffoldState.scaffoldGeometry.fabBounds?.size).isEqualTo(null)
-        }
-    }
-
-    @Test
-    fun scaffold_geometry_bottomBarSize() {
-        var bottomBarSize: IntSize = IntSize.Zero
-        val showBottom = mutableStateOf(true)
-        lateinit var scaffoldState: ScaffoldState
-        rule.setContent {
-            scaffoldState = rememberScaffoldState()
-            val bottom: @Composable (() -> Unit)? = if (showBottom.value) {
-                @Composable {
-                    Box(
-                        Modifier
-                            .onGloballyPositioned { positioned: LayoutCoordinates ->
-                                bottomBarSize = positioned.size
-                            }
-                            .fillMaxWidth()
-                            .preferredHeight(100.dp)
-                            .background(color = Color.Red)
-                    )
-                }
-            } else {
-                null
-            }
-            Scaffold(
-                scaffoldState = scaffoldState,
-                bottomBar = bottom
-            ) {
-                Text("body")
-            }
-        }
-        rule.runOnIdle {
-            assertThat(scaffoldState.scaffoldGeometry.bottomBarBounds?.size)
-                .isEqualTo(bottomBarSize.toSize())
-            showBottom.value = false
-        }
-
-        rule.runOnIdle {
-            assertThat(scaffoldState.scaffoldGeometry.bottomBarBounds?.size)
-                .isEqualTo(null)
-        }
-    }
-
-    @Test
-    fun scaffold_geometry_topBarSize() {
-        var topBarSize: IntSize = IntSize.Zero
-        val showTop = mutableStateOf(true)
-        lateinit var scaffoldState: ScaffoldState
-        rule.setContent {
-            scaffoldState = rememberScaffoldState()
-            val top: @Composable (() -> Unit)? = if (showTop.value) {
-                @Composable {
-                    Box(
-                        Modifier
-                            .onGloballyPositioned { positioned: LayoutCoordinates ->
-                                topBarSize = positioned.size
-                            }
-                            .fillMaxWidth()
-                            .preferredHeight(100.dp)
-                            .background(color = Color.Red)
-                    )
-                }
-            } else {
-                null
-            }
-            Scaffold(
-                scaffoldState = scaffoldState,
-                topBar = top
-            ) {
-                Text("body")
-            }
-        }
-        rule.runOnIdle {
-            assertThat(scaffoldState.scaffoldGeometry.topBarBounds?.size)
-                .isEqualTo(topBarSize.toSize())
-            showTop.value = false
-        }
-
-        rule.runOnIdle {
-            assertThat(scaffoldState.scaffoldGeometry.topBarBounds?.size)
-                .isEqualTo(null)
+            assertThat(fabPlacement).isEqualTo(null)
+            assertThat(fabPlacement).isEqualTo(null)
         }
     }
 
@@ -541,36 +454,4 @@
             }
         }
     }
-
-    @Test
-    fun scaffold_bottomBar_geometryPropagation() {
-        var bottomBarSize: IntSize = IntSize.Zero
-        lateinit var geometry: ScaffoldGeometry
-
-        lateinit var scaffoldState: ScaffoldState
-        rule.setContent {
-            scaffoldState = rememberScaffoldState()
-            Scaffold(
-                scaffoldState = scaffoldState,
-                bottomBar = {
-                    geometry = AmbientScaffoldGeometry.current
-                    Box(
-                        Modifier
-                            .onGloballyPositioned { positioned: LayoutCoordinates ->
-                                bottomBarSize = positioned.size
-                            }
-                            .fillMaxWidth()
-                            .preferredHeight(100.dp)
-                            .background(color = Color.Red)
-                    )
-                }
-            ) {
-                Text("body")
-            }
-        }
-        rule.runOnIdle {
-            assertThat(geometry.bottomBarBounds?.size).isEqualTo(bottomBarSize.toSize())
-            assertThat(geometry.bottomBarBounds?.width).isNotEqualTo(0f)
-        }
-    }
 }
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AppBar.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AppBar.kt
index 2171598..2013f40 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AppBar.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AppBar.kt
@@ -174,10 +174,9 @@
     elevation: Dp = BottomAppBarElevation,
     content: @Composable RowScope.() -> Unit
 ) {
-    val scaffoldGeometry = AmbientScaffoldGeometry.current
-    val fabBounds = scaffoldGeometry.fabBounds
-    val shape = if (cutoutShape != null && scaffoldGeometry.isFabDocked && fabBounds != null) {
-        BottomAppBarCutoutShape(cutoutShape, fabBounds)
+    val fabPlacement = AmbientFabPlacement.current
+    val shape = if (cutoutShape != null && fabPlacement?.isDocked == true) {
+        BottomAppBarCutoutShape(cutoutShape, fabPlacement)
     } else {
         RectangleShape
     }
@@ -199,7 +198,7 @@
  */
 private data class BottomAppBarCutoutShape(
     val cutoutShape: Shape,
-    val fabBounds: Rect
+    val fabPlacement: FabPlacement
 ) : Shape {
 
     override fun createOutline(size: Size, density: Density): Outline {
@@ -223,11 +222,11 @@
         val cutoutOffset = with(density) { BottomAppBarCutoutOffset.toPx() }
 
         val cutoutSize = Size(
-            width = fabBounds.width + (cutoutOffset * 2),
-            height = fabBounds.height + (cutoutOffset * 2)
+            width = fabPlacement.width + (cutoutOffset * 2),
+            height = fabPlacement.height + (cutoutOffset * 2)
         )
 
-        val cutoutStartX = fabBounds.left - cutoutOffset
+        val cutoutStartX = fabPlacement.left - cutoutOffset
         val cutoutEndX = cutoutStartX + cutoutSize.width
 
         val cutoutRadius = cutoutSize.height / 2f
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
index 241ba33..415a846 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
@@ -16,34 +16,30 @@
 
 package androidx.compose.material
 
-import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Providers
 import androidx.compose.runtime.Stable
+import androidx.compose.runtime.emptyContent
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.onDispose
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.staticAmbientOf
-import androidx.compose.runtime.structuralEqualityPolicy
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Layout
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.layout.boundsInParent
-import androidx.compose.ui.onGloballyPositioned
-import androidx.compose.ui.platform.DensityAmbient
+import androidx.compose.ui.layout.ExperimentalSubcomposeLayoutApi
+import androidx.compose.ui.layout.SubcomposeLayout
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastMap
+import androidx.compose.ui.util.fastMaxBy
 import androidx.compose.ui.zIndex
 
 /**
@@ -69,8 +65,6 @@
      * Whether or not drawer sheet in scaffold (if set) can be interacted by gestures.
      */
     var isDrawerGesturesEnabled by mutableStateOf(isDrawerGesturesEnabled)
-
-    internal val scaffoldGeometry = ScaffoldGeometry()
 }
 
 /**
@@ -91,17 +85,6 @@
     ScaffoldState(drawerState, snackbarHostState, isDrawerGesturesEnabled)
 }
 
-@Stable
-internal class ScaffoldGeometry {
-    var topBarBounds by mutableStateOf<Rect?>(null, structuralEqualityPolicy())
-    var bottomBarBounds by mutableStateOf<Rect?>(null, structuralEqualityPolicy())
-    var fabBounds by mutableStateOf<Rect?>(null, structuralEqualityPolicy())
-
-    var isFabDocked by mutableStateOf(false)
-}
-
-internal val AmbientScaffoldGeometry = staticAmbientOf { ScaffoldGeometry() }
-
 /**
  * The possible positions for a [FloatingActionButton] attached to a [Scaffold].
  */
@@ -174,7 +157,7 @@
  * `onFoo` color for [backgroundColor], or, if it is not a color from the theme, this will keep
  * the same value set above this Surface.
  * @param bodyContent content of your screen. The lambda receives an [PaddingValues] that should be
- * applied to the content root via [Modifier.padding] to properly offset top and bottom bars. If
+ * applied to the content root via Modifier.padding to properly offset top and bottom bars. If
  * you're using VerticalScroller, apply this modifier to the child of the scroller, and not on
  * the scroller itself.
  */
@@ -183,10 +166,10 @@
 fun Scaffold(
     modifier: Modifier = Modifier,
     scaffoldState: ScaffoldState = rememberScaffoldState(),
-    topBar: @Composable (() -> Unit)? = null,
-    bottomBar: @Composable (() -> Unit)? = null,
+    topBar: @Composable () -> Unit = emptyContent(),
+    bottomBar: @Composable () -> Unit = emptyContent(),
     snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
-    floatingActionButton: @Composable (() -> Unit)? = null,
+    floatingActionButton: @Composable () -> Unit = emptyContent(),
     floatingActionButtonPosition: FabPosition = FabPosition.End,
     isFloatingActionButtonDocked: Boolean = false,
     drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
@@ -199,27 +182,19 @@
     contentColor: Color = contentColorFor(backgroundColor),
     bodyContent: @Composable (PaddingValues) -> Unit
 ) {
-    scaffoldState.scaffoldGeometry.isFabDocked = isFloatingActionButtonDocked
     val child = @Composable { childModifier: Modifier ->
         Surface(modifier = childModifier, color = backgroundColor, contentColor = contentColor) {
-            Column(Modifier.fillMaxSize()) {
-                if (topBar != null) {
-                    TopBarContainer(Modifier.zIndex(TopAppBarZIndex), scaffoldState, topBar)
-                }
-                Box(Modifier.weight(1f, fill = true)) {
-                    ScaffoldContent(Modifier.fillMaxSize(), scaffoldState, bodyContent)
-                    Column(Modifier.align(Alignment.BottomCenter)) {
-                        snackbarHost(scaffoldState.snackbarHostState)
-                        ScaffoldBottom(
-                            scaffoldState = scaffoldState,
-                            fabPos = floatingActionButtonPosition,
-                            isFabDocked = isFloatingActionButtonDocked,
-                            fab = floatingActionButton,
-                            bottomBar = bottomBar
-                        )
-                    }
-                }
-            }
+            ScaffoldLayout(
+                isFabDocked = isFloatingActionButtonDocked,
+                fabPosition = floatingActionButtonPosition,
+                topBar = topBar,
+                bodyContent = bodyContent,
+                snackbar = {
+                    snackbarHost(scaffoldState.snackbarHostState)
+                },
+                fab = floatingActionButton,
+                bottomBar = bottomBar
+            )
         }
     }
 
@@ -241,156 +216,178 @@
     }
 }
 
-private fun FabPosition.toColumnAlign() =
-    if (this == FabPosition.End) Alignment.End else Alignment.CenterHorizontally
-
 /**
- * Scaffold part that is on the bottom. Includes FAB and BottomBar
- */
-@Composable
-private fun ScaffoldBottom(
-    scaffoldState: ScaffoldState,
-    fabPos: FabPosition,
-    isFabDocked: Boolean,
-    fab: @Composable (() -> Unit)? = null,
-    bottomBar: @Composable (() -> Unit)? = null
-) {
-    if (isFabDocked && bottomBar != null && fab != null) {
-        DockedBottomBar(
-            fabPosition = fabPos,
-            fab = { FabContainer(Modifier, scaffoldState, fab) },
-            bottomBar = { BottomBarContainer(scaffoldState, bottomBar) }
-        )
-    } else {
-        Column(Modifier.fillMaxWidth()) {
-            if (fab != null) {
-                FabContainer(
-                    Modifier.align(fabPos.toColumnAlign())
-                        .padding(start = FabSpacing, end = FabSpacing, bottom = FabSpacing),
-                    scaffoldState,
-                    fab
-                )
-            }
-            if (bottomBar != null) {
-                BottomBarContainer(scaffoldState, bottomBar)
-            }
-        }
-    }
-}
-
-/**
- * Simple `Stack` implementation that places [fab] on top (z-axis) of [bottomBar], with the midpoint
- * of the [fab] aligned to the top edge of the [bottomBar].
+ * Layout for a [Scaffold]'s content.
  *
- * This is needed as we want the total height of the BottomAppBar to be equal to the height of
- * [bottomBar] + half the height of [fab], which is only possible with a custom layout.
+ * @param isFabDocked whether the FAB (if present) is docked to the bottom bar or not
+ * @param fabPosition [FabPosition] for the FAB (if present)
+ * @param topBar the content to place at the top of the [Scaffold], typically a [TopAppBar]
+ * @param bodyContent the main 'body' of the [Scaffold]
+ * @param snackbar the [Snackbar] displayed on top of the [bodyContent]
+ * @param fab the [FloatingActionButton] displayed on top of the [bodyContent], below the [snackbar]
+ * and above the [bottomBar]
+ * @param bottomBar the content to place at the bottom of the [Scaffold], on top of the
+ * [bodyContent], typically a [BottomAppBar].
  */
+@OptIn(ExperimentalSubcomposeLayoutApi::class)
 @Composable
-private fun DockedBottomBar(
+private fun ScaffoldLayout(
+    isFabDocked: Boolean,
     fabPosition: FabPosition,
+    topBar: @Composable () -> Unit,
+    bodyContent: @Composable (PaddingValues) -> Unit,
+    snackbar: @Composable () -> Unit,
     fab: @Composable () -> Unit,
     bottomBar: @Composable () -> Unit
 ) {
-    Layout(
-        children = {
-            bottomBar()
-            fab()
-        }
-    ) { measurables, constraints ->
-        val (appBarPlaceable, fabPlaceable) = measurables.map { it.measure(constraints) }
+    SubcomposeLayout<ScaffoldLayoutContent> { constraints ->
+        val layoutWidth = constraints.maxWidth
+        val layoutHeight = constraints.maxHeight
 
-        val layoutWidth = appBarPlaceable.width
-        // Total height is the app bar height + half the fab height
-        val layoutHeight = appBarPlaceable.height + (fabPlaceable.height / 2)
-
-        val appBarVerticalOffset = layoutHeight - appBarPlaceable.height
-        val fabPosX = if (fabPosition == FabPosition.End) {
-            layoutWidth - fabPlaceable.width - DockedFabEndSpacing.toIntPx()
-        } else {
-            (layoutWidth - fabPlaceable.width) / 2
-        }
+        val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
 
         layout(layoutWidth, layoutHeight) {
-            appBarPlaceable.placeRelative(0, appBarVerticalOffset)
-            fabPlaceable.placeRelative(fabPosX, 0)
-        }
-    }
-}
+            val topBarPlaceables = subcompose(ScaffoldLayoutContent.TopBar, topBar).fastMap {
+                it.measure(looseConstraints)
+            }
 
-@Composable
-private fun ScaffoldContent(
-    modifier: Modifier,
-    scaffoldState: ScaffoldState,
-    content: @Composable (PaddingValues) -> Unit
-) {
-    ScaffoldSlot(modifier) {
-        val innerPadding = with(DensityAmbient.current) {
-            val bottom = scaffoldState.scaffoldGeometry.bottomBarBounds?.height?.toDp() ?: 0.dp
-            PaddingValues(bottom = bottom)
-        }
-        content(innerPadding)
-    }
-}
+            val topBarHeight = topBarPlaceables.fastMaxBy { it.height }?.height ?: 0
 
-@Composable
-private fun BottomBarContainer(
-    scaffoldState: ScaffoldState,
-    bottomBar: @Composable () -> Unit
-) {
-    BoundsAwareScaffoldSlot(
-        Modifier,
-        { scaffoldState.scaffoldGeometry.bottomBarBounds = it },
-        slotContent = {
-            Providers(AmbientScaffoldGeometry provides scaffoldState.scaffoldGeometry) {
-                bottomBar()
+            val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).fastMap {
+                it.measure(looseConstraints)
+            }
+
+            val snackbarHeight = snackbarPlaceables.fastMaxBy { it.height }?.height ?: 0
+
+            val fabPlaceables = subcompose(ScaffoldLayoutContent.Fab) {
+                // TODO: b/169257866 - remove box and zIndex modifier
+                // Currently we need an extra box here with a high zIndex to ensure that the FAB is
+                // always placed above the bottom bar - although we control the natural drawing
+                // order below, currently the FAB has a default elevation lower than the bottom
+                // app bar, so without this box it will be placed below the bottom bar.
+                Box(Modifier.zIndex(Float.POSITIVE_INFINITY)) { fab() }
+            }.fastMap {
+                it.measure(looseConstraints)
+            }
+
+            val fabWidth = fabPlaceables.fastMaxBy { it.width }?.width ?: 0
+            val fabHeight = fabPlaceables.fastMaxBy { it.height }?.height ?: 0
+
+            // FAB distance from the left of the layout, taking into account LTR / RTL
+            val fabLeftOffset = if (fabWidth != 0 && fabHeight != 0) {
+                if (fabPosition == FabPosition.End) {
+                    if (layoutDirection == LayoutDirection.Ltr) {
+                        layoutWidth - FabSpacing.toIntPx() - fabWidth
+                    } else {
+                        FabSpacing.toIntPx()
+                    }
+                } else {
+                    (layoutWidth - fabWidth) / 2
+                }
+            } else {
+                0
+            }
+
+            val fabPlacement = if (fabWidth != 0 && fabHeight != 0) {
+                FabPlacement(
+                    isDocked = isFabDocked,
+                    left = fabLeftOffset,
+                    width = fabWidth,
+                    height = fabHeight
+                )
+            } else {
+                null
+            }
+
+            val bottomBarPlaceables = subcompose(ScaffoldLayoutContent.BottomBar) {
+                Providers(
+                    AmbientFabPlacement provides fabPlacement,
+                    children = bottomBar
+                )
+            }.fastMap { it.measure(looseConstraints) }
+
+            val bottomBarHeight = bottomBarPlaceables.fastMaxBy { it.height }?.height ?: 0
+
+            val fabOffsetFromBottom = if (fabWidth != 0 && fabHeight != 0) {
+                if (bottomBarHeight == 0) {
+                    fabHeight + FabSpacing.toIntPx()
+                } else {
+                    if (isFabDocked) {
+                        // Total height is the bottom bar height + half the FAB height
+                        bottomBarHeight + (fabHeight / 2)
+                    } else {
+                        // Total height is the bottom bar height + the FAB height + the padding
+                        // between the FAB and bottom bar
+                        bottomBarHeight + fabHeight + FabSpacing.toIntPx()
+                    }
+                }
+            } else {
+                0
+            }
+
+            val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
+                snackbarHeight + if (fabOffsetFromBottom != 0) {
+                    fabOffsetFromBottom
+                } else {
+                    bottomBarHeight
+                }
+            } else {
+                0
+            }
+
+            val bodyContentHeight = layoutHeight - topBarHeight
+
+            val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) {
+                val innerPadding = PaddingValues(bottom = bottomBarHeight.toDp())
+                bodyContent(innerPadding)
+            }.fastMap { it.measure(looseConstraints.copy(maxHeight = bodyContentHeight)) }
+
+            // Placing to control drawing order to match default elevation of each placeable
+
+            bodyContentPlaceables.fastForEach {
+                it.place(0, topBarHeight)
+            }
+            topBarPlaceables.fastForEach {
+                it.place(0, 0)
+            }
+            snackbarPlaceables.fastForEach {
+                it.place(0, layoutHeight - snackbarOffsetFromBottom)
+            }
+            // The bottom bar is always at the bottom of the layout
+            bottomBarPlaceables.fastForEach {
+                it.place(0, layoutHeight - bottomBarHeight)
+            }
+            // Explicitly not using placeRelative here as `leftOffset` already accounts for RTL
+            fabPlaceables.fastForEach {
+                it.place(fabLeftOffset, layoutHeight - fabOffsetFromBottom)
             }
         }
-    )
-}
-
-@Composable
-private fun FabContainer(
-    modifier: Modifier,
-    scaffoldState: ScaffoldState,
-    fab: @Composable () -> Unit
-) {
-    BoundsAwareScaffoldSlot(modifier, { scaffoldState.scaffoldGeometry.fabBounds = it }, fab)
-}
-
-@Composable
-private fun TopBarContainer(
-    modifier: Modifier,
-    scaffoldState: ScaffoldState,
-    topBar: @Composable () -> Unit
-) {
-    BoundsAwareScaffoldSlot(modifier, { scaffoldState.scaffoldGeometry.topBarBounds = it }, topBar)
-}
-
-@Composable
-private fun BoundsAwareScaffoldSlot(
-    modifier: Modifier,
-    onBoundsKnown: (Rect?) -> Unit,
-    slotContent: @Composable () -> Unit
-) {
-    onDispose {
-        onBoundsKnown(null)
     }
-    ScaffoldSlot(
-        modifier = modifier.onGloballyPositioned { coords ->
-            onBoundsKnown(coords.boundsInParent)
-        },
-        content = slotContent
-    )
 }
 
 /**
- * Default slot implementation for Scaffold slots content
+ * Placement information for a [FloatingActionButton] inside a [Scaffold].
+ *
+ * @property isDocked whether the FAB should be docked with the bottom bar
+ * @property left the FAB's offset from the left edge of the bottom bar, already adjusted for RTL
+ * support
+ * @property width the width of the FAB
+ * @property height the height of the FAB
  */
-@Composable
-private fun ScaffoldSlot(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
-    Box(modifier) { content() }
-}
+@Immutable
+internal class FabPlacement(
+    val isDocked: Boolean,
+    val left: Int,
+    val width: Int,
+    val height: Int
+)
 
+/**
+ * Ambient containing a [FabPlacement] that is read by [BottomAppBar] to calculate notch location.
+ */
+internal val AmbientFabPlacement = staticAmbientOf<FabPlacement?> { null }
+
+// FAB spacing above the bottom bar / bottom of the Scaffold
 private val FabSpacing = 16.dp
-private val DockedFabEndSpacing = 16.dp
-private const val TopAppBarZIndex = 1f
\ No newline at end of file
+
+private enum class ScaffoldLayoutContent { TopBar, MainContent, Snackbar, Fab, BottomBar }