Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9e701690 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "STL: Reset PriorityNestedScrollConnection on first pointer down" into main

parents 1a16f030 47f03223
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -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()
                        }
                    }
                }
+10 −0
Original line number Diff line number Diff line
@@ -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()
+5 −1
Original line number Diff line number Diff line
@@ -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)
+37 −0
Original line number Diff line number Diff line
@@ -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)
    }
}