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

Commit ef89e6e6 authored by omarmt's avatar omarmt
Browse files

STL findUserActionResult uses the current orientation and direction

We want to determine if the current direction indicates a user action.
If not, we can apply an overscroll effect.

Test: Manually tested on Flexiglass
Bug: 378470603
Flag: com.android.systemui.scene_container
Change-Id: I3bb142bfc055307429ea53187cdb14690e8ad474
parent c7f817b5
Loading
Loading
Loading
Loading
+1 −22
Original line number Diff line number Diff line
@@ -108,7 +108,7 @@ internal class DraggableHandlerImpl(

        swipes.updateSwipesResults(fromContent)
        val result =
            swipes.findUserActionResult(overSlop)
            (if (overSlop < 0f) swipes.upOrLeftResult else swipes.downOrRightResult)
                // As we were unable to locate a valid target scene, the initial SwipeAnimation
                // cannot be defined. Consequently, a simple NoOp Controller will be returned.
                ?: return NoOpDragController
@@ -448,27 +448,6 @@ internal class Swipes(val upOrLeft: Swipe.Resolved, val downOrRight: Swipe.Resol
        this.upOrLeftResult = upOrLeftResult
        this.downOrRightResult = downOrRightResult
    }

    /**
     * Returns the [UserActionResult] in the direction of [directionOffset].
     *
     * @param directionOffset signed float that indicates the direction. Positive is down or right
     *   negative is up or left.
     * @return null when there are no targets in either direction. If one direction is null and you
     *   drag into the null direction this function will return the opposite direction, assuming
     *   that the users intention is to start the drag into the other direction eventually. If
     *   [directionOffset] is 0f and both direction are available, it will default to
     *   [upOrLeftResult].
     */
    fun findUserActionResult(directionOffset: Float): UserActionResult? {
        return when {
            upOrLeftResult == null && downOrRightResult == null -> null
            (directionOffset < 0f && upOrLeftResult != null) || downOrRightResult == null ->
                upOrLeftResult

            else -> downOrRightResult
        }
    }
}

internal class NestedScrollHandlerImpl(
+6 −117
Original line number Diff line number Diff line
@@ -19,7 +19,9 @@ package com.android.compose.animation.scene
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.overscroll
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.UserInput
@@ -102,7 +104,7 @@ class DraggableHandlerTest {
                userActions =
                    mapOf(Swipe.Up to SceneB, Swipe.Up(fromSource = Edge.Bottom) to SceneA),
            ) {
                Text("SceneC")
                Text("SceneC", Modifier.overscroll(verticalOverscrollEffect))
            }
            overlay(
                key = OverlayA,
@@ -434,35 +436,12 @@ class DraggableHandlerTest {
    }

    @Test
    fun onDragIntoNoAction_startTransitionToOppositeDirection() = runGestureTest {
    fun onDragIntoNoAction_stayIdle() = runGestureTest {
        navigateToSceneC()

        // We are on SceneC which has no action in Down direction
        val dragController = onDragStarted(overSlop = 10f)
        assertTransition(
            currentScene = SceneC,
            fromScene = SceneC,
            toScene = SceneB,
            progress = -0.1f,
        )

        // Reverse drag direction, it will consume the previous drag
        dragController.onDragDelta(pixels = -10f)
        assertTransition(
            currentScene = SceneC,
            fromScene = SceneC,
            toScene = SceneB,
            progress = 0.0f,
        )

        // Continue reverse drag direction, it should record progress to Scene B
        dragController.onDragDelta(pixels = -10f)
        assertTransition(
            currentScene = SceneC,
            fromScene = SceneC,
            toScene = SceneB,
            progress = 0.1f,
        )
        onDragStarted(overSlop = 10f, expectedConsumedOverSlop = 0f)
        assertIdle(currentScene = SceneC)
    }

    @Test
@@ -941,30 +920,6 @@ class DraggableHandlerTest {
        assertIdle(SceneA)
    }

    @Test
    fun scrollFromIdleWithNoTargetScene_shouldUseOverscrollSpecIfAvailable() = runGestureTest {
        layoutState.transitions = transitions {
            overscroll(SceneC, Orientation.Vertical) { fade(TestElements.Foo) }
        }
        // Start at scene C.
        navigateToSceneC()

        val scene = layoutState.transitionState.currentScene
        // We should have overscroll spec for scene C
        assertThat(layoutState.transitions.overscrollSpec(scene, Orientation.Vertical)).isNotNull()
        assertThat(layoutState.currentTransition?.currentOverscrollSpec).isNull()

        val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways)
        nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f))

        // We scrolled down, under scene C there is nothing, so we can use the overscroll spec
        assertThat(layoutState.currentTransition?.currentOverscrollSpec).isNotNull()
        assertThat(layoutState.currentTransition?.currentOverscrollSpec?.content).isEqualTo(SceneC)
        val transition = layoutState.currentTransition
        assertThat(transition).isNotNull()
        assertThat(transition!!.progress).isEqualTo(-0.1f)
    }

    @Test
    fun nestedScrollUseFromSourceInfo() = runGestureTest {
        // Start at scene C.
@@ -1228,72 +1183,6 @@ class DraggableHandlerTest {
        assertThat(transition).hasOverscrollSpec()
    }

    @Test
    fun overscroll_releaseAtNegativePercent_up() = runGestureTest {
        // Make Scene A overscrollable.
        layoutState.transitions = transitions {
            from(SceneA, to = SceneB) { spec = spring(dampingRatio = Spring.DampingRatioNoBouncy) }
            overscroll(SceneA, Orientation.Vertical) { fade(TestElements.Foo) }
        }

        mutableUserActionsA = mapOf(Swipe.Up to UserActionResult(SceneB))

        val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
        val dragController = onDragStarted(pointersInfo = middle, overSlop = down(1f))
        val transition = assertThat(transitionState).isSceneTransition()
        assertThat(transition).hasFromScene(SceneA)
        assertThat(transition).hasToScene(SceneB)
        assertThat(transition).hasProgress(-1f)

        // Release to A.
        dragController.onDragStoppedAnimateNow(
            velocity = 0f,
            onAnimationStart = {
                assertTransition(fromScene = SceneA, toScene = SceneB, progress = -1f)
            },
            expectedConsumedVelocity = 0f,
        )

        // We kept the overscroll at 100% so that the placement logic didn't change at the end of
        // the animation.
        assertIdle(SceneA)
        assertThat(transition).hasProgress(0f)
        assertThat(transition).hasOverscrollSpec()
    }

    @Test
    fun overscroll_releaseAtNegativePercent_down() = runGestureTest {
        // Make Scene A overscrollable.
        layoutState.transitions = transitions {
            from(SceneA, to = SceneC) { spec = spring(dampingRatio = Spring.DampingRatioNoBouncy) }
            overscroll(SceneA, Orientation.Vertical) { fade(TestElements.Foo) }
        }

        mutableUserActionsA = mapOf(Swipe.Down to UserActionResult(SceneC))

        val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
        val dragController = onDragStarted(pointersInfo = middle, overSlop = up(1f))
        val transition = assertThat(transitionState).isSceneTransition()
        assertThat(transition).hasFromScene(SceneA)
        assertThat(transition).hasToScene(SceneC)
        assertThat(transition).hasProgress(-1f)

        // Release to A.
        dragController.onDragStoppedAnimateNow(
            velocity = 0f,
            onAnimationStart = {
                assertTransition(fromScene = SceneA, toScene = SceneC, progress = -1f)
            },
            expectedConsumedVelocity = 0f,
        )

        // We kept the overscroll at 100% so that the placement logic didn't change at the end of
        // the animation.
        assertIdle(SceneA)
        assertThat(transition).hasProgress(0f)
        assertThat(transition).hasOverscrollSpec()
    }

    @Test
    fun requireFullDistanceSwipe() = runGestureTest {
        mutableUserActionsA +=
+25 −28
Original line number Diff line number Diff line
@@ -1006,77 +1006,74 @@ class ElementTest {

    @Test
    fun elementTransitionDuringNestedScrollOverscroll() {
        lateinit var density: Density
        // 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 overscrollTranslateY = 10.dp
        val layoutWidth = 200.dp
        val layoutHeight = 400.dp

        val state =
            rule.runOnUiThread {
                MutableSceneTransitionLayoutState(
                    initialScene = SceneB,
                    transitions =
                        transitions {
                            overscroll(SceneB, Orientation.Vertical) {
                                progressConverter = ProgressConverter.linear()
                                translate(TestElements.Foo, y = overscrollTranslateY)
                            }
                        },
                    initialScene = SceneA,
                    transitions = transitions { overscrollDisabled(SceneB, Orientation.Vertical) },
                )
                    as MutableSceneTransitionLayoutStateImpl
            }

        rule.setContent {
            density = LocalDensity.current
            touchSlop = LocalViewConfiguration.current.touchSlop
            SceneTransitionLayout(
                state = state,
                modifier = Modifier.size(layoutWidth, layoutHeight),
            ) {
                scene(SceneA) { Spacer(Modifier.fillMaxSize()) }
                scene(SceneB, userActions = mapOf(Swipe.Up to SceneA)) {
                scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
                    Box(
                        Modifier
                            // A scrollable that does not consume the scroll gesture
                            .scrollable(
                                rememberScrollableState(consumeScrollDelta = { 0f }),
                                Orientation.Vertical,
                                state = rememberScrollableState(consumeScrollDelta = { 0f }),
                                orientation = Orientation.Vertical,
                            )
                            .fillMaxSize()
                    ) {
                        Spacer(Modifier.element(TestElements.Foo).fillMaxSize())
                    )
                }
                scene(SceneB) {
                    Spacer(
                        Modifier.overscroll(verticalOverscrollEffect)
                            .element(TestElements.Foo)
                            .fillMaxSize()
                    )
                }
            }
        }

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

        // Swipe by half of verticalSwipeDistance.
        rule.onRoot().performTouchInput {
            val middleTop = Offset((layoutWidth / 2).toPx(), 0f)
            down(middleTop)
            // Scroll 50%
            // Scroll 50%.
            moveBy(Offset(0f, touchSlop + layoutHeight.toPx() * 0.5f), delayMillis = 1_000)
        }

        val transition = assertThat(state.transitionState).isSceneTransition()
        assertThat(transition).hasOverscrollSpec()
        assertThat(transition).hasProgress(-0.5f)
        fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 0.5f)
        assertThat(transition).hasProgress(0.5f)
        rule.onNodeWithTag(TestElements.Foo.testTag).assertTopPositionInRootIsEqualTo(0.dp)

        rule.onRoot().performTouchInput {
            // Scroll another 100%
            // Scroll another 100%.
            moveBy(Offset(0f, layoutHeight.toPx()), delayMillis = 1_000)
        }

        // Scroll 150% (Scene B overscroll by 50%)
        assertThat(transition).hasProgress(-1.5f)
        assertThat(transition).hasOverscrollSpec()
        fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 1.5f)
        // Scroll 150% (Scene B overscroll by 50%).
        assertThat(transition).hasProgress(1f)
        rule
            .onNodeWithTag(TestElements.Foo.testTag)
            .assertTopPositionInRootIsEqualTo(expectedOffset(layoutHeight * 0.5f, density))
    }

    @Test