Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +24 −28 Original line number Diff line number Diff line Loading @@ -913,7 +913,6 @@ 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) Loading @@ -925,13 +924,6 @@ internal class NestedScrollHandlerImpl( // moving on to the next scene. var canChangeScene = false fun hasNextScene(amount: Float): Boolean { val transitionState = layoutState.transitionState val scene = transitionState.currentScene val fromScene = layoutImpl.scene(scene) val nextScene = when { amount < 0f -> { val actionUpOrLeft = Swipe( direction = Loading @@ -939,11 +931,9 @@ internal class NestedScrollHandlerImpl( Orientation.Horizontal -> SwipeDirection.Left Orientation.Vertical -> SwipeDirection.Up }, pointerCount = pointersInfo().pointersDown, pointerCount = 1, ) fromScene.userActions[actionUpOrLeft] } amount > 0f -> { val actionDownOrRight = Swipe( direction = Loading @@ -951,10 +941,17 @@ internal class NestedScrollHandlerImpl( Orientation.Horizontal -> SwipeDirection.Right Orientation.Vertical -> SwipeDirection.Down }, pointerCount = pointersInfo().pointersDown, pointerCount = 1, ) fromScene.userActions[actionDownOrRight] } 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] else -> null } if (nextScene != null) return true Loading Loading @@ -1052,11 +1049,10 @@ internal class NestedScrollHandlerImpl( canContinueScroll = { true }, canScrollOnFling = false, onStart = { offsetAvailable -> val pointers = pointersInfo() dragController = draggableHandler.onDragStarted( pointersDown = pointers.pointersDown, startedPosition = pointers.startedPosition, pointersDown = 1, startedPosition = null, overSlop = if (isIntercepting) 0f else offsetAvailable, ) }, Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt +11 −53 Original line number Diff line number Diff line Loading @@ -18,21 +18,12 @@ 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. Loading Loading @@ -130,11 +121,6 @@ private data class NestedScrollToSceneElement( } } internal data class PointersInfo( val pointersDown: Int, val startedPosition: Offset, ) private class NestedScrollToSceneNode( layoutImpl: SceneTransitionLayoutImpl, orientation: Orientation, Loading @@ -149,48 +135,22 @@ 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 var nestedScrollNode: DelegatableNode = nestedScrollModifierNode( connection = priorityNestedScrollConnection, dispatcher = null, ) } private val pointerInputHandler: suspend PointerInputScope.() -> Unit = { coroutineScope { awaitPointerEventScope { // Await this scope to guarantee that the PointerInput API receives touch events // before the NestedScroll API. override fun onAttach() { 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 override fun onDetach() { // Make sure we reset the scroll connection when this modifier is removed from composition priorityNestedScrollConnection.reset() undelegate(nestedScrollNode) } } } } private val pointerInputNode = delegate(SuspendingPointerInputModifierNode(pointerInputHandler)) private var nestedScrollNode: DelegatableNode = nestedScrollModifierNode( connection = priorityNestedScrollConnection, dispatcher = null, ) fun update( layoutImpl: SceneTransitionLayoutImpl, Loading @@ -201,7 +161,7 @@ private class NestedScrollToSceneNode( ) { // Clean up the old nested scroll connection priorityNestedScrollConnection.reset() pointerInputNode.resetPointerInputHandler() undelegate(nestedScrollNode) // Create a new nested scroll connection priorityNestedScrollConnection = Loading @@ -211,13 +171,13 @@ private class NestedScrollToSceneNode( topOrLeftBehavior = topOrLeftBehavior, bottomOrRightBehavior = bottomOrRightBehavior, isExternalOverscrollGesture = isExternalOverscrollGesture, pointersInfo = pointerInfo(), ) nestedScrollNode = nestedScrollModifierNode( connection = priorityNestedScrollConnection, dispatcher = null, ) delegate(nestedScrollNode) } } Loading @@ -227,7 +187,6 @@ private fun scenePriorityNestedScrollConnection( topOrLeftBehavior: NestedScrollBehavior, bottomOrRightBehavior: NestedScrollBehavior, isExternalOverscrollGesture: () -> Boolean, pointersInfo: () -> PointersInfo, ) = NestedScrollHandlerImpl( layoutImpl = layoutImpl, Loading @@ -235,6 +194,5 @@ private fun scenePriorityNestedScrollConnection( topOrLeftBehavior = topOrLeftBehavior, bottomOrRightBehavior = bottomOrRightBehavior, isExternalOverscrollGesture = isExternalOverscrollGesture, pointersInfo = pointersInfo, ) .connection packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +1 −2 Original line number Diff line number Diff line Loading @@ -113,8 +113,7 @@ class DraggableHandlerTest { orientation = draggableHandler.orientation, topOrLeftBehavior = nestedScrollBehavior, bottomOrRightBehavior = nestedScrollBehavior, isExternalOverscrollGesture = { isExternalOverscrollGesture }, pointersInfo = { PointersInfo(pointersDown = 1, startedPosition = Offset.Zero) } isExternalOverscrollGesture = { isExternalOverscrollGesture } ) .connection Loading packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +0 −74 Original line number Diff line number Diff line Loading @@ -838,80 +838,6 @@ class ElementTest { fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 1.5f) } @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 Loading Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +24 −28 Original line number Diff line number Diff line Loading @@ -913,7 +913,6 @@ 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) Loading @@ -925,13 +924,6 @@ internal class NestedScrollHandlerImpl( // moving on to the next scene. var canChangeScene = false fun hasNextScene(amount: Float): Boolean { val transitionState = layoutState.transitionState val scene = transitionState.currentScene val fromScene = layoutImpl.scene(scene) val nextScene = when { amount < 0f -> { val actionUpOrLeft = Swipe( direction = Loading @@ -939,11 +931,9 @@ internal class NestedScrollHandlerImpl( Orientation.Horizontal -> SwipeDirection.Left Orientation.Vertical -> SwipeDirection.Up }, pointerCount = pointersInfo().pointersDown, pointerCount = 1, ) fromScene.userActions[actionUpOrLeft] } amount > 0f -> { val actionDownOrRight = Swipe( direction = Loading @@ -951,10 +941,17 @@ internal class NestedScrollHandlerImpl( Orientation.Horizontal -> SwipeDirection.Right Orientation.Vertical -> SwipeDirection.Down }, pointerCount = pointersInfo().pointersDown, pointerCount = 1, ) fromScene.userActions[actionDownOrRight] } 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] else -> null } if (nextScene != null) return true Loading Loading @@ -1052,11 +1049,10 @@ internal class NestedScrollHandlerImpl( canContinueScroll = { true }, canScrollOnFling = false, onStart = { offsetAvailable -> val pointers = pointersInfo() dragController = draggableHandler.onDragStarted( pointersDown = pointers.pointersDown, startedPosition = pointers.startedPosition, pointersDown = 1, startedPosition = null, overSlop = if (isIntercepting) 0f else offsetAvailable, ) }, Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt +11 −53 Original line number Diff line number Diff line Loading @@ -18,21 +18,12 @@ 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. Loading Loading @@ -130,11 +121,6 @@ private data class NestedScrollToSceneElement( } } internal data class PointersInfo( val pointersDown: Int, val startedPosition: Offset, ) private class NestedScrollToSceneNode( layoutImpl: SceneTransitionLayoutImpl, orientation: Orientation, Loading @@ -149,48 +135,22 @@ 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 var nestedScrollNode: DelegatableNode = nestedScrollModifierNode( connection = priorityNestedScrollConnection, dispatcher = null, ) } private val pointerInputHandler: suspend PointerInputScope.() -> Unit = { coroutineScope { awaitPointerEventScope { // Await this scope to guarantee that the PointerInput API receives touch events // before the NestedScroll API. override fun onAttach() { 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 override fun onDetach() { // Make sure we reset the scroll connection when this modifier is removed from composition priorityNestedScrollConnection.reset() undelegate(nestedScrollNode) } } } } private val pointerInputNode = delegate(SuspendingPointerInputModifierNode(pointerInputHandler)) private var nestedScrollNode: DelegatableNode = nestedScrollModifierNode( connection = priorityNestedScrollConnection, dispatcher = null, ) fun update( layoutImpl: SceneTransitionLayoutImpl, Loading @@ -201,7 +161,7 @@ private class NestedScrollToSceneNode( ) { // Clean up the old nested scroll connection priorityNestedScrollConnection.reset() pointerInputNode.resetPointerInputHandler() undelegate(nestedScrollNode) // Create a new nested scroll connection priorityNestedScrollConnection = Loading @@ -211,13 +171,13 @@ private class NestedScrollToSceneNode( topOrLeftBehavior = topOrLeftBehavior, bottomOrRightBehavior = bottomOrRightBehavior, isExternalOverscrollGesture = isExternalOverscrollGesture, pointersInfo = pointerInfo(), ) nestedScrollNode = nestedScrollModifierNode( connection = priorityNestedScrollConnection, dispatcher = null, ) delegate(nestedScrollNode) } } Loading @@ -227,7 +187,6 @@ private fun scenePriorityNestedScrollConnection( topOrLeftBehavior: NestedScrollBehavior, bottomOrRightBehavior: NestedScrollBehavior, isExternalOverscrollGesture: () -> Boolean, pointersInfo: () -> PointersInfo, ) = NestedScrollHandlerImpl( layoutImpl = layoutImpl, Loading @@ -235,6 +194,5 @@ private fun scenePriorityNestedScrollConnection( topOrLeftBehavior = topOrLeftBehavior, bottomOrRightBehavior = bottomOrRightBehavior, isExternalOverscrollGesture = isExternalOverscrollGesture, pointersInfo = pointersInfo, ) .connection
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +1 −2 Original line number Diff line number Diff line Loading @@ -113,8 +113,7 @@ class DraggableHandlerTest { orientation = draggableHandler.orientation, topOrLeftBehavior = nestedScrollBehavior, bottomOrRightBehavior = nestedScrollBehavior, isExternalOverscrollGesture = { isExternalOverscrollGesture }, pointersInfo = { PointersInfo(pointersDown = 1, startedPosition = Offset.Zero) } isExternalOverscrollGesture = { isExternalOverscrollGesture } ) .connection Loading
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +0 −74 Original line number Diff line number Diff line Loading @@ -838,80 +838,6 @@ class ElementTest { fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 1.5f) } @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 Loading