Merge "NestedScrollToScene communicate desired behavior while scrolling" into main
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index 20b1303..cda2f83 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -64,6 +64,7 @@
     internal val orientation: Orientation,
     internal val coroutineScope: CoroutineScope,
 ) : DraggableHandler {
+    internal val nestedScrollKey = Any()
     /** The [DraggableHandler] can only have one active [DragController] at a time. */
     private var dragController: DragControllerImpl? = null
 
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
index ddff2f7..1e36f3d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
@@ -18,6 +18,9 @@
 
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
 import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode
 import androidx.compose.ui.node.DelegatableNode
 import androidx.compose.ui.node.DelegatingNode
@@ -122,13 +125,55 @@
 }
 
 private class NestedScrollToSceneNode(
-    layoutImpl: SceneTransitionLayoutImpl,
-    orientation: Orientation,
-    topOrLeftBehavior: NestedScrollBehavior,
-    bottomOrRightBehavior: NestedScrollBehavior,
-    isExternalOverscrollGesture: () -> Boolean,
+    private var layoutImpl: SceneTransitionLayoutImpl,
+    private var orientation: Orientation,
+    private var topOrLeftBehavior: NestedScrollBehavior,
+    private var bottomOrRightBehavior: NestedScrollBehavior,
+    private var isExternalOverscrollGesture: () -> Boolean,
 ) : DelegatingNode() {
     lateinit var pointersInfoOwner: PointersInfoOwner
+    private var scrollBehaviorOwner: ScrollBehaviorOwner? = null
+
+    private fun requireScrollBehaviorOwner(): ScrollBehaviorOwner {
+        var behaviorOwner = scrollBehaviorOwner
+        if (behaviorOwner == null) {
+            behaviorOwner = requireScrollBehaviorOwner(layoutImpl.draggableHandler(orientation))
+            scrollBehaviorOwner = behaviorOwner
+        }
+        return behaviorOwner
+    }
+
+    val updateScrollBehaviorsConnection =
+        object : NestedScrollConnection {
+            /**
+             * When using [NestedScrollConnection.onPostScroll], we can specify the desired behavior
+             * before our parent components. This gives them the option to override our behavior if
+             * they choose.
+             *
+             * The behavior can be communicated at every scroll gesture to ensure that the hierarchy
+             * is respected, even if one of our descendant nodes changes behavior after we set it.
+             */
+            override fun onPostScroll(
+                consumed: Offset,
+                available: Offset,
+                source: NestedScrollSource,
+            ): Offset {
+                // If we have some remaining scroll, that scroll can be used to initiate a
+                // transition between scenes. We can assume that the behavior is only needed if
+                // there is some remaining amount.
+                if (available != Offset.Zero) {
+                    requireScrollBehaviorOwner()
+                        .updateScrollBehaviors(
+                            topOrLeftBehavior = topOrLeftBehavior,
+                            bottomOrRightBehavior = bottomOrRightBehavior,
+                            isExternalOverscrollGesture = isExternalOverscrollGesture,
+                        )
+                }
+
+                return Offset.Zero
+            }
+        }
+
     private var priorityNestedScrollConnection: PriorityNestedScrollConnection =
         scenePriorityNestedScrollConnection(
             layoutImpl = layoutImpl,
@@ -145,14 +190,22 @@
             dispatcher = null,
         )
 
+    private var updateScrollBehaviorsNestedScrollNode: DelegatableNode =
+        nestedScrollModifierNode(
+            connection = updateScrollBehaviorsConnection,
+            dispatcher = null,
+        )
+
     override fun onAttach() {
         pointersInfoOwner = requireAncestorPointersInfoOwner()
         delegate(nestedScrollNode)
+        delegate(updateScrollBehaviorsNestedScrollNode)
     }
 
     override fun onDetach() {
         // Make sure we reset the scroll connection when this modifier is removed from composition
         priorityNestedScrollConnection.reset()
+        scrollBehaviorOwner = null
     }
 
     fun update(
@@ -162,6 +215,12 @@
         bottomOrRightBehavior: NestedScrollBehavior,
         isExternalOverscrollGesture: () -> Boolean,
     ) {
+        this.layoutImpl = layoutImpl
+        this.orientation = orientation
+        this.topOrLeftBehavior = topOrLeftBehavior
+        this.bottomOrRightBehavior = bottomOrRightBehavior
+        this.isExternalOverscrollGesture = isExternalOverscrollGesture
+
         // Clean up the old nested scroll connection
         priorityNestedScrollConnection.reset()
         undelegate(nestedScrollNode)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index aeb6262..faae01f 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -22,9 +22,12 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.node.DelegatableNode
 import androidx.compose.ui.node.DelegatingNode
 import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.node.PointerInputModifierNode
+import androidx.compose.ui.node.TraversableNode
+import androidx.compose.ui.node.findNearestAncestor
 import androidx.compose.ui.unit.IntSize
 
 /**
@@ -53,7 +56,7 @@
     draggableHandler: DraggableHandlerImpl,
     swipeDetector: SwipeDetector,
 ) : DelegatingNode(), PointerInputModifierNode {
-    private val delegate =
+    private val multiPointerDraggableNode =
         delegate(
             MultiPointerDraggableNode(
                 orientation = draggableHandler.orientation,
@@ -74,21 +77,25 @@
                 // Make sure to update the delegate orientation. Note that this will automatically
                 // reset the underlying pointer input handler, so previous gestures will be
                 // cancelled.
-                delegate.orientation = value.orientation
+                multiPointerDraggableNode.orientation = value.orientation
             }
         }
 
+    override fun onAttach() {
+        delegate(ScrollBehaviorOwnerNode(draggableHandler.nestedScrollKey))
+    }
+
     override fun onPointerEvent(
         pointerEvent: PointerEvent,
         pass: PointerEventPass,
         bounds: IntSize,
-    ) = delegate.onPointerEvent(pointerEvent, pass, bounds)
+    ) = multiPointerDraggableNode.onPointerEvent(pointerEvent, pass, bounds)
 
-    override fun onCancelPointerInput() = delegate.onCancelPointerInput()
+    override fun onCancelPointerInput() = multiPointerDraggableNode.onCancelPointerInput()
 
     private fun enabled(): Boolean {
         return draggableHandler.isDrivingTransition ||
-            currentScene().shouldEnableSwipes(delegate.orientation)
+            currentScene().shouldEnableSwipes(multiPointerDraggableNode.orientation)
     }
 
     private fun currentScene(): Scene {
@@ -118,3 +125,39 @@
         return currentScene().shouldEnableSwipes(oppositeOrientation)
     }
 }
+
+/** Find the [ScrollBehaviorOwner] for the current orientation. */
+internal fun DelegatableNode.requireScrollBehaviorOwner(
+    draggableHandler: DraggableHandlerImpl
+): ScrollBehaviorOwner {
+    val ancestorNode =
+        checkNotNull(findNearestAncestor(draggableHandler.nestedScrollKey)) {
+            "This should never happen! Couldn't find a ScrollBehaviorOwner. " +
+                "Are we inside an SceneTransitionLayout?"
+        }
+    return ancestorNode as ScrollBehaviorOwner
+}
+
+internal fun interface ScrollBehaviorOwner {
+    fun updateScrollBehaviors(
+        topOrLeftBehavior: NestedScrollBehavior,
+        bottomOrRightBehavior: NestedScrollBehavior,
+        isExternalOverscrollGesture: () -> Boolean,
+    )
+}
+
+/**
+ * We need a node that receives the desired behavior.
+ *
+ * TODO(b/353234530) move this logic into [SwipeToSceneNode]
+ */
+private class ScrollBehaviorOwnerNode(override val traverseKey: Any) :
+    Modifier.Node(), TraversableNode, ScrollBehaviorOwner {
+    override fun updateScrollBehaviors(
+        topOrLeftBehavior: NestedScrollBehavior,
+        bottomOrRightBehavior: NestedScrollBehavior,
+        isExternalOverscrollGesture: () -> Boolean
+    ) {
+        // This method will be used to update the desired behavior in a following CL.
+    }
+}