Use the same measure policy for different passes in SubcomposeLayout by default

This CL changes the default intermediateMeasurePolicy in SubcomposeLayout
to reusing the measurePolicy used in lookahead. In the intermediate
pass this MeasurePolicy will not subcompose any new slots, and instead
will retrieve the measureables already created during the lookahead pass
for any given slot. The MeasurePolicy will operate on potentially
animating constraints, and measure & layout children accordingly.

As a result of this change, layouts built on top of SubcomposeLayout
that do not have conditional slots (e.g. Scaffold, TabRow,
BoxWithConstraints, etc) will work nicely with lookahead.

RelNote: "
New default intermediateMeasurePolicy that reuses measure policy from
lookahead pass allows SubcomposeLayout subtypes without conditional
slots such as Scaffold, TabRow, and BoxWithConstraints to work
with lookahead by default.
"

Test: Added
Change-Id: Id84c8357e63905ea09b07acee91094489eb04402
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
index 1844ce5..e729d44 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
@@ -44,6 +44,7 @@
 import androidx.compose.animation.demos.lookahead.LookaheadWithFlowRowDemo
 import androidx.compose.animation.demos.lookahead.LookaheadWithIntrinsicsDemo
 import androidx.compose.animation.demos.lookahead.LookaheadWithMovableContentDemo
+import androidx.compose.animation.demos.lookahead.LookaheadWithScaffold
 import androidx.compose.animation.demos.lookahead.LookaheadWithSubcompose
 import androidx.compose.animation.demos.lookahead.LookaheadWithTabRowDemo
 import androidx.compose.animation.demos.lookahead.ScreenSizeChangeDemo
@@ -134,6 +135,9 @@
                 ComposableDemo("Lookahead With Tab Row") {
                     LookaheadWithTabRowDemo()
                 },
+                ComposableDemo("Lookahead With Scaffold") {
+                    LookaheadWithScaffold()
+                },
             )
         ),
         DemoCategory(
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithScaffold.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithScaffold.kt
new file mode 100644
index 0000000..4a0fad9
--- /dev/null
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithScaffold.kt
@@ -0,0 +1,395 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation.demos.lookahead
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.TweenSpec
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.CutCornerShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.BackdropScaffold
+import androidx.compose.material.BackdropValue
+import androidx.compose.material.BottomAppBar
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.ExtendedFloatingActionButton
+import androidx.compose.material.FabPosition
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.ListItem
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Scaffold
+import androidx.compose.material.ScrollableTabRow
+import androidx.compose.material.Snackbar
+import androidx.compose.material.SnackbarHost
+import androidx.compose.material.Tab
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material.rememberBackdropScaffoldState
+import androidx.compose.material.rememberScaffoldState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import kotlin.math.abs
+import kotlin.math.roundToInt
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+private val colors = listOf(
+    Color(0xffff6f69), Color(0xffffcc5c), Color(0xff264653), Color(0xff2a9d84)
+)
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Preview
+@Composable
+fun LookaheadWithScaffold() {
+    val hasPadding by produceState(initialValue = true) {
+        while (true) {
+            delay(3000)
+            value = !value
+        }
+    }
+    LookaheadScope {
+        Box(
+            Modifier
+                .fillMaxHeight()
+                .background(Color.Gray)
+                .animateBounds(
+                    if (hasPadding) Modifier.padding(bottom = 300.dp) else Modifier
+                )
+        ) {
+            var state by remember { mutableStateOf(0) }
+            val titles = listOf(
+                "SimpleScaffold", "W/Cutout", "SimpleSnackbar", "CustomSnackbar", "Backdrop"
+            )
+            Column {
+                ScrollableTabRow(
+                    selectedTabIndex = state,
+                ) {
+                    titles.forEachIndexed { index, title ->
+                        Tab(
+                            selected = state == index,
+                            onClick = { state = index },
+                            text = { Text(title) }
+                        )
+                    }
+                }
+                when (state) {
+                    0 -> SimpleScaffoldWithTopBar()
+                    1 -> ScaffoldWithBottomBarAndCutout()
+                    2 -> ScaffoldWithSimpleSnackbar()
+                    3 -> ScaffoldWithCustomSnackbar()
+                    4 -> BackdropScaffoldSample()
+                }
+            }
+        }
+    }
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+fun SimpleScaffoldWithTopBar() {
+    val scaffoldState = rememberScaffoldState()
+    val scope = rememberCoroutineScope()
+    Scaffold(
+        scaffoldState = scaffoldState,
+        drawerContent = { Text("Drawer content") },
+        topBar = {
+            TopAppBar(
+                title = { Text("Simple Scaffold Screen") },
+                navigationIcon = {
+                    IconButton(
+                        onClick = {
+                            scope.launch { scaffoldState.drawerState.open() }
+                        }
+                    ) {
+                        Icon(Icons.Filled.Menu, contentDescription = "Localized description")
+                    }
+                }
+            )
+        },
+        floatingActionButtonPosition = FabPosition.End,
+        floatingActionButton = {
+            ExtendedFloatingActionButton(
+                text = { Text("Inc") },
+                onClick = { /* fab click handler */ }
+            )
+        },
+        content = { innerPadding ->
+            Box(
+                Modifier
+                    .fillMaxSize()
+                    .background(Color.White)
+            )
+            LazyColumn(contentPadding = innerPadding) {
+                items(count = 20) {
+                    Box(
+                        Modifier
+                            .fillMaxWidth()
+                            .height(150.dp)
+                            .background(colors[it % colors.size])
+                    )
+                }
+            }
+        }
+    )
+}
+
+@Composable
+fun ScaffoldWithBottomBarAndCutout() {
+    val scaffoldState = rememberScaffoldState()
+
+    // Consider negative values to mean 'cut corner' and positive values to mean 'round corner'
+    val sharpEdgePercent = -50f
+    val roundEdgePercent = 45f
+    // Start with sharp edges
+    val animatedProgress = remember { Animatable(sharpEdgePercent) }
+    // Create a coroutineScope for the animation
+    val coroutineScope = rememberCoroutineScope()
+    // animation value to animate shape
+    val progress = animatedProgress.value.roundToInt()
+
+    // When progress is 0, there is no modification to the edges so we are just drawing a rectangle.
+    // This allows for a smooth transition between cut corners and round corners.
+    val fabShape = if (progress < 0) {
+        CutCornerShape(abs(progress))
+    } else if (progress == roundEdgePercent.toInt()) {
+        CircleShape
+    } else {
+        RoundedCornerShape(progress)
+    }
+    // lambda to call to trigger shape animation
+    val changeShape: () -> Unit = {
+        val target = animatedProgress.targetValue
+        val nextTarget = if (target == roundEdgePercent) sharpEdgePercent else roundEdgePercent
+        coroutineScope.launch {
+            animatedProgress.animateTo(
+                targetValue = nextTarget,
+                animationSpec = TweenSpec(durationMillis = 600)
+            )
+        }
+    }
+
+    Scaffold(
+        scaffoldState = scaffoldState,
+        drawerContent = { Text("Drawer content") },
+        topBar = { TopAppBar(title = { Text("Scaffold with bottom cutout") }) },
+        bottomBar = {
+            BottomAppBar(cutoutShape = fabShape) {
+                IconButton(
+                    onClick = {
+                        coroutineScope.launch { scaffoldState.drawerState.open() }
+                    }
+                ) {
+                    Icon(Icons.Filled.Menu, contentDescription = "Localized description")
+                }
+            }
+        },
+        floatingActionButton = {
+            ExtendedFloatingActionButton(
+                text = { Text("Change shape") },
+                onClick = changeShape,
+                shape = fabShape
+            )
+        },
+        floatingActionButtonPosition = FabPosition.Center,
+        isFloatingActionButtonDocked = true,
+        content = { innerPadding ->
+            LazyColumn(contentPadding = innerPadding) {
+                items(count = 100) {
+                    Box(
+                        Modifier
+                            .fillMaxWidth()
+                            .height(50.dp)
+                            .background(colors[it % colors.size])
+                    )
+                }
+            }
+        }
+    )
+}
+
+@Composable
+fun ScaffoldWithSimpleSnackbar() {
+    val scaffoldState = rememberScaffoldState()
+    val scope = rememberCoroutineScope()
+    Scaffold(
+        scaffoldState = scaffoldState,
+        floatingActionButton = {
+            var clickCount by remember { mutableStateOf(0) }
+            ExtendedFloatingActionButton(
+                text = { Text("Show snackbar") },
+                onClick = {
+                    // show snackbar as a suspend function
+                    scope.launch {
+                        scaffoldState.snackbarHostState.showSnackbar("Snackbar # ${++clickCount}")
+                    }
+                }
+            )
+        },
+        content = { innerPadding ->
+            Text(
+                text = "Body content",
+                modifier = Modifier
+                    .padding(innerPadding)
+                    .fillMaxSize()
+                    .wrapContentSize()
+            )
+        }
+    )
+}
+
+@Composable
+fun ScaffoldWithCustomSnackbar() {
+    val scaffoldState = rememberScaffoldState()
+    val scope = rememberCoroutineScope()
+    Scaffold(
+        scaffoldState = scaffoldState,
+        snackbarHost = {
+            // reuse default SnackbarHost to have default animation and timing handling
+            SnackbarHost(it) { data ->
+                // custom snackbar with the custom border
+                Snackbar(
+                    modifier = Modifier.border(2.dp, MaterialTheme.colors.secondary),
+                    snackbarData = data
+                )
+            }
+        },
+        floatingActionButton = {
+            var clickCount by remember { mutableStateOf(0) }
+            ExtendedFloatingActionButton(
+                text = { Text("Show snackbar") },
+                onClick = {
+                    scope.launch {
+                        scaffoldState.snackbarHostState.showSnackbar("Snackbar # ${++clickCount}")
+                    }
+                }
+            )
+        },
+        content = { innerPadding ->
+            Text(
+                text = "Custom Snackbar Demo",
+                modifier = Modifier
+                    .padding(innerPadding)
+                    .fillMaxSize()
+                    .wrapContentSize()
+            )
+        }
+    )
+}
+
+@Preview
+@Composable
+@OptIn(ExperimentalMaterialApi::class)
+fun BackdropScaffoldSample() {
+    val scope = rememberCoroutineScope()
+    val selection = remember { mutableStateOf(1) }
+    val scaffoldState = rememberBackdropScaffoldState(BackdropValue.Concealed)
+    LaunchedEffect(scaffoldState) {
+        scaffoldState.reveal()
+    }
+    BackdropScaffold(
+        scaffoldState = scaffoldState,
+        appBar = {
+            TopAppBar(
+                title = { Text("Backdrop scaffold") },
+                navigationIcon = {
+                    if (scaffoldState.isConcealed) {
+                        IconButton(onClick = { scope.launch { scaffoldState.reveal() } }) {
+                            Icon(Icons.Default.Menu, contentDescription = "Localized description")
+                        }
+                    } else {
+                        IconButton(onClick = { scope.launch { scaffoldState.conceal() } }) {
+                            Icon(Icons.Default.Close, contentDescription = "Localized description")
+                        }
+                    }
+                },
+                actions = {
+                    var clickCount by remember { mutableStateOf(0) }
+                    IconButton(
+                        onClick = {
+                            // show snackbar as a suspend function
+                            scope.launch {
+                                scaffoldState.snackbarHostState
+                                    .showSnackbar("Snackbar #${++clickCount}")
+                            }
+                        }
+                    ) {
+                        Icon(Icons.Default.Favorite, contentDescription = "Localized description")
+                    }
+                },
+                elevation = 0.dp,
+                backgroundColor = Color.Transparent
+            )
+        },
+        backLayerContent = {
+            LazyColumn {
+                items(if (selection.value >= 3) 3 else 5) {
+                    ListItem(
+                        Modifier.clickable {
+                            selection.value = it
+                            scope.launch { scaffoldState.conceal() }
+                        },
+                        text = { Text("Select $it") }
+                    )
+                }
+            }
+        },
+        frontLayerContent = {
+            Text("Selection: ${selection.value}")
+            LazyColumn {
+                items(50) {
+                    ListItem(
+                        text = { Text("Item $it") },
+                        icon = {
+                            Icon(
+                                Icons.Default.Favorite,
+                                contentDescription = "Localized description"
+                            )
+                        }
+                    )
+                }
+            }
+        }
+    )
+}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithTabRowDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithTabRowDemo.kt
index c4cb2dd..6ec1f2b 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithTabRowDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/lookahead/LookaheadWithTabRowDemo.kt
@@ -28,14 +28,12 @@
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.MaterialTheme
 import androidx.compose.material.ScrollableTabRow
 import androidx.compose.material.Tab
 import androidx.compose.material.TabPosition
 import androidx.compose.material.TabRow
-import androidx.compose.material.TabRowDefaults
 import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
@@ -65,9 +63,9 @@
             }
         }
         Column(
-            Modifier
+            Modifier.fillMaxWidth()
                 .animateBounds(
-                    if (isWide) Modifier.fillMaxWidth() else Modifier.width(300.dp)
+                    if (isWide) Modifier else Modifier.padding(end = 100.dp)
                 )
                 .fillMaxHeight()
                 .background(Color(0xFFfffbd0))
@@ -79,7 +77,6 @@
     }
 }
 
-@OptIn(ExperimentalComposeUiApi::class)
 @Composable
 fun FancyTabs() {
     var state by remember { mutableStateOf(0) }
@@ -87,11 +84,6 @@
     Column {
         TabRow(
             selectedTabIndex = state,
-            indicator = @Composable { tabPositions ->
-                TabRowDefaults.Indicator(
-                    Modifier.animateBounds(Modifier.tabIndicatorOffset(tabPositions[state]))
-                )
-            },
         ) {
             titles.forEachIndexed { index, title ->
                 FancyTab(title = title, onClick = { state = index }, selected = (index == state))
@@ -105,17 +97,13 @@
     }
 }
 
-@OptIn(ExperimentalComposeUiApi::class)
 @Composable
 fun FancyTab(title: String, onClick: () -> Unit, selected: Boolean) {
     Tab(selected, onClick) {
         Column(
             Modifier
                 .padding(10.dp)
-                .height(50.dp)
-                .animateBounds(
-                    Modifier.fillMaxWidth()
-                ),
+                .height(50.dp),
             verticalArrangement = Arrangement.SpaceBetween
         ) {
             Box(
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 7e1d053..c25d244 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -2350,17 +2350,22 @@
     method @androidx.compose.runtime.Stable public static operator long times(long, long size);
   }
 
-  @androidx.compose.ui.ExperimentalComposeUiApi public sealed interface SubcomposeIntermediateMeasureScope extends androidx.compose.ui.layout.MeasureScope {
+  @androidx.compose.ui.ExperimentalComposeUiApi public sealed interface SubcomposeIntermediateMeasureScope extends androidx.compose.ui.layout.SubcomposeMeasureScope {
+    method public long getLookaheadConstraints();
+    method public kotlin.jvm.functions.Function2<androidx.compose.ui.layout.SubcomposeMeasureScope,androidx.compose.ui.unit.Constraints,androidx.compose.ui.layout.MeasureResult> getLookaheadMeasurePolicy();
     method public long getLookaheadSize();
     method public java.util.List<androidx.compose.ui.layout.Measurable> measurablesForSlot(Object? slotId);
+    method public default java.util.List<androidx.compose.ui.layout.Measurable> subcompose(Object? slotId, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    property public abstract long lookaheadConstraints;
+    property public abstract kotlin.jvm.functions.Function2<androidx.compose.ui.layout.SubcomposeMeasureScope,androidx.compose.ui.unit.Constraints,androidx.compose.ui.layout.MeasureResult> lookaheadMeasurePolicy;
     property public abstract long lookaheadSize;
   }
 
   public final class SubcomposeLayoutKt {
     method @androidx.compose.runtime.Composable public static void SubcomposeLayout(optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
-    method @androidx.compose.runtime.Composable @androidx.compose.ui.ExperimentalComposeUiApi public static void SubcomposeLayout(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeIntermediateMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult>? intermediateMeasurePolicy, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
+    method @androidx.compose.runtime.Composable @androidx.compose.ui.ExperimentalComposeUiApi public static void SubcomposeLayout(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeIntermediateMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> intermediateMeasurePolicy, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
     method @androidx.compose.runtime.Composable @androidx.compose.ui.UiComposable public static void SubcomposeLayout(androidx.compose.ui.layout.SubcomposeLayoutState state, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
-    method @androidx.compose.runtime.Composable @androidx.compose.ui.ExperimentalComposeUiApi @androidx.compose.ui.UiComposable public static void SubcomposeLayout(androidx.compose.ui.layout.SubcomposeLayoutState state, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeIntermediateMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult>? intermediateMeasurePolicy, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
+    method @androidx.compose.runtime.Composable @androidx.compose.ui.ExperimentalComposeUiApi @androidx.compose.ui.UiComposable public static void SubcomposeLayout(androidx.compose.ui.layout.SubcomposeLayoutState state, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeIntermediateMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> intermediateMeasurePolicy, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
     method public static androidx.compose.ui.layout.SubcomposeSlotReusePolicy SubcomposeSlotReusePolicy(int maxSlotsToRetainForReuse);
   }
 
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadLayoutTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadLayoutTest.kt
index 6f9dba1..01bb045 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadLayoutTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadLayoutTest.kt
@@ -156,6 +156,63 @@
     }
 
     @Test
+    fun defaultIntermediateMeasurePolicyInSubcomposeLayout() {
+        val expectedSizes = listOf(
+            IntSize(200, 100),
+            IntSize(400, 300),
+            IntSize(100, 500),
+            IntSize(20, 5),
+            IntSize(90, 120)
+        )
+        val targetSize = IntSize(260, 350)
+        var actualSize by mutableStateOf(IntSize.Zero)
+        var actualTargetSize by mutableStateOf(IntSize.Zero)
+        var iteration by mutableStateOf(0)
+
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                SubcomposeLayout(
+                    Modifier
+                        .requiredSize(targetSize.width.dp, targetSize.height.dp)
+                        .intermediateLayout { measurable, _ ->
+                            val intermediateConstraints = Constraints.fixed(
+                                expectedSizes[iteration].width,
+                                expectedSizes[iteration].height
+                            )
+                            measurable
+                                .measure(intermediateConstraints)
+                                .run {
+                                    layout(width, height) { place(0, 0) }
+                                }
+                        }) { constraints ->
+                    val placeable = subcompose(0) {
+                        Box(Modifier.fillMaxSize())
+                    }[0].measure(constraints)
+                    val size = placeable.run { IntSize(width, height) }
+                    if (this is SubcomposeIntermediateMeasureScope) {
+                        actualSize = size
+                    } else {
+                        actualTargetSize = size
+                    }
+                    layout(size.width, size.height) {
+                        placeable.place(0, 0)
+                    }
+                }
+            }
+        }
+
+        repeat(5) {
+            rule.runOnIdle {
+                assertEquals(targetSize, actualTargetSize)
+                assertEquals(expectedSizes[iteration], actualSize)
+                if (iteration < 4) {
+                    iteration++
+                }
+            }
+        }
+    }
+
+    @Test
     fun lookaheadLayoutAnimation() {
         var isLarge by mutableStateOf(true)
         var size1 = IntSize.Zero
@@ -1646,7 +1703,7 @@
     }
 
     @Test
-    fun subcomposeLayoutDefaultPlacementBehavior() {
+    fun subcomposeLayoutSkipToLookaheadConstraintsPlacementBehavior() {
         val actualPlacementOrder = mutableStateListOf<Int>()
         val expectedPlacementOrder1 = listOf(1, 3, 5, 2, 4, 0)
         val expectedPlacementOrder2 = listOf(2, 0, 3, 1, 5, 4)
@@ -1658,8 +1715,10 @@
         // Expect the default placement to be the same as lookahead
         rule.setContent {
             LookaheadScope {
-                val placeables = mutableListOf<Placeable>()
-                SubcomposeLayout { constraints ->
+                SubcomposeLayout(
+                    intermediateMeasurePolicy = { lookaheadMeasurePolicy(lookaheadConstraints) }
+                ) { constraints ->
+                    val placeables = mutableListOf<Placeable>()
                     repeat(3) { id ->
                         subcompose(id) {
                             Box(Modifier.trackMainPassPlacement {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index c42301b..6ebfd55 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -41,13 +41,11 @@
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.LayoutNode.LayoutState
 import androidx.compose.ui.node.LayoutNode.UsageByParent
-import androidx.compose.ui.node.LayoutNodeLayoutDelegate
 import androidx.compose.ui.node.requireOwner
 import androidx.compose.ui.platform.createSubcomposition
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.util.fastForEach
 
 /**
  * Analogue of [Layout] which allows to subcompose the actual content during the measuring stage
@@ -99,15 +97,15 @@
  * The intermediate measure pass allows adjustments to the measurement/placement using the
  * pre-calculated layout information as animation targets to smooth over any
  * any layout changes. In this measurement, [intermediateMeasurePolicy] will be invoked with
- * the intermediate/animating constraints. It is recommended to separate out
- * the logic for measuring/laying out children (that was used in [measurePolicy]) from subcomposing,
- * and share that logic in the [intermediateMeasurePolicy]. This will allow the measure/layout
- * logic to be _each frame_ (rather than only in the frames where the target constraints change)
- * with intermediate (or animating) constraints passed from parent layout.
+ * the intermediate/animating constraints. By default, [measurePolicy] will be invoked in
+ * [intermediateMeasurePolicy], and hence the same measure logic in [measurePolicy] with
+ * intermediate constraints will be used to measure and layout children in the intermediate
+ * pass.
  *
- * When no [intermediateMeasurePolicy] is provided (i.e. [intermediateMeasurePolicy] is `null`),
- * the default intermediateMeasurePolicy will be used to measure & layout all children with
- * the same constraints at the same position as the lookahead pass.
+ * Note: When [measurePolicy] is invoked in the intermediate pass, `subcompose` will simply
+ * return the measurables associated with the given slot id based on the subcomposition during
+ * lookahead pass. This means if a given slot id has not been subcomposed in the lookahead pass,
+ * invoking subcompose during intermediate pass will result in an empty list.
  *
  * Possible use cases:
  * * You need to know the constraints passed by the parent during the composition and can't solve
@@ -130,7 +128,9 @@
 fun SubcomposeLayout(
     modifier: Modifier = Modifier,
     intermediateMeasurePolicy:
-    (SubcomposeIntermediateMeasureScope.(Constraints) -> MeasureResult)? = null,
+    (SubcomposeIntermediateMeasureScope.(Constraints) -> MeasureResult) = { constraints ->
+        lookaheadMeasurePolicy(constraints)
+    },
     measurePolicy: SubcomposeMeasureScope.(Constraints) -> MeasureResult
 ) {
     SubcomposeLayout(
@@ -159,24 +159,24 @@
  * The intermediate measure pass allows adjustments to the measurement/placement using the
  * pre-calculated layout information as animation targets to smooth over any
  * any layout changes. In this measurement, intermediateMeasurePolicy will be invoked with
- * the intermediate/animating constraints. It is recommended to separate out
- * the logic for measuring/laying out children (that was used in measurePolicy) from subcomposing,
- * and share that logic in the intermediateMeasurePolicy. This will allow the measure/layout
- * logic to be _each frame_ (rather than only in the frames where the target constraints change)
- * with intermediate (or animating) constraints passed from parent layout.
+ * the intermediate/animating constraints. By default, measure policy will be invoked in
+ * intermediateMeasurePolicy, and hence the same measure logic in measurePolicy with
+ * intermediate constraints will be used to measure and layout children in the intermediate
+ * pass.
  *
- * Note: Unlike [SubcomposeMeasureScope], [SubcomposeIntermediateMeasureScope] does NOT permit
- * subcomposition. Instead, it allows retrieval of measurables from already subcomposed content
- * based on their slotId using [measurablesForSlot].
+ * Note: When measurePolicy is invoked in [SubcomposeIntermediateMeasureScope], `subcompose` will
+ * simply retrieve the measurables associated with the given slot id based on the subcomposition
+ * during lookahead pass. This means if a given slot id has not been subcomposed in the lookahead
+ * pass, invoking subcompose during intermediate pass will result in an empty list.
  *
  * @sample androidx.compose.ui.samples.SubcomposeLayoutWithIntermediateMeasurePolicySample
  */
 @ExperimentalComposeUiApi
-sealed interface SubcomposeIntermediateMeasureScope : MeasureScope {
+sealed interface SubcomposeIntermediateMeasureScope : SubcomposeMeasureScope {
     /**
      * Returns the list of measureables associated with [slotId] that was subcomposed in the
-     * [SubcomposeLayout]'s measurePolicy block. If the given [slotId] was not used in the
-     * subcomoposition, the returned list will be empty.
+     * [SubcomposeLayout]'s measurePolicy block during the lookahead pass. If the given [slotId]
+     * was not used in the subcomoposition, the returned list will be empty.
      */
     fun measurablesForSlot(slotId: Any?): List<Measurable>
 
@@ -184,6 +184,47 @@
      * The size returned in the [MeasureResult] by the measurePolicy invoked during lookahead pass.
      */
     val lookaheadSize: IntSize
+
+    /**
+     * This is the measure policy that is supplied to SubcomposeLayout in the measurePolicy
+     * parameter. It is used in the lookahead pass, and it is also invoked by default in the
+     * intermediateMeasurePolicy for the intermediate measure pass.
+     *
+     * During the intermediate pass, the [lookaheadMeasurePolicy] will receive potentially
+     * different (i.e. animating) constraints, and will subsequently remeasure and replace
+     * all children according to the new constraints.
+     *
+     * Note: Intermediate measure pass will NOT run **new** subcompositions. [subcompose]
+     * calls in from the [lookaheadMeasurePolicy] in this pass will instead retrieve the measurables
+     * for the given slot based on the subcomposition from lookahead pass. In the rare
+     * case where slots are subcomposed conditionally dependent on constraints, it's recommended
+     * to provide to [SubcomposeLayout] a custom intermediate measure policy. A less desirable
+     * solution to this use case is to invoke [lookaheadMeasurePolicy] with [lookaheadConstraints]
+     * as its parameter, which will skip any intermediate stages straight to the lookahead
+     * sizes & positions.
+     */
+    val lookaheadMeasurePolicy: SubcomposeMeasureScope.(Constraints) -> MeasureResult
+
+    /**
+     * Returns the [Constraints] used in the lookahead pass.
+     *
+     * Note: Using this with [lookaheadMeasurePolicy] will effectively skip any intermediate stages
+     * from lookahead-based layout animations. Therefore it is recommended to use [Constraints]
+     * passed to intermediate measure policy to measure and layout children during intermediate
+     * pass. The only exception to that should be when some of the subcompositions are conditional.
+     * In that case, a custom intermediate measure policy should ideally be provided to
+     * [SubcomposeLayout]. Using [lookaheadConstraints] with [lookaheadMeasurePolicy] should be
+     * considered as the last resort.
+     */
+    val lookaheadConstraints: Constraints
+
+    /**
+     * This function retrieves [Measurable]s created for [slotId] based on
+     * the subcomposition that happened in the lookahead pass. If [slotId] was not subcomposed
+     * in the lookahead pass, [subcompose] will return an [emptyList].
+     */
+    override fun subcompose(slotId: Any?, content: @Composable () -> Unit): List<Measurable> =
+        measurablesForSlot(slotId)
 }
 
 /**
@@ -214,7 +255,7 @@
     measurePolicy: SubcomposeMeasureScope.(Constraints) -> MeasureResult
 ) {
     @OptIn(ExperimentalComposeUiApi::class)
-    SubcomposeLayout(state, modifier, null, measurePolicy)
+    SubcomposeLayout(state, modifier, { lookaheadMeasurePolicy(it) }, measurePolicy)
 }
 
 /**
@@ -236,15 +277,15 @@
  * The intermediate measure pass allows adjustments to the measurement/placement using the
  * pre-calculated layout information as animation targets to smooth over any
  * any layout changes. In this measurement, [intermediateMeasurePolicy] will be invoked with
- * the intermediate/animating constraints. It is recommended to separate out
- * the logic for measuring/laying out children (that was used in [measurePolicy]) from subcomposing,
- * and share that logic in the [intermediateMeasurePolicy]. This will allow the measure/layout
- * logic to be _each frame_ (rather than only in the frames where the target constraints change)
- * with intermediate (or animating) constraints passed from parent layout.
+ * the intermediate/animating constraints. By default, [measurePolicy] will be invoked in
+ * [intermediateMeasurePolicy], and hence the same measure logic in [measurePolicy] with
+ * intermediate constraints will be used to measure and layout children in the intermediate
+ * pass.
  *
- * When no [intermediateMeasurePolicy] is provided (i.e. [intermediateMeasurePolicy] is `null`),
- * the default intermediateMeasurePolicy will be used to measure & layout all children with
- * the same constraints at the same position as the lookahead pass.
+ * Note: When [measurePolicy] is invoked in the intermediate pass, `subcompose` will simply
+ * return the measurables associated with the given slot id based on the subcomposition during
+ * lookahead pass. This means if a given slot id has not been subcomposed in the lookahead pass,
+ * invoking subcompose during intermediate pass will result in an empty list.
  *
  * Possible use cases:
  * * You need to know the constraints passed by the parent during the composition and can't solve
@@ -268,7 +309,9 @@
     state: SubcomposeLayoutState,
     modifier: Modifier = Modifier,
     intermediateMeasurePolicy:
-    (SubcomposeIntermediateMeasureScope.(Constraints) -> MeasureResult)? = null,
+    (SubcomposeIntermediateMeasureScope.(Constraints) -> MeasureResult) = { constraints ->
+        lookaheadMeasurePolicy(constraints)
+    },
     measurePolicy: SubcomposeMeasureScope.(Constraints) -> MeasureResult
 ) {
     val compositionContext = rememberCompositionContext()
@@ -386,8 +429,8 @@
         { measurePolicy = state.createMeasurePolicy(it) }
 
     internal val setIntermediateMeasurePolicy:
-        LayoutNode.((SubcomposeIntermediateMeasureScope.(Constraints) -> MeasureResult)?) -> Unit =
-        { state.intermediateMeasurePolicy = it ?: state.defaultIntermediateMeasurePolicy }
+        LayoutNode.(SubcomposeIntermediateMeasureScope.(Constraints) -> MeasureResult) -> Unit =
+        { state.intermediateMeasurePolicy = it }
 
     /**
      * Composes the content for the given [slotId]. This makes the next scope.subcompose(slotId)
@@ -559,53 +602,17 @@
     private val slotIdToNode = mutableMapOf<Any?, LayoutNode>()
     private val scope = Scope()
     private val intermediateMeasureScope = IntermediateMeasureScopeImpl()
-    private var lookaheadMeasuredSize: IntSize = IntSize.Zero
-
-    /**
-     * Default intermediate measure policy. It measures the children with the lookahead constraints
-     * in the same phase as lookahead (e.g. if a child is measured in placement block in lookahead
-     * it will be measured in placement block in the intermediate measure pass.)
-     * Children will be placed at the lookahead position. Child placement order is also ensured to
-     * be consistent with lookahead.
-     */
-    internal val defaultIntermediateMeasurePolicy:
-        SubcomposeIntermediateMeasureScope.(Constraints) -> MeasureResult = {
-        // Measure all the children that were lookahead-measured in the measure pass
-        root.measurePassDelegate.childDelegates.fastForEach {
-            it.measureBasedOnLookahead()
-        }
-
-        layout(lookaheadSize.width, lookaheadSize.height) {
-            // Going through the children again to measure any child that is supposed to be
-            // measured in the layout pass.
-            root.measurePassDelegate.childDelegates.fastForEach {
-                it.measureBasedOnLookahead()
-            }
-            // Place all the placeables in the sequence they were placed in lookahead
-            val placeOrderList = arrayOfNulls<LayoutNodeLayoutDelegate.MeasurePassDelegate?>(
-                root.childLookaheadMeasurables.size
-            )
-            // Create a list to store the delegates in the order they are placed
-            root.lookaheadPassDelegate!!.childDelegates.fastForEach {
-                if (it.placeOrder != LayoutNode.NotPlacedPlaceOrder) {
-                    placeOrderList[it.placeOrder] = it.measurePassDelegate
-                }
-            }
-            placeOrderList.forEach {
-                it?.placeBasedOnLookahead()
-            }
-        }
-    }
 
     /**
      * This is the intermediateMeasurePolicy that developers set in [SubcomposeLayout]. It defaults
-     * to [defaultIntermediateMeasurePolicy].
+     * to invoking [SubcomposeIntermediateMeasureScope.lookaheadMeasurePolicy].
      *
      * Note: This intermediate measure policy is only invoked when in a [LookaheadScope].
      */
     internal var intermediateMeasurePolicy:
-        (SubcomposeIntermediateMeasureScope.(Constraints) -> MeasureResult) =
-        defaultIntermediateMeasurePolicy
+        (SubcomposeIntermediateMeasureScope.(Constraints) -> MeasureResult) = {
+        lookaheadMeasurePolicy(it)
+    }
     private val precomposeMap = mutableMapOf<Any?, LayoutNode>()
     private val reusableSlotIdsSet = SubcomposeSlotReusePolicy.SlotIdsSet()
 
@@ -832,36 +839,40 @@
 
     fun createMeasurePolicy(
         block: SubcomposeMeasureScope.(Constraints) -> MeasureResult
-    ): MeasurePolicy = object : LayoutNode.NoIntrinsicsMeasurePolicy(error = NoIntrinsicsMessage) {
-        override fun MeasureScope.measure(
-            measurables: List<Measurable>,
-            constraints: Constraints
-        ): MeasureResult {
-            scope.layoutDirection = layoutDirection
-            scope.density = density
-            scope.fontScale = fontScale
-            val isIntermediate =
-                (root.layoutState == LayoutState.Measuring ||
-                    root.layoutState == LayoutState.LayingOut) && root.lookaheadRoot != null
-            if (isIntermediate) {
-                return intermediateMeasurePolicy.invoke(intermediateMeasureScope, constraints)
-            } else {
-                currentIndex = 0
-                val result = scope.block(constraints)
-                val indexAfterMeasure = currentIndex
-                lookaheadMeasuredSize = IntSize(result.width, result.height)
-                return object : MeasureResult {
-                    override val width: Int
-                        get() = result.width
-                    override val height: Int
-                        get() = result.height
-                    override val alignmentLines: Map<AlignmentLine, Int>
-                        get() = result.alignmentLines
+    ): MeasurePolicy {
+        intermediateMeasureScope.lookaheadMeasurePolicy = block
+        return object : LayoutNode.NoIntrinsicsMeasurePolicy(error = NoIntrinsicsMessage) {
+            override fun MeasureScope.measure(
+                measurables: List<Measurable>,
+                constraints: Constraints
+            ): MeasureResult {
+                scope.layoutDirection = layoutDirection
+                scope.density = density
+                scope.fontScale = fontScale
+                val isIntermediate =
+                    (root.layoutState == LayoutState.Measuring ||
+                        root.layoutState == LayoutState.LayingOut) && root.lookaheadRoot != null
+                if (isIntermediate) {
+                    return intermediateMeasurePolicy.invoke(intermediateMeasureScope, constraints)
+                } else {
+                    currentIndex = 0
+                    intermediateMeasureScope.lookaheadConstraints = constraints
+                    val result = scope.block(constraints)
+                    val indexAfterMeasure = currentIndex
+                    intermediateMeasureScope.lookaheadSize = IntSize(result.width, result.height)
+                    return object : MeasureResult {
+                        override val width: Int
+                            get() = result.width
+                        override val height: Int
+                            get() = result.height
+                        override val alignmentLines: Map<AlignmentLine, Int>
+                            get() = result.alignmentLines
 
-                    override fun placeChildren() {
-                        currentIndex = indexAfterMeasure
-                        result.placeChildren()
-                        disposeOrReuseStartingFromIndex(currentIndex)
+                        override fun placeChildren() {
+                            currentIndex = indexAfterMeasure
+                            result.placeChildren()
+                            disposeOrReuseStartingFromIndex(currentIndex)
+                        }
                     }
                 }
             }
@@ -1003,8 +1014,10 @@
          * This is the size returned in the MeasureResult in the measure policy from the lookahead
          * pass.
          */
-        override val lookaheadSize: IntSize
-            get() = lookaheadMeasuredSize
+        override var lookaheadSize: IntSize = IntSize.Zero
+        override lateinit var lookaheadMeasurePolicy:
+            SubcomposeMeasureScope.(Constraints) -> MeasureResult
+        override var lookaheadConstraints: Constraints = Constraints()
     }
 }