summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author omarmt <omarmt@google.com> 2024-06-13 13:50:55 +0000
committer omarmt <omarmt@google.com> 2024-06-18 10:01:41 +0000
commit9a42e828b01f90d84c5a99daf89bd71b955621f6 (patch)
tree0a42555c8b88f1139892e3e3c1c531e74836dd6d
parent6d28c9007ed4efecefc36629e0cd5435fdf3ab5c (diff)
Expose startedPosition and pointersDown to NestedScrollHandler
Until now, we have handled scrolls as gestures without a specific starting position (using null) and have only considered one finger on the screen. Moving forward, we aim to accurately expose this information and we can achieve this by leveraging the PointerInput API. The NestedScrollToSceneNode now employs a PointerInputHandler to determine the number of pointers and obtain the starting position during the nested scroll. The PointerInputHandler awaits the creation of the AwaitPointerEventScope to register the nestedScrollHandler. This approach ensures that touch events are always received by the PointerInputHandler first, in the Initial step, and later these events are consumed by our descendant scrollable in the Main step. Test: atest ElementTest Bug: 330200163 Flag: com.android.systemui.scene_container Change-Id: Ib912aec5af40145af7a0d580aa81b3098d0ad092
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt52
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt64
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt3
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt73
4 files changed, 156 insertions, 36 deletions
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 e9633c2f6603..f551207485f6 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
@@ -889,6 +889,7 @@ internal class NestedScrollHandlerImpl(
private val topOrLeftBehavior: NestedScrollBehavior,
private val bottomOrRightBehavior: NestedScrollBehavior,
private val isExternalOverscrollGesture: () -> Boolean,
+ private val pointersInfo: () -> PointersInfo,
) {
private val layoutState = layoutImpl.state
private val draggableHandler = layoutImpl.draggableHandler(orientation)
@@ -900,34 +901,36 @@ internal class NestedScrollHandlerImpl(
// moving on to the next scene.
var canChangeScene = false
- val actionUpOrLeft =
- Swipe(
- direction =
- when (orientation) {
- Orientation.Horizontal -> SwipeDirection.Left
- Orientation.Vertical -> SwipeDirection.Up
- },
- pointerCount = 1,
- )
-
- val actionDownOrRight =
- Swipe(
- direction =
- when (orientation) {
- Orientation.Horizontal -> SwipeDirection.Right
- Orientation.Vertical -> SwipeDirection.Down
- },
- pointerCount = 1,
- )
-
fun hasNextScene(amount: Float): Boolean {
val transitionState = layoutState.transitionState
val scene = transitionState.currentScene
val fromScene = layoutImpl.scene(scene)
val nextScene =
when {
- amount < 0f -> fromScene.userActions[actionUpOrLeft]
- amount > 0f -> fromScene.userActions[actionDownOrRight]
+ amount < 0f -> {
+ val actionUpOrLeft =
+ Swipe(
+ direction =
+ when (orientation) {
+ Orientation.Horizontal -> SwipeDirection.Left
+ Orientation.Vertical -> SwipeDirection.Up
+ },
+ pointerCount = pointersInfo().pointersDown,
+ )
+ fromScene.userActions[actionUpOrLeft]
+ }
+ amount > 0f -> {
+ val actionDownOrRight =
+ Swipe(
+ direction =
+ when (orientation) {
+ Orientation.Horizontal -> SwipeDirection.Right
+ Orientation.Vertical -> SwipeDirection.Down
+ },
+ pointerCount = pointersInfo().pointersDown,
+ )
+ fromScene.userActions[actionDownOrRight]
+ }
else -> null
}
if (nextScene != null) return true
@@ -1025,10 +1028,11 @@ internal class NestedScrollHandlerImpl(
canContinueScroll = { true },
canScrollOnFling = false,
onStart = { offsetAvailable ->
+ val pointers = pointersInfo()
dragController =
draggableHandler.onDragStarted(
- pointersDown = 1,
- startedPosition = null,
+ pointersDown = pointers.pointersDown,
+ startedPosition = pointers.startedPosition,
overSlop = if (isIntercepting) 0f else offsetAvailable,
)
},
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 1fa6b3f7d6c0..dd795cd9ddfe 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,12 +18,21 @@ package com.android.compose.animation.scene
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
import androidx.compose.ui.node.DelegatableNode
import androidx.compose.ui.node.DelegatingNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.util.fastMap
+import androidx.compose.ui.util.fastReduce
import com.android.compose.nestedscroll.PriorityNestedScrollConnection
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.isActive
/**
* Defines the behavior of the [SceneTransitionLayout] when a scrollable component is scrolled.
@@ -121,6 +130,11 @@ private data class NestedScrollToSceneElement(
}
}
+internal data class PointersInfo(
+ val pointersDown: Int,
+ val startedPosition: Offset,
+)
+
private class NestedScrollToSceneNode(
layoutImpl: SceneTransitionLayoutImpl,
orientation: Orientation,
@@ -135,7 +149,42 @@ private class NestedScrollToSceneNode(
topOrLeftBehavior = topOrLeftBehavior,
bottomOrRightBehavior = bottomOrRightBehavior,
isExternalOverscrollGesture = isExternalOverscrollGesture,
+ pointersInfo = pointerInfo()
+ )
+
+ private var lastPointers: List<PointerInputChange>? = null
+
+ private fun pointerInfo(): () -> PointersInfo = {
+ val pointers =
+ requireNotNull(lastPointers) { "NestedScroll API was called before PointerInput API" }
+ PointersInfo(
+ pointersDown = pointers.size,
+ startedPosition = pointers.fastMap { it.position }.fastReduce { a, b -> (a + b) / 2f },
)
+ }
+
+ private val pointerInputHandler: suspend PointerInputScope.() -> Unit = {
+ coroutineScope {
+ awaitPointerEventScope {
+ // Await this scope to guarantee that the PointerInput API receives touch events
+ // before the NestedScroll API.
+ delegate(nestedScrollNode)
+
+ try {
+ while (isActive) {
+ // During the initial phase, we receive the event after our ancestors.
+ lastPointers = awaitPointerEvent(PointerEventPass.Initial).changes
+ }
+ } finally {
+ // Clean up the nested scroll connection
+ priorityNestedScrollConnection.reset()
+ undelegate(nestedScrollNode)
+ }
+ }
+ }
+ }
+
+ private val pointerInputNode = delegate(SuspendingPointerInputModifierNode(pointerInputHandler))
private var nestedScrollNode: DelegatableNode =
nestedScrollModifierNode(
@@ -143,15 +192,6 @@ private class NestedScrollToSceneNode(
dispatcher = null,
)
- override fun onAttach() {
- delegate(nestedScrollNode)
- }
-
- override fun onDetach() {
- // Make sure we reset the scroll connection when this modifier is removed from composition
- priorityNestedScrollConnection.reset()
- }
-
fun update(
layoutImpl: SceneTransitionLayoutImpl,
orientation: Orientation,
@@ -161,7 +201,7 @@ private class NestedScrollToSceneNode(
) {
// Clean up the old nested scroll connection
priorityNestedScrollConnection.reset()
- undelegate(nestedScrollNode)
+ pointerInputNode.resetPointerInputHandler()
// Create a new nested scroll connection
priorityNestedScrollConnection =
@@ -171,13 +211,13 @@ private class NestedScrollToSceneNode(
topOrLeftBehavior = topOrLeftBehavior,
bottomOrRightBehavior = bottomOrRightBehavior,
isExternalOverscrollGesture = isExternalOverscrollGesture,
+ pointersInfo = pointerInfo(),
)
nestedScrollNode =
nestedScrollModifierNode(
connection = priorityNestedScrollConnection,
dispatcher = null,
)
- delegate(nestedScrollNode)
}
}
@@ -187,6 +227,7 @@ private fun scenePriorityNestedScrollConnection(
topOrLeftBehavior: NestedScrollBehavior,
bottomOrRightBehavior: NestedScrollBehavior,
isExternalOverscrollGesture: () -> Boolean,
+ pointersInfo: () -> PointersInfo,
) =
NestedScrollHandlerImpl(
layoutImpl = layoutImpl,
@@ -194,5 +235,6 @@ private fun scenePriorityNestedScrollConnection(
topOrLeftBehavior = topOrLeftBehavior,
bottomOrRightBehavior = bottomOrRightBehavior,
isExternalOverscrollGesture = isExternalOverscrollGesture,
+ pointersInfo = pointersInfo,
)
.connection
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index 8625482d5f71..7f3b7a1fae1a 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -111,7 +111,8 @@ class DraggableHandlerTest {
orientation = draggableHandler.orientation,
topOrLeftBehavior = nestedScrollBehavior,
bottomOrRightBehavior = nestedScrollBehavior,
- isExternalOverscrollGesture = { isExternalOverscrollGesture }
+ isExternalOverscrollGesture = { isExternalOverscrollGesture },
+ pointersInfo = { PointersInfo(pointersDown = 1, startedPosition = Offset.Zero) }
)
.connection
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index beb74bc9bb36..772a4dac4540 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -837,6 +837,79 @@ class ElementTest {
}
@Test
+ fun elementTransitionDuringNestedScrollWith2Pointers() {
+ // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
+ // detected as a drag event.
+ var touchSlop = 0f
+ val translateY = 10.dp
+ val layoutWidth = 200.dp
+ val layoutHeight = 400.dp
+
+ val state =
+ rule.runOnUiThread {
+ MutableSceneTransitionLayoutState(
+ initialScene = SceneA,
+ transitions = transitions {
+ from(SceneA, to = SceneB) {
+ translate(TestElements.Foo, y = translateY)
+ }
+ },
+ )
+ as MutableSceneTransitionLayoutStateImpl
+ }
+
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ SceneTransitionLayout(
+ state = state,
+ modifier = Modifier.size(layoutWidth, layoutHeight)
+ ) {
+ scene(
+ SceneA,
+ userActions = mapOf(Swipe(SwipeDirection.Down, pointerCount = 2) to SceneB)
+ ) {
+ Box(
+ Modifier
+ // Unconsumed scroll gesture will be intercepted by STL
+ .verticalNestedScrollToScene()
+ // A scrollable that does not consume the scroll gesture
+ .scrollable(
+ rememberScrollableState(consumeScrollDelta = { 0f }),
+ Orientation.Vertical
+ )
+ .fillMaxSize()
+ ) {
+ Spacer(Modifier.element(TestElements.Foo).fillMaxSize())
+ }
+ }
+ scene(SceneB) { Spacer(Modifier.fillMaxSize()) }
+ }
+ }
+
+ assertThat(state.transitionState).isIdle()
+ val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag)
+ fooElement.assertTopPositionInRootIsEqualTo(0.dp)
+
+ // Swipe down with 2 pointers by half of verticalSwipeDistance.
+ rule.onRoot().performTouchInput {
+ val middleTop = Offset((layoutWidth / 2).toPx(), 0f)
+ repeat(2) { i -> down(pointerId = i, middleTop) }
+ repeat(2) { i ->
+ // Scroll 50%
+ moveBy(
+ pointerId = i,
+ delta = Offset(0f, touchSlop + layoutHeight.toPx() * 0.5f),
+ delayMillis = 1_000,
+ )
+ }
+ }
+
+ val transition = assertThat(state.transitionState).isTransition()
+ assertThat(transition).hasProgress(0.5f)
+ fooElement.assertTopPositionInRootIsEqualTo(translateY * 0.5f)
+ }
+
+ @Test
fun elementTransitionWithDistanceDuringOverscroll() {
val layoutWidth = 200.dp
val layoutHeight = 400.dp