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

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

Merge changes If9f58c37,Idf43855e into main

* changes:
  multiPointerDraggable shouldn't start drag gesture with consumed events
  Expose startedPosition and pointersDown to STL descendants, if available
parents 18357123 f7b4a5b8
Loading
Loading
Loading
Loading
+40 −23
Original line number Diff line number Diff line
@@ -913,6 +913,7 @@ internal class NestedScrollHandlerImpl(
    private val topOrLeftBehavior: NestedScrollBehavior,
    private val bottomOrRightBehavior: NestedScrollBehavior,
    private val isExternalOverscrollGesture: () -> Boolean,
    private val pointersInfoOwner: PointersInfoOwner,
) {
    private val layoutState = layoutImpl.state
    private val draggableHandler = layoutImpl.draggableHandler(orientation)
@@ -924,6 +925,20 @@ internal class NestedScrollHandlerImpl(
        // moving on to the next scene.
        var canChangeScene = false

        var _lastPointersInfo: PointersInfo? = null
        fun pointersInfo(): PointersInfo {
            return checkNotNull(_lastPointersInfo) {
                "PointersInfo should be initialized before the transition begins."
            }
        }

        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 =
@@ -931,9 +946,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 =
@@ -941,17 +958,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
@@ -985,6 +995,8 @@ internal class NestedScrollHandlerImpl(
                    return@PriorityNestedScrollConnection false
                }

                _lastPointersInfo = pointersInfoOwner.pointersInfo()

                // If the current swipe transition is *not* closed to 0f or 1f, then we want the
                // scroll events to intercept the current transition to continue the scene
                // transition.
@@ -1002,6 +1014,8 @@ internal class NestedScrollHandlerImpl(
                val isZeroOffset =
                    if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f

                _lastPointersInfo = pointersInfoOwner.pointersInfo()

                val canStart =
                    when (behavior) {
                        NestedScrollBehavior.DuringTransitionBetweenScenes -> {
@@ -1039,6 +1053,8 @@ internal class NestedScrollHandlerImpl(
                // We could start an overscroll animation
                canChangeScene = false

                _lastPointersInfo = pointersInfoOwner.pointersInfo()

                val canStart = behavior.canStartOnPostFling && hasNextScene(velocityAvailable)
                if (canStart) {
                    isIntercepting = false
@@ -1049,10 +1065,11 @@ internal class NestedScrollHandlerImpl(
            canContinueScroll = { true },
            canScrollOnFling = false,
            onStart = { offsetAvailable ->
                val pointersInfo = pointersInfo()
                dragController =
                    draggableHandler.onDragStarted(
                        pointersDown = 1,
                        startedPosition = null,
                        pointersDown = pointersInfo.pointersDown,
                        startedPosition = pointersInfo.startedPosition,
                        overSlop = if (isIntercepting) 0f else offsetAvailable,
                    )
            },
+77 −17
Original line number Diff line number Diff line
@@ -29,18 +29,21 @@ import androidx.compose.ui.input.pointer.PointerId
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.input.pointer.changedToDownIgnoreConsumed
import androidx.compose.ui.input.pointer.changedToDown
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.input.pointer.util.addPointerInputChange
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
import androidx.compose.ui.node.DelegatableNode
import androidx.compose.ui.node.DelegatingNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.ObserverModifierNode
import androidx.compose.ui.node.PointerInputModifierNode
import androidx.compose.ui.node.TraversableNode
import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.node.findNearestAncestor
import androidx.compose.ui.node.observeReads
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.unit.IntSize
@@ -48,11 +51,12 @@ import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastFirstOrNull
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastSumBy
import kotlin.coroutines.cancellation.CancellationException
import kotlin.math.sign
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch

/**
 * Make an element draggable in the given [orientation].
@@ -112,6 +116,18 @@ private data class MultiPointerDraggableElement(
    }
}

private val TRAVERSE_KEY = Any()

/** Find the nearest [PointersInfoOwner] ancestor or throw. */
internal fun DelegatableNode.requireAncestorPointersInfoOwner(): PointersInfoOwner {
    val ancestorNode =
        checkNotNull(findNearestAncestor(TRAVERSE_KEY)) {
            "This should never happen! Couldn't find a MultiPointerDraggableNode. " +
                "Are we inside an SceneTransitionLayout?"
        }
    return ancestorNode as PointersInfoOwner
}

internal class MultiPointerDraggableNode(
    orientation: Orientation,
    enabled: () -> Boolean,
@@ -120,15 +136,19 @@ internal class MultiPointerDraggableNode(
        (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController,
    var swipeDetector: SwipeDetector = DefaultSwipeDetector,
) :
    PointerInputModifierNode,
    DelegatingNode(),
    PointerInputModifierNode,
    CompositionLocalConsumerModifierNode,
    TraversableNode,
    PointersInfoOwner,
    ObserverModifierNode {
    private val pointerInputHandler: suspend PointerInputScope.() -> Unit = { pointerInput() }
    private val delegate = delegate(SuspendingPointerInputModifierNode(pointerInputHandler))
    private val velocityTracker = VelocityTracker()
    private var previousEnabled: Boolean = false

    override val traverseKey: Any = TRAVERSE_KEY

    var enabled: () -> Boolean = enabled
        set(value) {
            // Reset the pointer input whenever enabled changed.
@@ -185,12 +205,42 @@ internal class MultiPointerDraggableNode(
        bounds: IntSize
    ) = delegate.onPointerEvent(pointerEvent, pass, bounds)

    private var startedPosition: Offset? = null
    private var pointersDown: Int = 0

    override fun pointersInfo(): PointersInfo {
        return PointersInfo(
            startedPosition = startedPosition,
            // Note: We could have 0 pointers during fling or for other reasons.
            pointersDown = pointersDown.coerceAtLeast(1),
        )
    }

    private suspend fun PointerInputScope.pointerInput() {
        if (!enabled()) {
            return
        }

        coroutineScope {
            launch {
                // Intercepts pointer inputs and exposes [PointersInfo], via
                // [requireAncestorPointersInfoOwner], to our descendants.
                awaitPointerEventScope {
                    while (isActive) {
                        // During the Initial pass, we receive the event after our ancestors.
                        val pointers = awaitPointerEvent(PointerEventPass.Initial).changes

                        pointersDown = pointers.countDown()
                        if (pointersDown == 0) {
                            // There are no more pointers down
                            startedPosition = null
                        } else if (startedPosition == null) {
                            startedPosition = pointers.first().position
                        }
                    }
                }
            }

            awaitPointerEventScope {
                while (isActive) {
                    try {
@@ -314,15 +364,16 @@ internal class MultiPointerDraggableNode(
            }

        if (drag != null) {
            // Count the number of pressed pointers.
            val pressed = mutableSetOf<PointerId>()
            currentEvent.changes.fastForEach { change ->
                if (change.pressed) {
                    pressed.add(change.id)
                }
            }

            val controller = onDragStart(drag.position, overSlop, pressed.size)
            val controller =
                onDragStart(
                    // The startedPosition is the starting position when a gesture begins (when the
                    // first pointer touches the screen), not the point where we begin dragging.
                    // For example, this could be different if one of our children intercepts the
                    // gesture first and then we do.
                    requireNotNull(startedPosition),
                    overSlop,
                    pointersDown,
                )

            val successful: Boolean
            try {
@@ -364,12 +415,10 @@ internal class MultiPointerDraggableNode(
        fun canBeConsumed(changes: List<PointerInputChange>): Boolean {
            // At least one pointer down AND
            return changes.fastAny { it.pressed } &&
                // All pointers must be:
                // All pointers must be either:
                changes.fastAll {
                    // A) recently pressed: even if the event has already been consumed, we can
                    // still use the recently added finger event to determine whether to initiate
                    // dragging the scene.
                    it.changedToDownIgnoreConsumed() ||
                    // A) unconsumed AND recently pressed
                    it.changedToDown() ||
                        // B) unconsumed AND in a new position (on the current axis)
                        it.positionChange().toFloat() != 0f
                }
@@ -461,4 +510,15 @@ internal class MultiPointerDraggableNode(
            }
        }
    }

    private fun List<PointerInputChange>.countDown() = fastSumBy { if (it.pressed) 1 else 0 }
}

internal fun interface PointersInfoOwner {
    fun pointersInfo(): PointersInfo
}

internal data class PointersInfo(
    val startedPosition: Offset?,
    val pointersDown: Int,
)
+6 −0
Original line number Diff line number Diff line
@@ -128,6 +128,7 @@ private class NestedScrollToSceneNode(
    bottomOrRightBehavior: NestedScrollBehavior,
    isExternalOverscrollGesture: () -> Boolean,
) : DelegatingNode() {
    lateinit var pointersInfoOwner: PointersInfoOwner
    private var priorityNestedScrollConnection: PriorityNestedScrollConnection =
        scenePriorityNestedScrollConnection(
            layoutImpl = layoutImpl,
@@ -135,6 +136,7 @@ private class NestedScrollToSceneNode(
            topOrLeftBehavior = topOrLeftBehavior,
            bottomOrRightBehavior = bottomOrRightBehavior,
            isExternalOverscrollGesture = isExternalOverscrollGesture,
            pointersInfoOwner = { pointersInfoOwner.pointersInfo() }
        )

    private var nestedScrollNode: DelegatableNode =
@@ -144,6 +146,7 @@ private class NestedScrollToSceneNode(
        )

    override fun onAttach() {
        pointersInfoOwner = requireAncestorPointersInfoOwner()
        delegate(nestedScrollNode)
    }

@@ -171,6 +174,7 @@ private class NestedScrollToSceneNode(
                topOrLeftBehavior = topOrLeftBehavior,
                bottomOrRightBehavior = bottomOrRightBehavior,
                isExternalOverscrollGesture = isExternalOverscrollGesture,
                pointersInfoOwner = pointersInfoOwner,
            )
        nestedScrollNode =
            nestedScrollModifierNode(
@@ -187,6 +191,7 @@ private fun scenePriorityNestedScrollConnection(
    topOrLeftBehavior: NestedScrollBehavior,
    bottomOrRightBehavior: NestedScrollBehavior,
    isExternalOverscrollGesture: () -> Boolean,
    pointersInfoOwner: PointersInfoOwner,
) =
    NestedScrollHandlerImpl(
            layoutImpl = layoutImpl,
@@ -194,5 +199,6 @@ private fun scenePriorityNestedScrollConnection(
            topOrLeftBehavior = topOrLeftBehavior,
            bottomOrRightBehavior = bottomOrRightBehavior,
            isExternalOverscrollGesture = isExternalOverscrollGesture,
            pointersInfoOwner = pointersInfoOwner,
        )
        .connection
+4 −1
Original line number Diff line number Diff line
@@ -113,7 +113,10 @@ class DraggableHandlerTest {
                    orientation = draggableHandler.orientation,
                    topOrLeftBehavior = nestedScrollBehavior,
                    bottomOrRightBehavior = nestedScrollBehavior,
                    isExternalOverscrollGesture = { isExternalOverscrollGesture }
                    isExternalOverscrollGesture = { isExternalOverscrollGesture },
                    pointersInfoOwner = {
                        PointersInfo(startedPosition = Offset.Zero, pointersDown = 1)
                    }
                )
                .connection

+74 −0
Original line number Diff line number Diff line
@@ -838,6 +838,80 @@ 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