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

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

Merge changes I8bc615e6,Ib912aec5,I03ffcd8f into main

* changes:
  Remove unnecessary useUnmergedTree from ElementTest
  Expose startedPosition and pointersDown to NestedScrollHandler
  MultiPointerDraggableNode checks orientation only once
parents acb871e6 e40ab41b
Loading
Loading
Loading
Loading
+28 −24
Original line number Diff line number Diff line
@@ -901,6 +901,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)
@@ -912,6 +913,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 =
@@ -919,9 +927,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 =
@@ -929,17 +939,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
@@ -1037,10 +1040,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,
                    )
            },
+15 −7
Original line number Diff line number Diff line
@@ -138,11 +138,26 @@ internal class MultiPointerDraggableNode(
            }
        }

    private var _toFloat = orientation.toFunctionOffsetToFloat()

    private fun Offset.toFloat(): Float = _toFloat(this)

    private fun Orientation.toFunctionOffsetToFloat(): (Offset) -> Float =
        when (this) {
            Orientation.Vertical -> {
                { it.y }
            }
            Orientation.Horizontal -> {
                { it.x }
            }
        }

    var orientation: Orientation = orientation
        set(value) {
            // Reset the pointer input whenever orientation changed.
            if (value != field) {
                field = value
                _toFloat = field.toFunctionOffsetToFloat()
                delegate.resetPointerInputHandler()
            }
        }
@@ -367,13 +382,6 @@ internal class MultiPointerDraggableNode(
        return event
    }

    private fun Offset.toFloat(): Float {
        return when (orientation) {
            Orientation.Vertical -> y
            Orientation.Horizontal -> x
        }
    }

    /**
     * Continues to read drag events until all pointers are up or the drag event is canceled. The
     * initial pointer to use for driving the drag is [initialPointerId]. [hasDragged] passes the
+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
@@ -113,7 +113,8 @@ class DraggableHandlerTest {
                    orientation = draggableHandler.orientation,
                    topOrLeftBehavior = nestedScrollBehavior,
                    bottomOrRightBehavior = nestedScrollBehavior,
                    isExternalOverscrollGesture = { isExternalOverscrollGesture }
                    isExternalOverscrollGesture = { isExternalOverscrollGesture },
                    pointersInfo = { PointersInfo(pointersDown = 1, startedPosition = Offset.Zero) }
                )
                .connection

+77 −4
Original line number Diff line number Diff line
@@ -731,7 +731,7 @@ class ElementTest {
                onAnimatedFloat = { animatedFloat = it },
            )

        val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true)
        val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag)
        fooElement.assertTopPositionInRootIsEqualTo(0.dp)
        val transition = assertThat(state.transitionState).isTransition()
        assertThat(transition).isNotNull()
@@ -811,7 +811,7 @@ class ElementTest {
        }

        assertThat(state.transitionState).isIdle()
        val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true)
        val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag)
        fooElement.assertTopPositionInRootIsEqualTo(0.dp)

        // Swipe by half of verticalSwipeDistance.
@@ -838,6 +838,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
@@ -858,7 +931,7 @@ class ElementTest {
                onAnimatedFloat = { animatedFloat = it },
            )

        val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true)
        val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag)
        fooElement.assertTopPositionInRootIsEqualTo(0.dp)
        assertThat(animatedFloat).isEqualTo(100f)

@@ -914,7 +987,7 @@ class ElementTest {
                onAnimatedFloat = { animatedFloat = it },
            )

        val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true)
        val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag)
        fooElement.assertTopPositionInRootIsEqualTo(0.dp)
        assertThat(animatedFloat).isEqualTo(100f)