Add NotificationShade and QuickSettingsShade to the STL demo

This CL adds overlays to the STL demo, disabled by default by a config
flag.

See b/353679003#comment35 for videos.

Bug: 353679003
Test: Manual in the demo app
Flag: com.android.systemui.scene_container
Change-Id: Ic88b448689160d37924d62a21fe6472ee132aed3
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/DemoConfiguration.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/DemoConfiguration.kt
index aaa9cb6..dbc1ecb 100644
--- a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/DemoConfiguration.kt
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/DemoConfiguration.kt
@@ -67,13 +67,14 @@
     val interactiveNotifications: Boolean = false,
     val showMediaPlayer: Boolean = true,
     val isFullscreen: Boolean = false,
-    val canChangeScene: Boolean = true,
+    val canChangeSceneOrOverlays: Boolean = true,
     val transitionInterceptionThreshold: Float = 0.05f,
     val springConfigurations: DemoSpringConfigurations = DemoSpringConfigurations.presets[1],
     val useOverscrollSpec: Boolean = true,
     val overscrollProgressConverter: DemoOverscrollProgress = Tanh(maxProgress = 0.2f, tilt = 3f),
     val enableInterruptions: Boolean = true,
     val lsToShadeRequiresFullSwipe: ToggleableState = ToggleableState.Indeterminate,
+    val enableOverlays: Boolean = false,
 ) {
     companion object {
         val Saver = run {
@@ -82,7 +83,7 @@
             val interactiveNotificationsKey = "interactiveNotifications"
             val showMediaPlayerKey = "showMediaPlayer"
             val isFullscreenKey = "isFullscreen"
-            val canChangeSceneKey = "canChangeScene"
+            val canChangeSceneOrOverlaysKey = "canChangeSceneOrOverlays"
             val transitionInterceptionThresholdKey = "transitionInterceptionThreshold"
             val springConfigurationsKey = "springConfigurations"
             val useOverscrollSpec = "useOverscrollSpec"
@@ -98,7 +99,7 @@
                         interactiveNotificationsKey to it.interactiveNotifications,
                         showMediaPlayerKey to it.showMediaPlayer,
                         isFullscreenKey to it.isFullscreen,
-                        canChangeSceneKey to it.canChangeScene,
+                        canChangeSceneOrOverlaysKey to it.canChangeSceneOrOverlays,
                         transitionInterceptionThresholdKey to it.transitionInterceptionThreshold,
                         springConfigurationsKey to it.springConfigurations.save(),
                         useOverscrollSpec to it.useOverscrollSpec,
@@ -114,7 +115,7 @@
                         interactiveNotifications = it[interactiveNotificationsKey] as Boolean,
                         showMediaPlayer = it[showMediaPlayerKey] as Boolean,
                         isFullscreen = it[isFullscreenKey] as Boolean,
-                        canChangeScene = it[canChangeSceneKey] as Boolean,
+                        canChangeSceneOrOverlays = it[canChangeSceneOrOverlaysKey] as Boolean,
                         transitionInterceptionThreshold =
                             it[transitionInterceptionThresholdKey] as Float,
                         springConfigurations =
@@ -350,11 +351,24 @@
 
                 // Can change scene.
                 Checkbox(
-                    label = "Can change scene",
-                    checked = configuration.canChangeScene,
+                    label = "Can change scene or overlays",
+                    checked = configuration.canChangeSceneOrOverlays,
                     onCheckedChange = {
                         onConfigurationChange(
-                            configuration.copy(canChangeScene = !configuration.canChangeScene)
+                            configuration.copy(
+                                canChangeSceneOrOverlays = !configuration.canChangeSceneOrOverlays
+                            )
+                        )
+                    },
+                )
+
+                // Overlays.
+                Checkbox(
+                    label = "Overlays",
+                    checked = configuration.enableOverlays,
+                    onCheckedChange = {
+                        onConfigurationChange(
+                            configuration.copy(enableOverlays = !configuration.enableOverlays)
                         )
                     },
                 )
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/HorizontalHalfScreen.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/HorizontalHalfScreen.kt
new file mode 100644
index 0000000..9fe8df2
--- /dev/null
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/HorizontalHalfScreen.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 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 com.android.compose.animation.scene.demo
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import com.android.compose.animation.scene.SwipeSource
+import com.android.compose.animation.scene.SwipeSourceDetector
+
+/** A [SwipeSource] for the left or right half of the device. */
+enum class HorizontalHalfScreen(private val onResolve: (LayoutDirection) -> Resolved) :
+    SwipeSource {
+    Left(onResolve = { Resolved.Left }),
+    Right(onResolve = { Resolved.Right }),
+    Start(onResolve = { if (it == LayoutDirection.Ltr) Resolved.Left else Resolved.Right }),
+    End(onResolve = { if (it == LayoutDirection.Ltr) Resolved.Right else Resolved.Left });
+
+    override fun resolve(layoutDirection: LayoutDirection): SwipeSource.Resolved {
+        return onResolve(layoutDirection)
+    }
+
+    enum class Resolved : SwipeSource.Resolved {
+        Left,
+        Right,
+    }
+}
+
+object HorizontalHalfScreenDetector : SwipeSourceDetector {
+    override fun source(
+        layoutSize: IntSize,
+        position: IntOffset,
+        density: Density,
+        orientation: Orientation,
+    ): SwipeSource.Resolved {
+        return if (position.x < layoutSize.width / 2) {
+            HorizontalHalfScreen.Resolved.Left
+        } else {
+            HorizontalHalfScreen.Resolved.Right
+        }
+    }
+}
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Launcher.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Launcher.kt
index c2b79a0..e2cc555 100644
--- a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Launcher.kt
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Launcher.kt
@@ -38,11 +38,26 @@
 import com.android.compose.grid.VerticalGrid
 
 object Launcher {
-    fun userActions(shadeScene: SceneKey): Map<UserAction, UserActionResult> {
-        return mapOf(
-            Swipe.Down to shadeScene,
-            Swipe(SwipeDirection.Down, pointerCount = 2) to Scenes.QuickSettings,
-        )
+    fun userActions(
+        shadeScene: SceneKey,
+        configuration: DemoConfiguration,
+    ): Map<UserAction, UserActionResult> {
+        return buildList {
+                if (configuration.enableOverlays) {
+                    add(
+                        Swipe(SwipeDirection.Down, fromSource = HorizontalHalfScreen.Start) to
+                            UserActionResult.ShowOverlay(Overlays.QuickSettings)
+                    )
+                    add(
+                        Swipe(SwipeDirection.Down, fromSource = HorizontalHalfScreen.End) to
+                            UserActionResult.ShowOverlay(Overlays.Notifications)
+                    )
+                } else {
+                    add(Swipe.Down to shadeScene)
+                    add(Swipe(SwipeDirection.Down, pointerCount = 2) to Scenes.QuickSettings)
+                }
+            }
+            .toMap()
     }
 
     object Elements {
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Lockscreen.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Lockscreen.kt
index d50bd10..77fbf8c 100644
--- a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Lockscreen.kt
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Lockscreen.kt
@@ -48,16 +48,35 @@
         isLockscreenDismissable: Boolean,
         shadeScene: SceneKey,
         requiresFullDistanceSwipeToShade: Boolean,
+        configuration: DemoConfiguration,
         fastSwipeToQuickSettings: Boolean = true,
     ): Map<UserAction, UserActionResult> {
         return buildList {
-                add(
-                    Swipe.Down to
-                        UserActionResult(
-                            shadeScene,
-                            requiresFullDistanceSwipe = requiresFullDistanceSwipeToShade,
-                        )
-                )
+                if (configuration.enableOverlays) {
+                    add(
+                        Swipe(SwipeDirection.Down, fromSource = HorizontalHalfScreen.Start) to
+                            UserActionResult.ShowOverlay(
+                                Overlays.QuickSettings,
+                                requiresFullDistanceSwipe = requiresFullDistanceSwipeToShade,
+                            )
+                    )
+                    add(
+                        Swipe(SwipeDirection.Down, fromSource = HorizontalHalfScreen.End) to
+                            UserActionResult.ShowOverlay(
+                                Overlays.Notifications,
+                                requiresFullDistanceSwipe = requiresFullDistanceSwipeToShade,
+                            )
+                    )
+                } else {
+                    add(
+                        Swipe.Down to
+                            UserActionResult(
+                                shadeScene,
+                                requiresFullDistanceSwipe = requiresFullDistanceSwipeToShade,
+                            )
+                    )
+                }
+
                 add(Swipe.Start to Scenes.StubEnd)
                 add(Swipe.End to Scenes.StubStart)
                 add(
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/MediaPlayer.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/MediaPlayer.kt
index 4044bd7..98c62d0 100644
--- a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/MediaPlayer.kt
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/MediaPlayer.kt
@@ -79,6 +79,7 @@
                 Scenes.Shade,
                 Scenes.SplitShade,
                 Scenes.QuickSettings,
+                Overlays.QuickSettings,
             )
 
         override fun contentDuringTransition(
@@ -120,6 +121,7 @@
                     Scenes.SplitShade
                 transition.isTransitioningBetween(Scenes.Lockscreen, Scenes.QuickSettings) ->
                     Scenes.QuickSettings
+                transition.isTransitioningFromOrTo(Overlays.QuickSettings) -> Overlays.QuickSettings
                 else -> pickSingleContentIn(contents, transition, element)
             }
         }
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/NotificationShade.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/NotificationShade.kt
new file mode 100644
index 0000000..cf0b6ae
--- /dev/null
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/NotificationShade.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 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 com.android.compose.animation.scene.demo
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.android.compose.animation.scene.Back
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.UserActionResult
+
+object NotificationShade {
+    object Elements {
+        val Root = ElementKey("NotificationShadeRoot")
+        val Content = ElementKey("NotificationShadeContent")
+    }
+
+    val UserActions =
+        mapOf(
+            Back to UserActionResult.HideOverlay(Overlays.Notifications),
+            Swipe.Up to UserActionResult.HideOverlay(Overlays.Notifications),
+            Swipe.Left to UserActionResult.ReplaceByOverlay(Overlays.QuickSettings),
+        )
+}
+
+@Composable
+fun SceneScope.NotificationShade(
+    notificationList: @Composable SceneScope.() -> Unit,
+    modifier: Modifier = Modifier,
+) {
+    PartialShade(
+        modifier.element(NotificationShade.Elements.Root),
+
+        // The notification list already applies some padding.
+        innerPadding = PaddingValues(0.dp),
+    ) {
+        Column(Modifier.element(NotificationShade.Elements.Content)) { notificationList() }
+    }
+}
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/PartialShade.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/PartialShade.kt
new file mode 100644
index 0000000..a05248f
--- /dev/null
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/PartialShade.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 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 com.android.compose.animation.scene.demo
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.LowestZIndexContentPicker
+import com.android.compose.animation.scene.SceneScope
+
+object PartialShade {
+    object Elements {
+        val Background =
+            ElementKey("PartialShadeBackground", contentPicker = LowestZIndexContentPicker)
+    }
+
+    object Colors {
+        val Background
+            @Composable get() = Shade.Colors.Scrim
+    }
+
+    object Shapes {
+        val Background = RoundedCornerShape(Shade.Dimensions.ScrimCornerSize)
+    }
+}
+
+@Composable
+fun SceneScope.PartialShade(
+    modifier: Modifier = Modifier,
+    outerPadding: PaddingValues = PaddingValues(16.dp),
+    innerPadding: PaddingValues = PaddingValues(16.dp),
+    content: @Composable BoxScope.() -> Unit,
+) {
+    Box(modifier.fillMaxWidth(0.5f).fillMaxHeight().padding(outerPadding)) {
+        Box(
+            Modifier.element(PartialShade.Elements.Background)
+                .matchParentSize()
+                .background(PartialShade.Colors.Background, PartialShade.Shapes.Background)
+        )
+
+        Box(Modifier.padding(innerPadding), content = content)
+    }
+}
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/QuickSettingsShade.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/QuickSettingsShade.kt
new file mode 100644
index 0000000..e222bac
--- /dev/null
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/QuickSettingsShade.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 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 com.android.compose.animation.scene.demo
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.android.compose.animation.scene.Back
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.UserActionResult
+
+object QuickSettingsShade {
+    object Elements {
+        val Root = ElementKey("QuickSettingsShadeContentRoot")
+        val Content = ElementKey("QuickSettingsShadeContent")
+    }
+
+    val UserActions =
+        mapOf(
+            Back to UserActionResult.HideOverlay(Overlays.QuickSettings),
+            Swipe.Up to UserActionResult.HideOverlay(Overlays.QuickSettings),
+            Swipe.Right to UserActionResult.ReplaceByOverlay(Overlays.Notifications),
+        )
+}
+
+@Composable
+fun SceneScope.QuickSettingsShade(
+    mediaPlayer: @Composable (SceneScope.() -> Unit)?,
+    modifier: Modifier = Modifier,
+) {
+    PartialShade(modifier.element(QuickSettingsShade.Elements.Root)) {
+        Column(Modifier.element(QuickSettingsShade.Elements.Content)) {
+            Clock(MaterialTheme.colorScheme.onSurfaceVariant)
+            if (mediaPlayer != null) {
+                mediaPlayer()
+            }
+        }
+    }
+}
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Shade.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Shade.kt
index 4745500..8e12f36 100644
--- a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Shade.kt
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Shade.kt
@@ -390,7 +390,7 @@
 
 @Composable
 private fun SceneScope.Scrim(
-    notificationList: @Composable (SceneScope.() -> Unit),
+    notificationList: @Composable SceneScope.() -> Unit,
     shouldPunchHoleBehindScrim: Boolean,
     scrimMinTopPadding: Dp,
     modifier: Modifier = Modifier,
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/SplitLockscreen.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/SplitLockscreen.kt
index 6084293..a819e67 100644
--- a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/SplitLockscreen.kt
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/SplitLockscreen.kt
@@ -31,18 +31,23 @@
 import com.android.compose.animation.scene.SceneScope
 
 object SplitLockscreen {
-    fun userActions(isLockscreenDismissable: Boolean, shadeScene: SceneKey) =
+    fun userActions(
+        isLockscreenDismissable: Boolean,
+        shadeScene: SceneKey,
+        configuration: DemoConfiguration,
+    ) =
         Lockscreen.userActions(
             isLockscreenDismissable,
             shadeScene,
             requiresFullDistanceSwipeToShade = false,
+            configuration,
             fastSwipeToQuickSettings = false,
         )
 }
 
 @Composable
 fun SceneScope.SplitLockscreen(
-    notificationList: @Composable (SceneScope.() -> Unit),
+    notificationList: @Composable SceneScope.() -> Unit,
     mediaPlayer: @Composable (SceneScope.() -> Unit)?,
     isDismissable: Boolean,
     onToggleDismissable: () -> Unit,
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/SplitShade.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/SplitShade.kt
index 11e3f35..3f0504c 100644
--- a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/SplitShade.kt
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/SplitShade.kt
@@ -67,7 +67,7 @@
 
 @Composable
 fun SceneScope.SplitShade(
-    notificationList: @Composable (SceneScope.() -> Unit),
+    notificationList: @Composable SceneScope.() -> Unit,
     mediaPlayer: @Composable (SceneScope.() -> Unit)?,
     quickSettingsTiles: List<QuickSettingsTileViewModel>,
     nQuickSettingsRows: Int,
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/SystemUi.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/SystemUi.kt
index 2c26b48..0dcab7d 100644
--- a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/SystemUi.kt
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/SystemUi.kt
@@ -67,6 +67,7 @@
 import androidx.compose.runtime.saveable.SaverScope
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.toComposeRect
@@ -83,8 +84,10 @@
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.dp
 import androidx.window.layout.WindowMetricsCalculator
+import com.android.compose.animation.scene.DefaultEdgeDetector
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
+import com.android.compose.animation.scene.OverlayKey
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.animation.scene.SceneTransitionLayout
@@ -156,6 +159,11 @@
     }
 }
 
+object Overlays {
+    val Notifications = OverlayKey("NotificationsOverlay")
+    val QuickSettings = OverlayKey("QuickSettingsOverlay")
+}
+
 /** A [Saver] that restores a [MutableSceneTransitionLayoutState] to its previous [currentScene]. */
 class MutableSceneTransitionLayoutSaver(
     private val sceneSaver: Scenes.SceneSaver,
@@ -285,7 +293,7 @@
     val canChangeScene =
         remember(configuration) {
             { scene: SceneKey ->
-                if (configuration.canChangeScene) {
+                if (configuration.canChangeSceneOrOverlays) {
                     maybeUpdateLockscreenDismissed(scene)
                     true
                 } else {
@@ -304,7 +312,13 @@
             )
         }
     val layoutState =
-        rememberSaveable(transitions, canChangeScene, enableInterruptions, saver = stateSaver) {
+        rememberSaveable(
+            transitions,
+            canChangeScene,
+            enableInterruptions,
+            configuration,
+            saver = stateSaver,
+        ) {
             val initialScene =
                 initialScene?.let {
                     Scenes.ensureCorrectScene(
@@ -318,6 +332,9 @@
                 initialScene,
                 transitions,
                 canChangeScene = canChangeScene,
+                canShowOverlay = { configuration.canChangeSceneOrOverlays },
+                canHideOverlay = { configuration.canChangeSceneOrOverlays },
+                canReplaceOverlay = { _, _ -> configuration.canChangeSceneOrOverlays },
                 enableInterruptions = enableInterruptions,
             )
         }
@@ -385,6 +402,21 @@
                         .forEach { (scene, name) ->
                             Button(onClick = { onChangeScene(scene) }) { Text(name) }
                         }
+
+                    listOf(Overlays.Notifications to "NS", Overlays.QuickSettings to "QSS")
+                        .forEach { (overlay, name) ->
+                            Button(
+                                onClick = {
+                                    if (layoutState.currentOverlays.contains(overlay)) {
+                                        layoutState.hideOverlay(overlay, coroutineScope)
+                                    } else {
+                                        layoutState.showOverlay(overlay, coroutineScope)
+                                    }
+                                }
+                            ) {
+                                Text(name)
+                            }
+                        }
                 }
             }
         }
@@ -430,8 +462,11 @@
                             // Make this layout accessible to UiAutomator.
                             Modifier.semantics { testTagsAsResourceId = true }
                                 .testTag("SystemUiSceneTransitionLayout"),
+                        swipeSourceDetector =
+                            if (configuration.enableOverlays) HorizontalHalfScreenDetector
+                            else DefaultEdgeDetector,
                     ) {
-                        scene(Scenes.Launcher, Launcher.userActions(shadeScene)) {
+                        scene(Scenes.Launcher, Launcher.userActions(shadeScene, configuration)) {
                             Launcher(launcherColumns)
                         }
                         scene(
@@ -446,6 +481,7 @@
                                         ToggleableState.Indeterminate ->
                                             configuration.interactiveNotifications
                                     },
+                                configuration,
                             ),
                         ) {
                             Lockscreen(
@@ -465,7 +501,11 @@
                         }
                         scene(
                             Scenes.SplitLockscreen,
-                            SplitLockscreen.userActions(isLockscreenDismissable, shadeScene),
+                            SplitLockscreen.userActions(
+                                isLockscreenDismissable,
+                                shadeScene,
+                                configuration,
+                            ),
                         ) {
                             SplitLockscreen(
                                 notificationList = {
@@ -554,9 +594,31 @@
                                 ::onPowerButtonClicked,
                             )
                         }
+
                         scene(Scenes.AlwaysOnDisplay) {
                             AlwaysOnDisplay(Modifier.clickable { onChangeScene(lockscreenScene) })
                         }
+
+                        overlay(
+                            Overlays.Notifications,
+                            userActions = NotificationShade.UserActions,
+                            alignment = Alignment.TopEnd,
+                        ) {
+                            NotificationShade(
+                                notificationList = {
+                                    NotificationList(
+                                        maxNotificationCount = configuration.notificationsInShade
+                                    )
+                                }
+                            )
+                        }
+                        overlay(
+                            Overlays.QuickSettings,
+                            userActions = QuickSettingsShade.UserActions,
+                            alignment = Alignment.TopStart,
+                        ) {
+                            QuickSettingsShade(mediaPlayer)
+                        }
                     }
                 }
             }
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/notification/DemoNotifications.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/notification/DemoNotifications.kt
index 0b6a00c..7999782 100644
--- a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/notification/DemoNotifications.kt
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/notification/DemoNotifications.kt
@@ -26,6 +26,7 @@
 import com.android.compose.animation.scene.SceneTransitions
 import com.android.compose.animation.scene.StaticElementContentPicker
 import com.android.compose.animation.scene.content.state.TransitionState
+import com.android.compose.animation.scene.demo.Overlays
 import com.android.compose.animation.scene.demo.Scenes
 import com.android.compose.animation.scene.demo.SpringConfiguration
 import com.android.compose.animation.scene.demo.transitions.ToShadeScrimFadeEndFraction
@@ -71,7 +72,13 @@
 
 private object NotificationContentPicker : StaticElementContentPicker {
     override val contents =
-        setOf(Scenes.Lockscreen, Scenes.Shade, Scenes.SplitLockscreen, Scenes.SplitShade)
+        setOf(
+            Scenes.Lockscreen,
+            Scenes.Shade,
+            Scenes.SplitLockscreen,
+            Scenes.SplitShade,
+            Overlays.Notifications,
+        )
 
     override fun contentDuringTransition(
         element: ElementKey,
@@ -100,6 +107,7 @@
                     Scenes.SplitLockscreen
                 }
             }
+            transition.isTransitioningFromOrTo(Overlays.Notifications) -> Overlays.Notifications
             else -> pickSingleContentIn(contents, transition, element)
         }
     }
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/transitions/NotificationShadeTransitions.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/transitions/NotificationShadeTransitions.kt
new file mode 100644
index 0000000..e5c37aa
--- /dev/null
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/transitions/NotificationShadeTransitions.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 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 com.android.compose.animation.scene.demo.transitions
+
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.gestures.Orientation
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.SceneTransitionsBuilder
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.compose.animation.scene.demo.NotificationShade
+import com.android.compose.animation.scene.demo.Overlays
+import com.android.compose.animation.scene.demo.notification.NotificationList
+
+fun SceneTransitionsBuilder.notificationShadeTransitions() {
+    to(Overlays.Notifications) {
+        spec = tween(500)
+        toNotificationShade()
+    }
+
+    from(Overlays.Notifications) {
+        spec = tween(500)
+        reversed { toNotificationShade() }
+        sharedElement(NotificationList.Elements.Notifications, enabled = false)
+    }
+
+    overscroll(Overlays.Notifications, Orientation.Vertical) {
+        translate(NotificationShade.Elements.Root, y = { absoluteDistance })
+    }
+
+    overscroll(Overlays.Notifications, Orientation.Horizontal) {
+        translate(NotificationShade.Elements.Root, x = { absoluteDistance })
+    }
+}
+
+private fun TransitionBuilder.toNotificationShade() {
+    translate(NotificationShade.Elements.Root, Edge.Top)
+    fractionRange(start = 0.5f) { fade(NotificationList.Elements.Notifications) }
+}
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/transitions/QuickSettingsShadeTransitions.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/transitions/QuickSettingsShadeTransitions.kt
new file mode 100644
index 0000000..ee83dd8
--- /dev/null
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/transitions/QuickSettingsShadeTransitions.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 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 com.android.compose.animation.scene.demo.transitions
+
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.gestures.Orientation
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.SceneTransitionsBuilder
+import com.android.compose.animation.scene.demo.Clock
+import com.android.compose.animation.scene.demo.MediaPlayer
+import com.android.compose.animation.scene.demo.NotificationShade
+import com.android.compose.animation.scene.demo.Overlays
+import com.android.compose.animation.scene.demo.PartialShade
+import com.android.compose.animation.scene.demo.QuickSettingsShade
+import com.android.compose.animation.scene.demo.notification.NotificationList
+
+fun SceneTransitionsBuilder.quickSettingsShadeTransitions() {
+    to(Overlays.QuickSettings) {
+        spec = tween(500)
+        translate(QuickSettingsShade.Elements.Root, Edge.Top)
+    }
+
+    from(Overlays.QuickSettings, to = Overlays.Notifications) {
+        spec = tween(500)
+
+        anchoredTranslate(NotificationShade.Elements.Content, PartialShade.Elements.Background)
+        anchoredTranslate(QuickSettingsShade.Elements.Content, PartialShade.Elements.Background)
+
+        fractionRange(end = 0.5f) {
+            fade(MediaPlayer.Elements.MediaPlayer)
+            fade(Clock.Elements.Clock)
+        }
+
+        fractionRange(start = 0.5f) { fade(NotificationList.Elements.Notifications) }
+    }
+
+    overscroll(Overlays.QuickSettings, Orientation.Vertical) {
+        translate(QuickSettingsShade.Elements.Root, y = { absoluteDistance })
+    }
+
+    overscroll(Overlays.QuickSettings, Orientation.Horizontal) {
+        translate(QuickSettingsShade.Elements.Root, x = { absoluteDistance })
+    }
+}
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/transitions/SystemUiTransitions.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/transitions/SystemUiTransitions.kt
index ceb932a..89dfc41 100644
--- a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/transitions/SystemUiTransitions.kt
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/transitions/SystemUiTransitions.kt
@@ -48,6 +48,8 @@
     lockscreenTransitions(configuration)
     bouncerTransitions(configuration)
     launcherTransitions()
+    notificationShadeTransitions()
+    quickSettingsShadeTransitions()
 }
 
 object DemoInterruptionHandler : InterruptionHandler {