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

Commit 9a42e828 authored by omarmt's avatar omarmt
Browse files

Expose startedPosition and pointersDown to NestedScrollHandler

Until now, we have handled scrolls as gestures without a specific
starting position (using null) and have only considered one finger on
the screen.

Moving forward, we aim to accurately expose this information and we can
achieve this by leveraging the PointerInput API.
The NestedScrollToSceneNode now employs a PointerInputHandler to
determine the number of pointers and obtain the starting position during
 the nested scroll.
The PointerInputHandler awaits the creation of the
AwaitPointerEventScope to register the nestedScrollHandler.
This approach ensures that touch events are always received by the
PointerInputHandler first, in the Initial step, and later these events
are consumed by our descendant scrollable in the Main step.

Test: atest ElementTest
Bug: 330200163
Flag: com.android.systemui.scene_container
Change-Id: Ib912aec5af40145af7a0d580aa81b3098d0ad092
parent 6d28c900
Loading
Loading
Loading
Loading
+28 −24
Original line number Diff line number Diff line
@@ -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,6 +901,13 @@ 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 =
@@ -907,9 +915,11 @@ internal class NestedScrollHandlerImpl(
                                        Orientation.Horizontal -> SwipeDirection.Left
                                        Orientation.Vertical -> SwipeDirection.Up
                                    },
                pointerCount = 1,
                                pointerCount = pointersInfo().pointersDown,
                            )

                        fromScene.userActions[actionUpOrLeft]
                    }
                    amount > 0f -> {
                        val actionDownOrRight =
                            Swipe(
                                direction =
@@ -917,17 +927,10 @@ internal class NestedScrollHandlerImpl(
                                        Orientation.Horizontal -> SwipeDirection.Right
                                        Orientation.Vertical -> SwipeDirection.Down
                                    },
                pointerCount = 1,
                                pointerCount = pointersInfo().pointersDown,
                            )

        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]
                        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,
                    )
            },
+53 −11
Original line number Diff line number Diff line
@@ -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,22 +149,48 @@ private class NestedScrollToSceneNode(
            topOrLeftBehavior = topOrLeftBehavior,
            bottomOrRightBehavior = bottomOrRightBehavior,
            isExternalOverscrollGesture = isExternalOverscrollGesture,
            pointersInfo = pointerInfo()
        )

    private var nestedScrollNode: DelegatableNode =
        nestedScrollModifierNode(
            connection = priorityNestedScrollConnection,
            dispatcher = null,
    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 },
        )
    }

    override fun onAttach() {
    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)
    }

    override fun onDetach() {
        // Make sure we reset the scroll connection when this modifier is removed from composition
                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(
            connection = priorityNestedScrollConnection,
            dispatcher = null,
        )

    fun update(
        layoutImpl: SceneTransitionLayoutImpl,
@@ -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
+2 −1
Original line number Diff line number Diff line
@@ -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

+73 −0
Original line number Diff line number Diff line
@@ -836,6 +836,79 @@ 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