diff options
4 files changed, 59 insertions, 1 deletions
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt index b329534e6e3a..3487730945da 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt @@ -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 = @@ -90,6 +91,7 @@ internal fun Modifier.multiPointerDraggable( enabled, startDragImmediately, onDragStarted, + onFirstPointerDown, swipeDetector, dispatcher, ) @@ -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>() { @@ -110,6 +113,7 @@ private data class MultiPointerDraggableElement( enabled = enabled, startDragImmediately = startDragImmediately, onDragStarted = onDragStarted, + onFirstPointerDown = onFirstPointerDown, swipeDetector = swipeDetector, dispatcher = dispatcher, ) @@ -119,6 +123,7 @@ private data class MultiPointerDraggableElement( node.enabled = enabled node.startDragImmediately = startDragImmediately node.onDragStarted = onDragStarted + node.onFirstPointerDown = onFirstPointerDown node.swipeDetector = swipeDetector } } @@ -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, ) : @@ -225,6 +231,7 @@ internal class MultiPointerDraggableNode( startedPosition = null } else if (startedPosition == null) { startedPosition = pointers.first().position + onFirstPointerDown() } } } 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 f06214645144..d1e83bacf40a 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 @@ -67,6 +67,7 @@ private class SwipeToSceneNode( enabled = ::enabled, startDragImmediately = ::startDragImmediately, onDragStarted = draggableHandler::onDragStarted, + onFirstPointerDown = ::onFirstPointerDown, swipeDetector = swipeDetector, dispatcher = dispatcher, ) @@ -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() diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt index 228f7ba48d3e..16fb533bbe06 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt @@ -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) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt index 9ebc42650d45..d8a06f54e74b 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt @@ -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 @@ -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) + } } |