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

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

Merge "STL may not consume gesture during overscroll" into main

parents 673299b1 9bc29411
Loading
Loading
Loading
Loading
+16 −3
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import androidx.compose.ui.util.fastCoerceIn
import com.android.compose.animation.scene.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified
import com.android.compose.animation.scene.content.Scene
import com.android.compose.nestedscroll.PriorityNestedScrollConnection
@@ -288,7 +289,8 @@ private class DragControllerImpl(

        val toScene = swipeTransition._toScene
        val distance = swipeTransition.distance()
        val desiredOffset = swipeTransition.dragOffset + delta
        val previousOffset = swipeTransition.dragOffset
        val desiredOffset = previousOffset + delta

        fun hasReachedToSceneUpOrLeft() =
            distance < 0 &&
@@ -312,6 +314,7 @@ private class DragControllerImpl(
        val fromScene: Scene
        val currentTransitionOffset: Float
        val newOffset: Float
        val consumedDelta: Float
        if (hasReachedToScene) {
            // The new transition will start from the current toScene
            fromScene = toScene
@@ -319,11 +322,21 @@ private class DragControllerImpl(
            currentTransitionOffset = distance
            // The next transition will start with the remaining offset
            newOffset = desiredOffset - distance
            consumedDelta = delta
        } else {
            fromScene = swipeTransition._fromScene
            currentTransitionOffset = desiredOffset
            val desiredProgress = swipeTransition.computeProgress(desiredOffset)
            // note: the distance could be negative if fromScene is aboveOrLeft of toScene.
            currentTransitionOffset =
                when {
                    distance == DistanceUnspecified ||
                        swipeTransition.isWithinProgressRange(desiredProgress) -> desiredOffset
                    distance > 0f -> desiredOffset.fastCoerceIn(0f, distance)
                    else -> desiredOffset.fastCoerceIn(distance, 0f)
                }
            // If there is a new transition, we will use the same offset
            newOffset = currentTransitionOffset
            consumedDelta = newOffset - previousOffset
        }

        swipeTransition.dragOffset = currentTransitionOffset
@@ -363,7 +376,7 @@ private class DragControllerImpl(
            updateTransition(newSwipeTransition)
        }

        return delta
        return consumedDelta
    }

    override fun onStop(velocity: Float, canChangeScene: Boolean): Float {
+28 −16
Original line number Diff line number Diff line
@@ -195,10 +195,17 @@ class DraggableHandlerTest {
            startedPosition: Offset = Offset.Zero,
            overSlop: Float,
            pointersDown: Int = 1,
            expectedConsumedOverSlop: Float = overSlop,
        ): DragController {
            // overSlop should be 0f only if the drag gesture starts with startDragImmediately
            if (overSlop == 0f) error("Consider using onDragStartedImmediately()")
            return onDragStarted(draggableHandler, startedPosition, overSlop, pointersDown)
            return onDragStarted(
                draggableHandler = draggableHandler,
                startedPosition = startedPosition,
                overSlop = overSlop,
                pointersDown = pointersDown,
                expectedConsumedOverSlop = expectedConsumedOverSlop,
            )
        }

        fun onDragStartedImmediately(
@@ -213,7 +220,7 @@ class DraggableHandlerTest {
            startedPosition: Offset = Offset.Zero,
            overSlop: Float = 0f,
            pointersDown: Int = 1,
            expectedConsumed: Boolean = true,
            expectedConsumedOverSlop: Float = overSlop,
        ): DragController {
            val dragController =
                draggableHandler.onDragStarted(
@@ -223,14 +230,14 @@ class DraggableHandlerTest {
                )

            // MultiPointerDraggable will always call onDelta with the initial overSlop right after
            dragController.onDragDelta(pixels = overSlop, expectedConsumed = expectedConsumed)
            dragController.onDragDelta(pixels = overSlop, expectedConsumedOverSlop)

            return dragController
        }

        fun DragController.onDragDelta(pixels: Float, expectedConsumed: Boolean = true) {
        fun DragController.onDragDelta(pixels: Float, expectedConsumed: Float = pixels) {
            val consumed = onDrag(delta = pixels)
            assertThat(consumed).isEqualTo(if (expectedConsumed) pixels else 0f)
            assertThat(consumed).isEqualTo(expectedConsumed)
        }

        fun DragController.onDragStopped(
@@ -370,14 +377,14 @@ class DraggableHandlerTest {
        onDragStarted(
            horizontalDraggableHandler,
            overSlop = up(fractionOfScreen = 0.3f),
            expectedConsumed = false,
            expectedConsumedOverSlop = 0f,
        )
        assertIdle(currentScene = SceneA)

        onDragStarted(
            horizontalDraggableHandler,
            overSlop = down(fractionOfScreen = 0.3f),
            expectedConsumed = false,
            expectedConsumedOverSlop = 0f,
        )
        assertIdle(currentScene = SceneA)
    }
@@ -504,19 +511,19 @@ class DraggableHandlerTest {

        // start accelaratedScroll and scroll over to B -> null
        val dragController2 = onDragStartedImmediately()
        dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = false)
        dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = false)
        dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = 0f)
        dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = 0f)

        // here onDragStopped is already triggered, but subsequent onDelta/onDragStopped calls may
        // still be called. Make sure that they don't crash or change the scene
        dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = false)
        dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = 0f)
        dragController2.onDragStopped(velocity = 0f)

        advanceUntilIdle()
        assertIdle(SceneB)

        // These events can still come in after the animation has settled
        dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = false)
        dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = 0f)
        dragController2.onDragStopped(velocity = 0f)
        assertIdle(SceneB)
    }
@@ -1051,8 +1058,16 @@ class DraggableHandlerTest {

        // Swipe up to scene B at progress = 200%.
        val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)
        val dragController = onDragStarted(startedPosition = middle, overSlop = up(2f))
        val transition = assertTransition(fromScene = SceneA, toScene = SceneB, progress = 2f)
        val dragController =
            onDragStarted(
                startedPosition = middle,
                overSlop = up(2f),
                // Overscroll is disabled, it will scroll up to 100%
                expectedConsumedOverSlop = up(1f),
            )

        // The progress value is coerced in `[0..1]`
        assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f)

        // Release the finger.
        dragController.onDragStopped(velocity = -velocityThreshold)
@@ -1061,9 +1076,6 @@ class DraggableHandlerTest {
        // 100% and that the overscroll on scene B is doing nothing, we are already idle.
        runCurrent()
        assertIdle(SceneB)

        // Progress is snapped to 100%.
        assertThat(transition).hasProgress(1f)
    }

    @Test
+60 −0
Original line number Diff line number Diff line
@@ -29,6 +29,9 @@ 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.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.testTag
@@ -786,4 +789,61 @@ class SwipeToSceneTest {
            .onNode(isElement(SceneB.rootElementKey))
            .assertPositionInRootIsEqualTo(-layoutSize, 0.dp)
    }

    @Test
    fun whenOverscrollIsDisabled_dragGestureShouldNotBeConsumed() {
        val swipeDistance = 100.dp

        var availableOnPostScroll = Float.MIN_VALUE
        val connection =
            object : NestedScrollConnection {
                override fun onPostScroll(
                    consumed: Offset,
                    available: Offset,
                    source: NestedScrollSource
                ): Offset {
                    availableOnPostScroll = available.y
                    return super.onPostScroll(consumed, available, source)
                }
            }
        val state =
            rule.runOnUiThread {
                MutableSceneTransitionLayoutState(
                    SceneA,
                    transitions {
                        from(SceneA, to = SceneB) { distance = FixedDistance(swipeDistance) }
                        overscroll(SceneB, Orientation.Vertical)
                    }
                )
            }
        val layoutSize = 200.dp
        var touchSlop = 0f
        rule.setContent {
            touchSlop = LocalViewConfiguration.current.touchSlop
            SceneTransitionLayout(state, Modifier.size(layoutSize).nestedScroll(connection)) {
                scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
                    Box(Modifier.fillMaxSize())
                }
                scene(SceneB) { Box(Modifier.element(TestElements.Foo).fillMaxSize()) }
            }
        }

        // Swipe down by the swipe distance so that we are on scene B.
        rule.onRoot().performTouchInput {
            val middle = (layoutSize / 2).toPx()
            down(Offset(middle, middle))
            moveBy(Offset(0f, touchSlop + (swipeDistance).toPx()), delayMillis = 1_000)
        }
        val transition = state.currentTransition
        assertThat(transition).isNotNull()
        assertThat(transition!!.progress).isEqualTo(1f)
        assertThat(availableOnPostScroll).isEqualTo(0f)

        // Overscrolling on Scene B
        val ovescrollPx = 100f
        rule.onRoot().performTouchInput { moveBy(Offset(0f, ovescrollPx), delayMillis = 1_000) }
        // Overscroll is disabled on Scene B
        assertThat(transition.progress).isEqualTo(1f)
        assertThat(availableOnPostScroll).isEqualTo(ovescrollPx)
    }
}