summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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