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.
+ }
+}