Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +7 −0 Original line number Diff line number Diff line Loading @@ -81,6 +81,7 @@ internal fun Modifier.multiPointerDraggable( enabled: () -> Boolean, startDragImmediately: (startedPosition: Offset) -> Boolean, onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, onFirstPointerDown: () -> Unit = {}, swipeDetector: SwipeDetector = DefaultSwipeDetector, dispatcher: NestedScrollDispatcher, ): Modifier = Loading @@ -90,6 +91,7 @@ internal fun Modifier.multiPointerDraggable( enabled, startDragImmediately, onDragStarted, onFirstPointerDown, swipeDetector, dispatcher, ) Loading @@ -101,6 +103,7 @@ private data class MultiPointerDraggableElement( private val startDragImmediately: (startedPosition: Offset) -> Boolean, private val onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, private val onFirstPointerDown: () -> Unit, private val swipeDetector: SwipeDetector, private val dispatcher: NestedScrollDispatcher, ) : ModifierNodeElement<MultiPointerDraggableNode>() { Loading @@ -110,6 +113,7 @@ private data class MultiPointerDraggableElement( enabled = enabled, startDragImmediately = startDragImmediately, onDragStarted = onDragStarted, onFirstPointerDown = onFirstPointerDown, swipeDetector = swipeDetector, dispatcher = dispatcher, ) Loading @@ -119,6 +123,7 @@ private data class MultiPointerDraggableElement( node.enabled = enabled node.startDragImmediately = startDragImmediately node.onDragStarted = onDragStarted node.onFirstPointerDown = onFirstPointerDown node.swipeDetector = swipeDetector } } Loading @@ -129,6 +134,7 @@ internal class MultiPointerDraggableNode( var startDragImmediately: (startedPosition: Offset) -> Boolean, var onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, var onFirstPointerDown: () -> Unit, var swipeDetector: SwipeDetector = DefaultSwipeDetector, private val dispatcher: NestedScrollDispatcher, ) : Loading Loading @@ -225,6 +231,7 @@ internal class MultiPointerDraggableNode( startedPosition = null } else if (startedPosition == null) { startedPosition = pointers.first().position onFirstPointerDown() } } } Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt +10 −0 Original line number Diff line number Diff line Loading @@ -67,6 +67,7 @@ private class SwipeToSceneNode( enabled = ::enabled, startDragImmediately = ::startDragImmediately, onDragStarted = draggableHandler::onDragStarted, onFirstPointerDown = ::onFirstPointerDown, swipeDetector = swipeDetector, dispatcher = dispatcher, ) Loading Loading @@ -101,6 +102,15 @@ private class SwipeToSceneNode( delegate(ScrollBehaviorOwnerNode(draggableHandler.nestedScrollKey, nestedScrollHandlerImpl)) } private fun onFirstPointerDown() { // When we drag our finger across the screen, the NestedScrollConnection keeps track of all // the scroll events until we lift our finger. However, in some cases, the connection might // not receive the "up" event. This can lead to an incorrect initial state for the gesture. // To prevent this issue, we can call the reset() method when the first finger touches the // screen. This ensures that the NestedScrollConnection starts from a correct state. nestedScrollHandlerImpl.connection.reset() } override fun onDetach() { // Make sure we reset the scroll connection when this modifier is removed from composition nestedScrollHandlerImpl.connection.reset() Loading packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt +5 −1 Original line number Diff line number Diff line Loading @@ -129,7 +129,11 @@ class PriorityNestedScrollConnection( return onPriorityStop(available) } /** Method to call before destroying the object or to reset the initial state. */ /** * Method to call before destroying the object or to reset the initial state. * * TODO(b/303224944) This method should be removed. */ fun reset() { // Step 3c: To ensure that an onStop is always called for every onStart. onPriorityStop(velocity = Velocity.Zero) Loading packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt +37 −0 Original line number Diff line number Diff line Loading @@ -23,6 +23,9 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.platform.LocalViewConfiguration Loading Loading @@ -266,4 +269,38 @@ class NestedScrollToSceneTest { val transition = assertThat(state.transitionState).isTransition() assertThat(transition).hasProgress(0.5f) } @Test fun resetScrollTracking_afterMissingPointerUpEvent() { var canScroll = true var hasScrollable by mutableStateOf(true) val state = setup2ScenesAndScrollTouchSlop { if (hasScrollable) { Modifier.scrollable(rememberScrollableState { if (canScroll) it else 0f }, Vertical) } else { Modifier } } // The gesture is consumed by the component in the scene. scrollUp(percent = 0.2f) // STL keeps track of the scroll consumed. The scene remains in Idle. assertThat(state.transitionState).isIdle() // The scrollable component disappears, and does not send the signal (pointer up) to reset // the consumed amount. hasScrollable = false pointerUp() // A new scrollable component appears and allows the scene to consume the scroll. hasScrollable = true canScroll = false pointerDownAndScrollTouchSlop() scrollUp(percent = 0.2f) // STL can only start the transition if it has reset the amount of scroll consumed. val transition = assertThat(state.transitionState).isTransition() assertThat(transition).hasProgress(0.2f) } } Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +7 −0 Original line number Diff line number Diff line Loading @@ -81,6 +81,7 @@ internal fun Modifier.multiPointerDraggable( enabled: () -> Boolean, startDragImmediately: (startedPosition: Offset) -> Boolean, onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, onFirstPointerDown: () -> Unit = {}, swipeDetector: SwipeDetector = DefaultSwipeDetector, dispatcher: NestedScrollDispatcher, ): Modifier = Loading @@ -90,6 +91,7 @@ internal fun Modifier.multiPointerDraggable( enabled, startDragImmediately, onDragStarted, onFirstPointerDown, swipeDetector, dispatcher, ) Loading @@ -101,6 +103,7 @@ private data class MultiPointerDraggableElement( private val startDragImmediately: (startedPosition: Offset) -> Boolean, private val onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, private val onFirstPointerDown: () -> Unit, private val swipeDetector: SwipeDetector, private val dispatcher: NestedScrollDispatcher, ) : ModifierNodeElement<MultiPointerDraggableNode>() { Loading @@ -110,6 +113,7 @@ private data class MultiPointerDraggableElement( enabled = enabled, startDragImmediately = startDragImmediately, onDragStarted = onDragStarted, onFirstPointerDown = onFirstPointerDown, swipeDetector = swipeDetector, dispatcher = dispatcher, ) Loading @@ -119,6 +123,7 @@ private data class MultiPointerDraggableElement( node.enabled = enabled node.startDragImmediately = startDragImmediately node.onDragStarted = onDragStarted node.onFirstPointerDown = onFirstPointerDown node.swipeDetector = swipeDetector } } Loading @@ -129,6 +134,7 @@ internal class MultiPointerDraggableNode( var startDragImmediately: (startedPosition: Offset) -> Boolean, var onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, var onFirstPointerDown: () -> Unit, var swipeDetector: SwipeDetector = DefaultSwipeDetector, private val dispatcher: NestedScrollDispatcher, ) : Loading Loading @@ -225,6 +231,7 @@ internal class MultiPointerDraggableNode( startedPosition = null } else if (startedPosition == null) { startedPosition = pointers.first().position onFirstPointerDown() } } } Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt +10 −0 Original line number Diff line number Diff line Loading @@ -67,6 +67,7 @@ private class SwipeToSceneNode( enabled = ::enabled, startDragImmediately = ::startDragImmediately, onDragStarted = draggableHandler::onDragStarted, onFirstPointerDown = ::onFirstPointerDown, swipeDetector = swipeDetector, dispatcher = dispatcher, ) Loading Loading @@ -101,6 +102,15 @@ private class SwipeToSceneNode( delegate(ScrollBehaviorOwnerNode(draggableHandler.nestedScrollKey, nestedScrollHandlerImpl)) } private fun onFirstPointerDown() { // When we drag our finger across the screen, the NestedScrollConnection keeps track of all // the scroll events until we lift our finger. However, in some cases, the connection might // not receive the "up" event. This can lead to an incorrect initial state for the gesture. // To prevent this issue, we can call the reset() method when the first finger touches the // screen. This ensures that the NestedScrollConnection starts from a correct state. nestedScrollHandlerImpl.connection.reset() } override fun onDetach() { // Make sure we reset the scroll connection when this modifier is removed from composition nestedScrollHandlerImpl.connection.reset() Loading
packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt +5 −1 Original line number Diff line number Diff line Loading @@ -129,7 +129,11 @@ class PriorityNestedScrollConnection( return onPriorityStop(available) } /** Method to call before destroying the object or to reset the initial state. */ /** * Method to call before destroying the object or to reset the initial state. * * TODO(b/303224944) This method should be removed. */ fun reset() { // Step 3c: To ensure that an onStop is always called for every onStart. onPriorityStop(velocity = Velocity.Zero) Loading
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt +37 −0 Original line number Diff line number Diff line Loading @@ -23,6 +23,9 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.platform.LocalViewConfiguration Loading Loading @@ -266,4 +269,38 @@ class NestedScrollToSceneTest { val transition = assertThat(state.transitionState).isTransition() assertThat(transition).hasProgress(0.5f) } @Test fun resetScrollTracking_afterMissingPointerUpEvent() { var canScroll = true var hasScrollable by mutableStateOf(true) val state = setup2ScenesAndScrollTouchSlop { if (hasScrollable) { Modifier.scrollable(rememberScrollableState { if (canScroll) it else 0f }, Vertical) } else { Modifier } } // The gesture is consumed by the component in the scene. scrollUp(percent = 0.2f) // STL keeps track of the scroll consumed. The scene remains in Idle. assertThat(state.transitionState).isIdle() // The scrollable component disappears, and does not send the signal (pointer up) to reset // the consumed amount. hasScrollable = false pointerUp() // A new scrollable component appears and allows the scene to consume the scroll. hasScrollable = true canScroll = false pointerDownAndScrollTouchSlop() scrollUp(percent = 0.2f) // STL can only start the transition if it has reset the amount of scroll consumed. val transition = assertThat(state.transitionState).isTransition() assertThat(transition).hasProgress(0.2f) } }