[go: nahoru, domu]

Adds motion support for ThreePaneScaffold

Note: This is an initial implementation that will be used as a base for
additional changes (including API changes) and modifications that will
follow right after.

The change adds support for enter and exit transitions that are auto-
applied to the different panes at the ThreePaneScaffold.

The actual implementation is mapping the transitions that were defined
at the ThreePaneMotion to the actual pane with the current arrangement.
Then the PaneWrapper is applying an AnimatedVisibility using those
transitions.

Test: Manual
Bug: 296455429
Change-Id: I9b2e9ca41a4e4419e9661037856ba9bf0b4dbaba
diff --git a/compose/material3/material3-adaptive/build.gradle b/compose/material3/material3-adaptive/build.gradle
index aeb1c75..5af3dc0 100644
--- a/compose/material3/material3-adaptive/build.gradle
+++ b/compose/material3/material3-adaptive/build.gradle
@@ -19,6 +19,7 @@
 import androidx.build.LibraryType
 import androidx.build.PlatformIdentifier
 import androidx.build.Publish
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -122,6 +123,10 @@
     description = "Compose Material Design Adaptive Library"
 }
 
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
+}
+
 // Screenshot tests related setup
 android {
     sourceSets.androidTest.assets.srcDirs +=
diff --git a/compose/material3/material3-adaptive/samples/build.gradle b/compose/material3/material3-adaptive/samples/build.gradle
index 4e99c03..b9d84b8 100644
--- a/compose/material3/material3-adaptive/samples/build.gradle
+++ b/compose/material3/material3-adaptive/samples/build.gradle
@@ -36,6 +36,8 @@
     implementation(project(":compose:material3:material3-window-size-class"))
     implementation(project(":compose:ui:ui-util"))
     implementation("androidx.compose.ui:ui-tooling-preview:1.4.1")
+
+    debugImplementation("androidx.compose.ui:ui-tooling:1.4.1")
 }
 
 androidx {
diff --git a/compose/material3/material3-adaptive/samples/src/main/java/androidx/compose/material3-adaptive/samples/ThreePaneScaffoldSample.kt b/compose/material3/material3-adaptive/samples/src/main/java/androidx/compose/material3-adaptive/samples/ThreePaneScaffoldSample.kt
new file mode 100644
index 0000000..943a1a3
--- /dev/null
+++ b/compose/material3/material3-adaptive/samples/src/main/java/androidx/compose/material3-adaptive/samples/ThreePaneScaffoldSample.kt
@@ -0,0 +1,195 @@
+/*
+ * 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.material3.adaptive.samples
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.VerticalDivider
+import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.material3.adaptive.ListDetailPaneScaffold
+import androidx.compose.material3.adaptive.ListDetailPaneScaffoldRole
+import androidx.compose.material3.adaptive.ThreePaneScaffold
+import androidx.compose.material3.adaptive.ThreePaneScaffoldDefaults
+import androidx.compose.material3.adaptive.calculateStandardAdaptiveLayoutDirective
+import androidx.compose.material3.adaptive.calculateThreePaneScaffoldValue
+import androidx.compose.material3.adaptive.calculateWindowAdaptiveInfo
+import androidx.compose.material3.adaptive.rememberListDetailPaneScaffoldState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@Preview
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@Composable
+internal fun ListDetailPaneScaffoldSample() {
+    val layoutState = rememberListDetailPaneScaffoldState(
+        initialFocusHistory = listOf(ListDetailPaneScaffoldRole.List)
+    )
+    ListDetailPaneScaffold(
+        layoutState = layoutState,
+        listPane = {
+            Surface(
+                modifier = Modifier.preferredWidth(200.dp),
+                color = MaterialTheme.colorScheme.secondary,
+                >
+                    layoutState.navigateTo(ListDetailPaneScaffoldRole.Detail)
+                }
+            ) {
+                Text("List")
+            }
+        },
+    ) {
+        Surface(
+            color = MaterialTheme.colorScheme.primary,
+            >
+                layoutState.navigateBack()
+            }
+        ) {
+            Text("Details")
+        }
+    }
+}
+
+@Preview
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@Composable
+internal fun ListDetailExtraPaneScaffoldSample() {
+    val layoutState = rememberListDetailPaneScaffoldState(
+        initialFocusHistory = listOf(ListDetailPaneScaffoldRole.List)
+    )
+
+    ListDetailPaneScaffold(
+        layoutState = layoutState,
+        listPane = {
+            Surface(
+                modifier = Modifier.preferredWidth(200.dp),
+                color = MaterialTheme.colorScheme.secondary,
+                >
+                    layoutState.navigateTo(ListDetailPaneScaffoldRole.Detail)
+                }
+            ) {
+                Text("List")
+            }
+        },
+        extraPane = {
+            Surface(
+                modifier = Modifier.fillMaxSize(),
+                color = MaterialTheme.colorScheme.tertiary,
+                >
+                    layoutState.navigateBack()
+                }
+            ) {
+                Text("Extra")
+            }
+        }
+    ) {
+        Surface(
+            color = MaterialTheme.colorScheme.primary,
+        ) {
+            Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
+                Text("Details")
+                Row(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .padding(horizontal = 4.dp),
+                    horizontalArrangement = Arrangement.spacedBy(8.dp)
+                ) {
+                    Surface(
+                        >
+                            layoutState.navigateBack()
+                        },
+                        modifier = Modifier
+                            .weight(0.5f)
+                            .fillMaxHeight(),
+                        color = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f)
+                    ) {
+                        Box(
+                            modifier = Modifier.fillMaxSize(),
+                            contentAlignment = Alignment.Center
+                        ) {
+                            Text("Previous")
+                        }
+                    }
+                    VerticalDivider()
+                    Surface(
+                        >
+                            layoutState.navigateTo(ListDetailPaneScaffoldRole.Extra)
+                        },
+                        modifier = Modifier
+                            .weight(0.5f)
+                            .fillMaxHeight(),
+                        color = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
+                    ) {
+                        Box(
+                            modifier = Modifier.fillMaxSize(),
+                            contentAlignment = Alignment.Center
+                        ) {
+                            Text("Next")
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Preview
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@Composable
+internal fun ThreePaneScaffoldSample() {
+    val layoutDirective = calculateStandardAdaptiveLayoutDirective(calculateWindowAdaptiveInfo())
+    ThreePaneScaffold(
+        modifier = Modifier.fillMaxSize(),
+        layoutDirective = layoutDirective,
+        scaffoldValue = calculateThreePaneScaffoldValue(layoutDirective.maxHorizontalPartitions),
+        arrangement = ThreePaneScaffoldDefaults.ListDetailLayoutArrangement,
+        secondaryPane = {
+            Surface(
+                modifier = Modifier.preferredWidth(100.dp),
+                color = MaterialTheme.colorScheme.secondary
+            ) {
+                Text("Secondary")
+            }
+        },
+        tertiaryPane = {
+            Surface(
+                modifier = Modifier.fillMaxSize(),
+                color = MaterialTheme.colorScheme.tertiary
+            ) {
+                Text("Tertiary")
+            }
+        }
+    ) {
+        Surface(
+            modifier = Modifier.fillMaxSize(),
+            color = MaterialTheme.colorScheme.primary
+        ) {
+            Text("Primary")
+        }
+    }
+}
diff --git a/compose/material3/material3-adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/ListDetailPaneScaffold.kt b/compose/material3/material3-adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/ListDetailPaneScaffold.kt
index 54b781c..bc93c71 100644
--- a/compose/material3/material3-adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/ListDetailPaneScaffold.kt
+++ b/compose/material3/material3-adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/ListDetailPaneScaffold.kt
@@ -179,12 +179,14 @@
      * The list pane of [ListDetailPaneScaffold]. It is mapped to [ThreePaneScaffoldRole.Secondary].
      */
     List(ThreePaneScaffoldRole.Secondary),
+
     /**
      * The detail pane of [ListDetailPaneScaffold]. It is mapped to [ThreePaneScaffoldRole.Primary].
      */
     Detail(ThreePaneScaffoldRole.Primary),
+
     /**
      * The extra pane of [ListDetailPaneScaffold]. It is mapped to [ThreePaneScaffoldRole.Tertiary].
      */
-    Extra(ThreePaneScaffoldRole.Tertiary)
+    Extra(ThreePaneScaffoldRole.Tertiary);
 }
diff --git a/compose/material3/material3-adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffold.android.kt b/compose/material3/material3-adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffold.android.kt
index f6da9a4..0175dc0 100644
--- a/compose/material3/material3-adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffold.android.kt
+++ b/compose/material3/material3-adaptive/src/androidMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffold.android.kt
@@ -80,7 +80,7 @@
          */
         fun saver(
             initialLayoutDirective: AdaptiveLayoutDirective,
-            initialAdaptStrategies: ThreePaneScaffoldAdaptStrategies,
+            initialAdaptStrategies: ThreePaneScaffoldAdaptStrategies
         ): Saver<DefaultThreePaneScaffoldState, *> = listSaver(
             save = {
                 it.focusHistory.toList()
@@ -89,7 +89,7 @@
                 DefaultThreePaneScaffoldState(
                     initialFocusHistory = it,
                     initialLayoutDirective = initialLayoutDirective,
-                    initialAdaptStrategies = initialAdaptStrategies,
+                    initialAdaptStrategies = initialAdaptStrategies
                 )
             }
         )
diff --git a/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/AnimateBoundsModifier.kt b/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/AnimateBoundsModifier.kt
new file mode 100644
index 0000000..a72d4a4
--- /dev/null
+++ b/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/AnimateBoundsModifier.kt
@@ -0,0 +1,198 @@
+/*
+ * 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.material3.adaptive
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector
+import androidx.compose.animation.core.AnimationVector2D
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.TwoWayConverter
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.spring
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.graphics.drawscope.translate
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.intermediateLayout
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.round
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@Suppress("IllegalExperimentalApiUsage") // TODO: address before moving to beta
+@OptIn(ExperimentalComposeUiApi::class)
+internal fun Modifier.animateBounds(
+    modifier: Modifier = Modifier,
+    sizeAnimationSpec: FiniteAnimationSpec<IntSize> = spring(
+        Spring.DampingRatioNoBouncy,
+        Spring.StiffnessMediumLow
+    ),
+    positionAnimationSpec: FiniteAnimationSpec<IntOffset> = spring(
+        Spring.DampingRatioNoBouncy,
+        Spring.StiffnessMediumLow
+    ),
+    debug: Boolean = false,
+    lookaheadScope: (closestLookaheadScope: LookaheadScope) -> LookaheadScope = { it }
+) = composed {
+
+    val outerOffsetAnimation = remember { DeferredAnimation(IntOffset.VectorConverter) }
+    val outerSizeAnimation = remember { DeferredAnimation(IntSize.VectorConverter) }
+
+    val offsetAnimation = remember { DeferredAnimation(IntOffset.VectorConverter) }
+    val sizeAnimation = remember { DeferredAnimation(IntSize.VectorConverter) }
+
+    // The measure logic in `intermediateLayout` is skipped in the lookahead pass, as
+    // intermediateLayout is expected to produce intermediate stages of a layout transform.
+    // When the measure block is invoked after lookahead pass, the lookahead size of the
+    // child will be accessible as a parameter to the measure block.
+    this
+        .drawWithContent {
+            drawContent()
+            if (debug) {
+                val offset = outerOffsetAnimation.target!! - outerOffsetAnimation.value!!
+                translate(
+                    offset.x.toFloat(), offset.y.toFloat()
+                ) {
+                    drawRect(Color.Black.copy(alpha = 0.5f), style = Stroke(10f))
+                }
+            }
+        }
+        .intermediateLayout { measurable, constraints ->
+            val (w, h) = outerSizeAnimation.updateTarget(
+                lookaheadSize,
+                sizeAnimationSpec,
+            )
+            measurable
+                .measure(constraints)
+                .run {
+                    layout(w, h) {
+                        val (x, y) = outerOffsetAnimation.updateTargetBasedOnCoordinates(
+                            positionAnimationSpec
+                        )
+                        place(x, y)
+                    }
+                }
+        }
+        .then(modifier)
+        .drawWithContent {
+            drawContent()
+            if (debug) {
+                val offset = offsetAnimation.target!! - offsetAnimation.value!!
+                translate(
+                    offset.x.toFloat(), offset.y.toFloat()
+                ) {
+                    drawRect(Color.Green.copy(alpha = 0.5f), style = Stroke(10f))
+                }
+            }
+        }
+        .intermediateLayout { measurable, _ ->
+            // When layout changes, the lookahead pass will calculate a new final size for the
+            // child modifier. This lookahead size can be used to animate the size
+            // change, such that the animation starts from the current size and gradually
+            // change towards `lookaheadSize`.
+            val (width, height) = sizeAnimation.updateTarget(
+                lookaheadSize,
+                sizeAnimationSpec,
+            )
+            // Creates a fixed set of constraints using the animated size
+            val animatedConstraints = Constraints.fixed(width, height)
+            // Measure child/children with animated constraints.
+            val placeable = measurable.measure(animatedConstraints)
+            layout(placeable.width, placeable.height) {
+                val (x, y) = with(lookaheadScope(this@intermediateLayout)) {
+                    offsetAnimation.updateTargetBasedOnCoordinates(
+                        positionAnimationSpec,
+                    )
+                }
+                placeable.place(x, y)
+            }
+        }
+}
+
+context(LookaheadScope, Placeable.PlacementScope, CoroutineScope)
+@Suppress("IllegalExperimentalApiUsage") // TODO: address before moving to beta
+@OptIn(ExperimentalComposeUiApi::class)
+internal fun DeferredAnimation<IntOffset, AnimationVector2D>.updateTargetBasedOnCoordinates(
+    animationSpec: FiniteAnimationSpec<IntOffset>,
+): IntOffset {
+    coordinates?.let { coordinates ->
+        with(this@PlacementScope) {
+            val targetOffset = lookaheadScopeCoordinates.localLookaheadPositionOf(coordinates)
+            val animOffset = updateTarget(
+                targetOffset.round(),
+                animationSpec,
+            )
+            val current = lookaheadScopeCoordinates.localPositionOf(
+                coordinates,
+                Offset.Zero
+            ).round()
+            return (animOffset - current)
+        }
+    }
+
+    return IntOffset.Zero
+}
+
+// Experimenting with a way to initialize animation during measurement && only take the last target
+// change in a frame (if the target was changed multiple times in the same frame) as the
+// animation target.
+internal class DeferredAnimation<T, V : AnimationVector>(
+    private val vectorConverter: TwoWayConverter<T, V>
+) {
+    val value: T?
+        get() = animatable?.value ?: target
+    var target: T? by mutableStateOf(null)
+        private set
+    private var animatable: Animatable<T, V>? = null
+
+    internal val isActive: Boolean
+        get() = target != animatable?.targetValue || animatable?.isRunning == true
+
+    context (CoroutineScope)
+    fun updateTarget(
+        targetValue: T,
+        animationSpec: FiniteAnimationSpec<T>,
+    ): T {
+        target = targetValue
+        if (target != null && target != animatable?.targetValue) {
+            animatable?.run {
+                launch {
+                    animateTo(
+                        targetValue,
+                        animationSpec
+                    )
+                }
+            } ?: Animatable(targetValue, vectorConverter).let {
+                animatable = it
+            }
+        }
+        return animatable?.value ?: targetValue
+    }
+}
diff --git a/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffold.kt b/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffold.kt
index bd47b23..9d35814 100644
--- a/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffold.kt
+++ b/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffold.kt
@@ -16,15 +16,33 @@
 
 package androidx.compose.material3.adaptive
 
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.animation.core.VisibilityThreshold
+import androidx.compose.animation.core.snap
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.LookaheadScope
 import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.MultiContentMeasurePolicy
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.layout.boundsInWindow
 import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntRect
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.roundToIntRect
@@ -64,53 +82,302 @@
     arrangement: ThreePaneScaffoldArrangement,
     secondaryPane: @Composable ThreePaneScaffoldScope.(PaneAdaptedValue) -> Unit,
     tertiaryPane: (@Composable ThreePaneScaffoldScope.(PaneAdaptedValue) -> Unit)? = null,
-    primaryPane: @Composable ThreePaneScaffoldScope.(PaneAdaptedValue) -> Unit
+    primaryPane: @Composable ThreePaneScaffoldScope.(PaneAdaptedValue) -> Unit,
 ) {
+    val previousScaffoldValue = remember { ThreePaneScaffoldValueHolder(scaffoldValue) }
+    val paneMotion = calculateThreePaneMotion(
+        previousScaffoldValue.value,
+        scaffoldValue,
+        arrangement
+    )
+    previousScaffoldValue.value = scaffoldValue
+
+    // Create PaneWrappers for each of the panes and map the transitions according to each pane
+    // role and arrangement.
     val contents = listOf<@Composable () -> Unit>(
-        { PaneWrapper(scaffoldValue[ThreePaneScaffoldRole.Primary], primaryPane) },
-        { PaneWrapper(scaffoldValue[ThreePaneScaffoldRole.Secondary], secondaryPane) },
-        { PaneWrapper(scaffoldValue[ThreePaneScaffoldRole.Tertiary], tertiaryPane) },
+        {
+            PaneWrapper(
+                scaffoldValue[ThreePaneScaffoldRole.Primary],
+                positionAnimationSpec = paneMotion.animationSpec,
+                enterTransition = paneMotion.enterTransition(
+                    ThreePaneScaffoldRole.Primary,
+                    arrangement
+                ),
+                exitTransition = paneMotion.exitTransition(
+                    ThreePaneScaffoldRole.Primary,
+                    arrangement
+                ),
+                label = "Primary",
+                content = primaryPane
+            )
+        },
+        {
+            PaneWrapper(
+                scaffoldValue[ThreePaneScaffoldRole.Secondary],
+                positionAnimationSpec = paneMotion.animationSpec,
+                enterTransition = paneMotion.enterTransition(
+                    ThreePaneScaffoldRole.Secondary,
+                    arrangement
+                ),
+                exitTransition = paneMotion.exitTransition(
+                    ThreePaneScaffoldRole.Secondary,
+                    arrangement
+                ),
+                label = "Secondary",
+                content = secondaryPane
+            )
+        },
+        {
+            PaneWrapper(
+                scaffoldValue[ThreePaneScaffoldRole.Tertiary],
+                positionAnimationSpec = paneMotion.animationSpec,
+                enterTransition = paneMotion.enterTransition(
+                    ThreePaneScaffoldRole.Tertiary,
+                    arrangement
+                ),
+                exitTransition = paneMotion.exitTransition(
+                    ThreePaneScaffoldRole.Tertiary,
+                    arrangement
+                ),
+                label = "Tertiary",
+                content = tertiaryPane
+            )
+        },
     )
 
-    Layout(
-        contents = contents,
-        modifier = modifier,
-    ) { (primaryMeasurables, secondaryMeasurables, tertiaryMeasurables), constraints ->
-        layout(constraints.maxWidth, constraints.maxHeight) {
+    val measurePolicy =
+        remember { ThreePaneContentMeasurePolicy(layoutDirective, scaffoldValue, arrangement) }
+    measurePolicy.layoutDirective = layoutDirective
+    measurePolicy.scaffoldValue = scaffoldValue
+    measurePolicy.arrangement = arrangement
+
+    LookaheadScope {
+        Layout(
+            contents = contents,
+            modifier = modifier,
+            measurePolicy = measurePolicy
+        )
+    }
+}
+
+/**
+ * Holds the transitions that can be applied to the different panes.
+ */
+@ExperimentalMaterial3AdaptiveApi
+@Immutable
+internal class ThreePaneMotion internal constructor(
+    internal val animationSpec: FiniteAnimationSpec<IntOffset> = snap(),
+    private val firstPaneEnterTransition: EnterTransition = EnterTransition.None,
+    private val firstPaneExitTransition: ExitTransition = ExitTransition.None,
+    private val secondPaneEnterTransition: EnterTransition = EnterTransition.None,
+    private val secondPaneExitTransition: ExitTransition = ExitTransition.None,
+    private val thirdPaneEnterTransition: EnterTransition = EnterTransition.None,
+    private val thirdPaneExitTransition: ExitTransition = ExitTransition.None
+) {
+
+    /**
+     * Resolves and returns the [EnterTransition] for the given [ThreePaneScaffoldRole] at the given
+     * [ThreePaneScaffoldArrangement].
+     */
+    fun enterTransition(
+        role: ThreePaneScaffoldRole,
+        arrangement: ThreePaneScaffoldArrangement
+    ): EnterTransition {
+        // Quick return in case this instance is the NoMotion one.
+        if (this === NoMotion) return EnterTransition.None
+
+        return when (arrangement.indexOf(role)) {
+            0 -> firstPaneEnterTransition
+            1 -> secondPaneEnterTransition
+            else -> thirdPaneEnterTransition
+        }
+    }
+
+    /**
+     * Resolves and returns the [ExitTransition] for the given [ThreePaneScaffoldRole] at the given
+     * [ThreePaneScaffoldArrangement].
+     */
+    fun exitTransition(
+        role: ThreePaneScaffoldRole,
+        arrangement: ThreePaneScaffoldArrangement
+    ): ExitTransition {
+        // Quick return in case this instance is the NoMotion one.
+        if (this === NoMotion) return ExitTransition.None
+
+        return when (arrangement.indexOf(role)) {
+            0 -> firstPaneExitTransition
+            1 -> secondPaneExitTransition
+            else -> thirdPaneExitTransition
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is ThreePaneMotion) return false
+        if (this.animationSpec != other.animationSpec) return false
+        if (this.firstPaneEnterTransition != other.firstPaneEnterTransition) return false
+        if (this.firstPaneExitTransition != other.firstPaneExitTransition) return false
+        if (this.secondPaneEnterTransition != other.secondPaneEnterTransition) return false
+        if (this.secondPaneExitTransition != other.secondPaneExitTransition) return false
+        if (this.thirdPaneEnterTransition != other.thirdPaneEnterTransition) return false
+        if (this.thirdPaneExitTransition != other.thirdPaneExitTransition) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = animationSpec.hashCode()
+        result = 31 * result + firstPaneEnterTransition.hashCode()
+        result = 31 * result + firstPaneExitTransition.hashCode()
+        result = 31 * result + secondPaneEnterTransition.hashCode()
+        result = 31 * result + secondPaneExitTransition.hashCode()
+        result = 31 * result + thirdPaneEnterTransition.hashCode()
+        result = 31 * result + thirdPaneExitTransition.hashCode()
+        return result
+    }
+
+    companion object {
+        /**
+         * A ThreePaneMotion with all transitions set to [EnterTransition.None] and
+         * [ExitTransition.None].
+         */
+        val NoMotion = ThreePaneMotion()
+    }
+}
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+private class ThreePaneScaffoldValueHolder(var value: ThreePaneScaffoldValue)
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+private fun calculateThreePaneMotion(
+    previousScaffoldValue: ThreePaneScaffoldValue,
+    currentScaffoldValue: ThreePaneScaffoldValue,
+    arrangement: ThreePaneScaffoldArrangement
+): ThreePaneMotion {
+    if (previousScaffoldValue.equals(currentScaffoldValue)) {
+        return ThreePaneMotion.NoMotion
+    }
+    val previousExpandedCount = getExpandedCount(previousScaffoldValue)
+    val currentExpandedCount = getExpandedCount(currentScaffoldValue)
+    if (previousExpandedCount != currentExpandedCount) {
+        // TODO(conradchen): Address this case
+        return ThreePaneMotion.NoMotion
+    }
+    return when (previousExpandedCount) {
+        1 -> when (PaneAdaptedValue.Expanded) {
+            previousScaffoldValue[arrangement.firstPane] -> {
+                ThreePaneScaffoldDefaults.panesRightMotion
+            }
+
+            previousScaffoldValue[arrangement.thirdPane] -> {
+                ThreePaneScaffoldDefaults.panesLeftMotion
+            }
+
+            currentScaffoldValue[arrangement.thirdPane] -> {
+                ThreePaneScaffoldDefaults.panesRightMotion
+            }
+
+            else -> {
+                ThreePaneScaffoldDefaults.panesLeftMotion
+            }
+        }
+
+        2 -> when {
+            previousScaffoldValue[arrangement.firstPane] != PaneAdaptedValue.Expanded -> {
+                ThreePaneScaffoldDefaults.panesLeftMotion
+            }
+
+            previousScaffoldValue[arrangement.thirdPane] != PaneAdaptedValue.Expanded -> {
+                ThreePaneScaffoldDefaults.panesRightMotion
+            }
+
+            else -> {
+                // TODO(conradchen): Address this case when we need to support supporting pane
+                ThreePaneMotion.NoMotion
+            }
+        }
+
+        else -> {
+            // Should not happen
+            ThreePaneMotion.NoMotion
+        }
+    }
+}
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+private fun getExpandedCount(scaffoldValue: ThreePaneScaffoldValue): Int {
+    var count = 0
+    if (scaffoldValue.primary == PaneAdaptedValue.Expanded) {
+        count++
+    }
+    if (scaffoldValue.secondary == PaneAdaptedValue.Expanded) {
+        count++
+    }
+    if (scaffoldValue.tertiary == PaneAdaptedValue.Expanded) {
+        count++
+    }
+    return count
+}
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+private class ThreePaneContentMeasurePolicy(
+    var layoutDirective: AdaptiveLayoutDirective,
+    var scaffoldValue: ThreePaneScaffoldValue,
+    var arrangement: ThreePaneScaffoldArrangement
+) : MultiContentMeasurePolicy {
+
+    /**
+     * Data class that is used to store the position and width of an expanded pane to be reused when
+     * the pane is being hidden.
+     */
+    private data class PanePlacement(var positionX: Int = 0, var measuredWidth: Int = 0)
+
+    private val placementsCache = mapOf(
+        ThreePaneScaffoldRole.Primary to PanePlacement(),
+        ThreePaneScaffoldRole.Secondary to PanePlacement(),
+        ThreePaneScaffoldRole.Tertiary to PanePlacement()
+    )
+
+    override fun MeasureScope.measure(
+        measurables: List<List<Measurable>>,
+        constraints: Constraints
+    ): MeasureResult {
+        val primaryMeasurables = measurables[0]
+        val secondaryMeasurables = measurables[1]
+        val tertiaryMeasurables = measurables[2]
+        return layout(constraints.maxWidth, constraints.maxHeight) {
             if (coordinates == null) {
                 return@layout
             }
-            val paneMeasurables = buildList {
-                arrangement.forEach { role ->
-                    when (role) {
-                        ThreePaneScaffoldRole.Primary -> {
-                            createPaneMeasurableIfNeeded(
-                                primaryMeasurables,
-                                ThreePaneScaffoldDefaults.PrimaryPanePriority,
-                                ThreePaneScaffoldDefaults.PrimaryPanePreferredWidth.roundToPx()
-                            )
-                        }
-                        ThreePaneScaffoldRole.Secondary -> {
-                            createPaneMeasurableIfNeeded(
-                                secondaryMeasurables,
-                                ThreePaneScaffoldDefaults.SecondaryPanePriority,
-                                ThreePaneScaffoldDefaults.SecondaryPanePreferredWidth.roundToPx()
-                            )
-                        }
-                        ThreePaneScaffoldRole.Tertiary -> {
-                            createPaneMeasurableIfNeeded(
-                                tertiaryMeasurables,
-                                ThreePaneScaffoldDefaults.TertiaryPanePriority,
-                                ThreePaneScaffoldDefaults.TertiaryPanePreferredWidth.roundToPx()
-                            )
-                        }
-                    }
-                }
+            val visiblePanes = getPanesMeasurables(
+                arrangement = arrangement,
+                primaryMeasurables = primaryMeasurables,
+                scaffoldValue = scaffoldValue,
+                secondaryMeasurables = secondaryMeasurables,
+                tertiaryMeasurables = tertiaryMeasurables
+            ) {
+                it != PaneAdaptedValue.Hidden
+            }
+
+            val hiddenPanes = getPanesMeasurables(
+                arrangement = arrangement,
+                primaryMeasurables = primaryMeasurables,
+                scaffoldValue = scaffoldValue,
+                secondaryMeasurables = secondaryMeasurables,
+                tertiaryMeasurables = tertiaryMeasurables
+            ) {
+                it == PaneAdaptedValue.Hidden
             }
 
             val outerVerticalGutterSize = layoutDirective.gutterSizes.outerVertical.roundToPx()
             val innerVerticalGutterSize = layoutDirective.gutterSizes.innerVertical.roundToPx()
-            val outerHorizontalGutterSize = layoutDirective.gutterSizes.outerHorizontal.roundToPx()
+            val outerHorizontalGutterSize =
+                layoutDirective.gutterSizes.outerHorizontal.roundToPx()
+            val outerBounds = IntRect(
+                outerVerticalGutterSize,
+                outerHorizontalGutterSize,
+                constraints.maxWidth - outerVerticalGutterSize,
+                constraints.maxHeight - outerHorizontalGutterSize
+            )
 
             if (layoutDirective.excludedBounds.isNotEmpty()) {
                 val layoutBounds = coordinates!!.boundsInWindow()
@@ -126,14 +393,15 @@
                         // the current partition to the actual displayable bounds.
                         actualLeft = max(actualLeft, hingeBound.right)
                     } else if (hingeBound.right >= actualRight) {
-                        // The hinge is right at the right of the layout and there's no more room
-                        // for more partitions, adjust the right edge of the current partition to
-                        // the actual displayable bounds.
+                        // The hinge is right at the right of the layout and there's no more
+                        // room for more partitions, adjust the right edge of the current
+                        // partition to the actual displayable bounds.
                         actualRight = min(hingeBound.left, actualRight)
                         return@fastForEach
                     } else {
-                        // The hinge is inside the layout, add the current partition to the list and
-                        // move the left edge of the next partition to the right of the hinge.
+                        // The hinge is inside the layout, add the current partition to the list
+                        // and move the left edge of the next partition to the right of the
+                        // hinge.
                         layoutPhysicalPartitions.add(
                             Rect(actualLeft, actualTop, hingeBound.left, actualBottom)
                         )
@@ -153,9 +421,10 @@
                     measureAndPlacePanes(
                         layoutPhysicalPartitions[0],
                         innerVerticalGutterSize,
-                        paneMeasurables
+                        visiblePanes,
+                        isLookingAhead
                     )
-                } else if (layoutPhysicalPartitions.size < paneMeasurables.size) {
+                } else if (layoutPhysicalPartitions.size < visiblePanes.size) {
                     // Note that the only possible situation is we have only two physical partitions
                     // but three expanded panes to show. In this case fit two panes in the larger
                     // partition.
@@ -163,119 +432,262 @@
                         measureAndPlacePanes(
                             layoutPhysicalPartitions[0],
                             innerVerticalGutterSize,
-                            paneMeasurables.subList(0, 2)
+                            visiblePanes.subList(0, 2),
+                            isLookingAhead
                         )
-                        measureAndPlacePane(layoutPhysicalPartitions[1], paneMeasurables[2])
+                        measureAndPlacePane(
+                            layoutPhysicalPartitions[1],
+                            visiblePanes[2],
+                            isLookingAhead
+                        )
                     } else {
-                        measureAndPlacePane(layoutPhysicalPartitions[0], paneMeasurables[0])
+                        measureAndPlacePane(
+                            layoutPhysicalPartitions[0],
+                            visiblePanes[0],
+                            isLookingAhead
+                        )
                         measureAndPlacePanes(
                             layoutPhysicalPartitions[1],
                             innerVerticalGutterSize,
-                            paneMeasurables.subList(1, 3)
+                            visiblePanes.subList(1, 3),
+                            isLookingAhead
                         )
                     }
                 } else {
-                    // Layout each pane in a physical partition
-                    paneMeasurables.fastForEachIndexed { index, paneMeasurable ->
-                        measureAndPlacePane(layoutPhysicalPartitions[index], paneMeasurable)
+                    // Layout each visible pane in a physical partition
+                    visiblePanes.fastForEachIndexed { index, paneMeasurable ->
+                        measureAndPlacePane(
+                            layoutPhysicalPartitions[index],
+                            paneMeasurable,
+                            isLookingAhead
+                        )
                     }
                 }
             } else {
                 measureAndPlacePanesWithLocalBounds(
-                    IntRect(
-                        outerVerticalGutterSize,
-                        outerHorizontalGutterSize,
-                        constraints.maxWidth - outerVerticalGutterSize,
-                        constraints.maxHeight - outerHorizontalGutterSize),
+                    outerBounds,
                     innerVerticalGutterSize,
-                    paneMeasurables
+                    visiblePanes,
+                    isLookingAhead
                 )
             }
+
+            // Place the hidden panes. Those should only exist when isLookingAhead = true.
+            // Placing these type of pane during the lookahead phase ensures a proper motion
+            // at the AnimatedVisibility.
+            // The placement is done using the outerBounds, as the placementsCache holds
+            // absolute position values.
+            placeHiddenPanes(
+                outerBounds.top,
+                outerBounds.height,
+                hiddenPanes
+            )
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3AdaptiveApi::class)
+    private fun MeasureScope.getPanesMeasurables(
+        arrangement: ThreePaneScaffoldArrangement,
+        primaryMeasurables: List<Measurable>,
+        scaffoldValue: ThreePaneScaffoldValue,
+        secondaryMeasurables: List<Measurable>,
+        tertiaryMeasurables: List<Measurable>,
+        predicate: (PaneAdaptedValue) -> Boolean
+    ): List<PaneMeasurable> {
+        return buildList {
+            arrangement.forEach { role ->
+                if (predicate(scaffoldValue[role])) {
+                    when (role) {
+                        ThreePaneScaffoldRole.Primary -> {
+                            createPaneMeasurableIfNeeded(
+                                primaryMeasurables,
+                                ThreePaneScaffoldDefaults.PrimaryPanePriority,
+                                role,
+                                ThreePaneScaffoldDefaults.PrimaryPanePreferredWidth
+                                    .roundToPx()
+                            )
+                        }
+
+                        ThreePaneScaffoldRole.Secondary -> {
+                            createPaneMeasurableIfNeeded(
+                                secondaryMeasurables,
+                                ThreePaneScaffoldDefaults.SecondaryPanePriority,
+                                role,
+                                ThreePaneScaffoldDefaults.SecondaryPanePreferredWidth
+                                    .roundToPx()
+                            )
+                        }
+
+                        ThreePaneScaffoldRole.Tertiary -> {
+                            createPaneMeasurableIfNeeded(
+                                tertiaryMeasurables,
+                                ThreePaneScaffoldDefaults.TertiaryPanePriority,
+                                role,
+                                ThreePaneScaffoldDefaults.TertiaryPanePreferredWidth
+                                    .roundToPx()
+                            )
+                        }
+                    }
+                }
+            }
         }
     }
+
+    @OptIn(ExperimentalMaterial3AdaptiveApi::class)
+    private fun MutableList<PaneMeasurable>.createPaneMeasurableIfNeeded(
+        measurables: List<Measurable>,
+        priority: Int,
+        role: ThreePaneScaffoldRole,
+        defaultPreferredWidth: Int
+    ) {
+        if (measurables.isNotEmpty()) {
+            add(PaneMeasurable(measurables[0], priority, role, defaultPreferredWidth))
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3AdaptiveApi::class)
+    private fun Placeable.PlacementScope.measureAndPlacePane(
+        partitionBounds: Rect,
+        measurable: PaneMeasurable,
+        isLookingAhead: Boolean
+    ) {
+        val localBounds = getLocalBounds(partitionBounds)
+        measurable.measuredWidth = localBounds.width
+        measurable.apply {
+            measure(Constraints.fixed(measuredWidth, localBounds.height))
+                .place(localBounds.left, localBounds.top)
+        }
+        if (isLookingAhead) {
+            // Cache the values to be used when this measurable role is being hidden.
+            // See placeHiddenPanes.
+            val cachedPanePlacement = placementsCache[measurable.role]!!
+            cachedPanePlacement.measuredWidth = measurable.measuredWidth
+            cachedPanePlacement.positionX = localBounds.left
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3AdaptiveApi::class)
+    private fun Placeable.PlacementScope.measureAndPlacePanes(
+        partitionBounds: Rect,
+        spacerSize: Int,
+        measurables: List<PaneMeasurable>,
+        isLookingAhead: Boolean
+    ) {
+        measureAndPlacePanesWithLocalBounds(
+            getLocalBounds(partitionBounds),
+            spacerSize,
+            measurables,
+            isLookingAhead
+        )
+    }
+
+    @OptIn(ExperimentalMaterial3AdaptiveApi::class)
+    private fun Placeable.PlacementScope.measureAndPlacePanesWithLocalBounds(
+        partitionBounds: IntRect,
+        spacerSize: Int,
+        measurables: List<PaneMeasurable>,
+        isLookingAhead: Boolean
+    ) {
+        if (measurables.isEmpty()) {
+            return
+        }
+        val allocatableWidth = partitionBounds.width - (measurables.size - 1) * spacerSize
+        val totalPreferredWidth = measurables.sumOf { it.measuredWidth }
+        if (allocatableWidth > totalPreferredWidth) {
+            // Allocate the remaining space to the pane with the highest priority.
+            measurables.maxBy {
+                it.priority
+            }.measuredWidth += allocatableWidth - totalPreferredWidth
+        } else if (allocatableWidth < totalPreferredWidth) {
+            // Scale down all panes to fit in the available space.
+            val scale = allocatableWidth.toFloat() / totalPreferredWidth
+            measurables.fastForEach {
+                it.measuredWidth = (it.measuredWidth * scale).toInt()
+            }
+        }
+        var positionX = partitionBounds.left
+        measurables.fastForEach {
+            it.measure(Constraints.fixed(it.measuredWidth, partitionBounds.height))
+                .place(positionX, partitionBounds.top)
+            if (isLookingAhead) {
+                // Cache the values to be used when this measurable's role is being hidden.
+                // See placeHiddenPanes.
+                val cachedPanePlacement = placementsCache[it.role]!!
+                cachedPanePlacement.measuredWidth = it.measuredWidth
+                cachedPanePlacement.positionX = positionX
+            }
+            positionX += it.measuredWidth + spacerSize
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3AdaptiveApi::class)
+    private fun Placeable.PlacementScope.placeHiddenPanes(
+        partitionTop: Int,
+        partitionHeight: Int,
+        measurables: List<PaneMeasurable>
+    ) {
+        // When panes are being hidden, apply each pane's width and position from the cache to
+        // maintain the those before it's hidden by the AnimatedVisibility.
+        measurables.fastForEach {
+            val cachedPanePlacement = placementsCache[it.role]!!
+            it.measure(
+                Constraints.fixed(
+                    width = cachedPanePlacement.measuredWidth,
+                    height = partitionHeight
+                )
+            ).place(cachedPanePlacement.positionX, partitionTop)
+        }
+    }
+
+    private fun Placeable.PlacementScope.getLocalBounds(bounds: Rect): IntRect {
+        return bounds.translate(coordinates!!.windowToLocal(Offset.Zero)).roundToIntRect()
+    }
 }
 
+/**
+ * A conditional [Modifier.clipToBounds] that will only clip when the given [adaptedValue] is
+ * [PaneAdaptedValue.Hidden].
+ */
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+private fun Modifier.clipToBounds(adaptedValue: PaneAdaptedValue): Modifier =
+    if (adaptedValue == PaneAdaptedValue.Hidden) this.clipToBounds() else this
+
 @ExperimentalMaterial3AdaptiveApi
 @Composable
 private fun PaneWrapper(
     adaptedValue: PaneAdaptedValue,
-    pane: (@Composable ThreePaneScaffoldScope.(PaneAdaptedValue) -> Unit)?
+    positionAnimationSpec: FiniteAnimationSpec<IntOffset>,
+    enterTransition: EnterTransition,
+    exitTransition: ExitTransition,
+    label: String,
+    content: (@Composable ThreePaneScaffoldScope.(PaneAdaptedValue) -> Unit)?,
 ) {
-    if (adaptedValue != PaneAdaptedValue.Hidden) {
-        pane?.invoke(ThreePaneScaffoldScopeImpl, adaptedValue)
-    }
-}
-
-private fun MutableList<PaneMeasurable>.createPaneMeasurableIfNeeded(
-    measurables: List<Measurable>,
-    priority: Int,
-    defaultPreferredWidth: Int
-) {
-    if (measurables.isNotEmpty()) {
-        add(PaneMeasurable(measurables[0], priority, defaultPreferredWidth))
-    }
-}
-private fun Placeable.PlacementScope.measureAndPlacePane(
-    partitionBounds: Rect,
-    measurable: PaneMeasurable
-) {
-    val localBounds = getLocalBounds(partitionBounds)
-    measurable.measuredWidth = localBounds.width
-    measurable.apply {
-        measure(Constraints.fixed(measuredWidth, localBounds.height))
-            .place(localBounds.left, localBounds.top)
-    }
-}
-
-private fun Placeable.PlacementScope.measureAndPlacePanes(
-    partitionBounds: Rect,
-    spacerSize: Int,
-    measurables: List<PaneMeasurable>
-) {
-    measureAndPlacePanesWithLocalBounds(
-        getLocalBounds(partitionBounds),
-        spacerSize,
-        measurables
-    )
-}
-
-private fun Placeable.PlacementScope.measureAndPlacePanesWithLocalBounds(
-    partitionBounds: IntRect,
-    spacerSize: Int,
-    measurables: List<PaneMeasurable>
-) {
-    if (measurables.isEmpty()) {
-        return
-    }
-    val allocatableWidth = partitionBounds.width - (measurables.size - 1) * spacerSize
-    val totalPreferredWidth = measurables.sumOf { it.measuredWidth }
-    if (allocatableWidth > totalPreferredWidth) {
-        // Allocate the remaining space to the pane with the highest priority.
-        measurables.maxBy {
-            it.priority
-        }.measuredWidth += allocatableWidth - totalPreferredWidth
-    } else if (allocatableWidth < totalPreferredWidth) {
-        // Scale down all panes to fit in the available space.
-        val scale = allocatableWidth.toFloat() / totalPreferredWidth
-        measurables.fastForEach {
-            it.measuredWidth = (it.measuredWidth * scale).toInt()
+    if (content != null) {
+        AnimatedVisibility(
+            visible = adaptedValue == PaneAdaptedValue.Expanded,
+            modifier = Modifier
+                .clipToBounds(adaptedValue)
+                .then(
+                    if (adaptedValue == PaneAdaptedValue.Expanded) {
+                        Modifier.animateBounds(positionAnimationSpec = positionAnimationSpec)
+                    } else {
+                        Modifier
+                    }
+                ),
+            enter = enterTransition,
+            exit = exitTransition,
+            label = "AnimatedVisibility: $label"
+        ) {
+            content.invoke(ThreePaneScaffoldScopeImpl, adaptedValue)
         }
     }
-    var positionX = partitionBounds.left
-    measurables.fastForEach {
-        it.measure(Constraints.fixed(it.measuredWidth, partitionBounds.height))
-            .place(positionX, partitionBounds.top)
-        positionX += it.measuredWidth + spacerSize
-    }
 }
 
-private fun Placeable.PlacementScope.getLocalBounds(bounds: Rect): IntRect {
-    return bounds.translate(coordinates!!.windowToLocal(Offset.Zero)).roundToIntRect()
-}
-
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
 private class PaneMeasurable(
     val measurable: Measurable,
     val priority: Int,
+    val role: ThreePaneScaffoldRole,
     defaultPreferredWidth: Int
 ) : Measurable by measurable {
     private val data = ((parentData as? PaneScaffoldParentData) ?: PaneScaffoldParentData())
@@ -306,6 +718,7 @@
      * Denotes [ThreePaneScaffold] to use the list-detail arrangement to arrange its panes, which
      * allocates panes in the order of secondary, primary, and tertiary form start to end.
      */
+    // TODO(conradchen/sgibly): Consider moving this to the ListDetailPaneScaffoldDefaults
     val ListDetailLayoutArrangement = ThreePaneScaffoldArrangement(
         ThreePaneScaffoldRole.Secondary,
         ThreePaneScaffoldRole.Primary,
@@ -327,6 +740,7 @@
      * [ThreePaneScaffoldScope.preferredWidth].
      */
     val SecondaryPanePreferredWidth = 412.dp
+
     /**
      * The default preferred width of [ThreePaneScaffoldRole.Tertiary]. See more details in
      * [ThreePaneScaffoldScope.preferredWidth].
@@ -358,4 +772,39 @@
             secondaryPaneAdaptStrategy,
             tertiaryPaneAdaptStrategy
         )
+
+    /**
+     * A default [SpringSpec] for the panes motion.
+     */
+    private val PaneSpringSpec: SpringSpec<IntOffset> =
+        spring(
+            dampingRatio = 0.7f,
+            stiffness = 600f,
+            visibilityThreshold = IntOffset.VisibilityThreshold
+        )
+
+    private val slideInFromLeft = slideInHorizontally(PaneSpringSpec) { -it }
+    private val slideInFromRight = slideInHorizontally(PaneSpringSpec) { it }
+    private val slideOutToLeft = slideOutHorizontally(PaneSpringSpec) { -it }
+    private val slideOutToRight = slideOutHorizontally(PaneSpringSpec) { it }
+
+    internal val panesLeftMotion = ThreePaneMotion(
+        PaneSpringSpec,
+        slideInFromLeft,
+        slideOutToRight,
+        slideInFromLeft,
+        slideOutToRight,
+        slideInFromLeft,
+        slideOutToRight
+    )
+
+    internal val panesRightMotion = ThreePaneMotion(
+        PaneSpringSpec,
+        slideInFromRight,
+        slideOutToLeft,
+        slideInFromRight,
+        slideOutToLeft,
+        slideInFromRight,
+        slideOutToLeft
+    )
 }
diff --git a/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldArrangement.kt b/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldArrangement.kt
index 51e8642..f472d0a 100644
--- a/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldArrangement.kt
+++ b/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldArrangement.kt
@@ -75,6 +75,17 @@
     action(2, thirdPane)
 }
 
+@ExperimentalMaterial3AdaptiveApi
+internal fun ThreePaneScaffoldArrangement.indexOf(role: ThreePaneScaffoldRole): Int {
+    forEachIndexed { i, r ->
+        if (r == role) {
+            return i
+        }
+    }
+    // should never reach this far
+    return 0
+}
+
 /**
  * The set of the available pane roles of [ThreePaneScaffold].
  */
@@ -86,12 +97,14 @@
      * details in a list-detail settings.
      */
     Primary,
+
     /**
      * The secondary pane of [ThreePaneScaffold]. It is supposed to have the second highest priority
      * during layout adaptation and usually contains the supplement content of the screen, like
      * content list in a list-detail settings.
      */
     Secondary,
+
     /**
      * The tertiary pane of [ThreePaneScaffold]. It is supposed to have the lowest priority during
      * layout adaptation and usually contains the additional info which will only be shown under
diff --git a/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldValue.kt b/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldValue.kt
index d7326ea..f1e1931 100644
--- a/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldValue.kt
+++ b/compose/material3/material3-adaptive/src/commonMain/kotlin/androidx/compose/material3/adaptive/ThreePaneScaffoldValue.kt
@@ -51,8 +51,7 @@
 @ExperimentalMaterial3AdaptiveApi
 fun calculateThreePaneScaffoldValue(
     maxHorizontalPartitions: Int,
-    adaptStrategies: ThreePaneScaffoldAdaptStrategies =
-        ThreePaneScaffoldDefaults.adaptStrategies(),
+    adaptStrategies: ThreePaneScaffoldAdaptStrategies = ThreePaneScaffoldDefaults.adaptStrategies(),
     currentFocus: ThreePaneScaffoldRole? = null,
 ): ThreePaneScaffoldValue {
     var expandedCount = if (currentFocus != null) 1 else 0
@@ -63,6 +62,7 @@
                 expandedCount++
                 PaneAdaptedValue.Expanded
             }
+
             else -> adaptStrategies[role].adapt()
         }
     }