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

Commit ae080274 authored by Omar Miatello's avatar Omar Miatello Committed by Android (Google) Code Review
Browse files

Merge changes from topic "transitionInterceptionThreshold" into main

* changes:
  Add TestGestureScope.progress to make SceneGestureHandlerTest more readable
  The animation between scenes can only be intercepted in a defined range
parents dc8cf702 6384061e
Loading
Loading
Loading
Loading
+26 −6
Original line number Diff line number Diff line
@@ -39,13 +39,13 @@ import kotlinx.coroutines.launch

@VisibleForTesting
class SceneGestureHandler(
    private val layoutImpl: SceneTransitionLayoutImpl,
    internal val layoutImpl: SceneTransitionLayoutImpl,
    internal val orientation: Orientation,
    private val coroutineScope: CoroutineScope,
) {
    val draggable: DraggableHandler = SceneDraggableHandler(this)

    private var transitionState
    internal var transitionState
        get() = layoutImpl.state.transitionState
        set(value) {
            layoutImpl.state.transitionState = value
@@ -58,7 +58,7 @@ class SceneGestureHandler(
     * Note: the initialScene here does not matter, it's only used for initializing the transition
     * and will be replaced when a drag event starts.
     */
    private val swipeTransition = SwipeTransition(initialScene = currentScene)
    internal val swipeTransition = SwipeTransition(initialScene = currentScene)

    internal val currentScene: Scene
        get() = layoutImpl.scene(transitionState.currentScene)
@@ -415,7 +415,7 @@ class SceneGestureHandler(
        }
    }

    private class SwipeTransition(initialScene: Scene) : TransitionState.Transition {
    internal class SwipeTransition(initialScene: Scene) : TransitionState.Transition {
        var _currentScene by mutableStateOf(initialScene)
        override val currentScene: SceneKey
            get() = _currentScene.key
@@ -598,9 +598,29 @@ class SceneNestedScrollHandler(
        return PriorityNestedScrollConnection(
            canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
                canChangeScene = offsetBeforeStart == Offset.Zero
                gestureHandler.isDrivingTransition &&

                val canInterceptSwipeTransition =
                    canChangeScene &&
                        gestureHandler.isDrivingTransition &&
                        offsetAvailable.toAmount() != 0f
                if (!canInterceptSwipeTransition) return@PriorityNestedScrollConnection false

                val progress = gestureHandler.swipeTransition.progress
                val threshold = gestureHandler.layoutImpl.transitionInterceptionThreshold
                fun isProgressCloseTo(value: Float) = (progress - value).absoluteValue <= threshold

                // The transition is always between 0 and 1. If it is close to either of these
                // intervals, we want to go directly to the TransitionState.Idle.
                // The progress value can go beyond this range in the case of overscroll.
                val shouldSnapToIdle = isProgressCloseTo(0f) || isProgressCloseTo(1f)
                if (shouldSnapToIdle) {
                    gestureHandler.swipeTransition.stopOffsetAnimation()
                    gestureHandler.transitionState =
                        TransitionState.Idle(gestureHandler.swipeTransition.currentScene)
                }

                // Start only if we cannot consume this event
                !shouldSnapToIdle
            },
            canStartPostScroll = { offsetAvailable, offsetBeforeStart ->
                val amount = offsetAvailable.toAmount()
+6 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.compose.animation.scene

import androidx.annotation.FloatRange
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
@@ -41,6 +42,8 @@ import androidx.compose.ui.platform.LocalDensity
 * @param transitions the definition of the transitions used to animate a change of scene.
 * @param state the observable state of this layout.
 * @param edgeDetector the edge detector used to detect which edge a swipe is started from, if any.
 * @param transitionInterceptionThreshold used during a scene transition. For the scene to be
 *   intercepted, the progress value must be above the threshold, and below (1 - threshold).
 * @param scenes the configuration of the different scenes of this layout.
 */
@Composable
@@ -51,6 +54,7 @@ fun SceneTransitionLayout(
    modifier: Modifier = Modifier,
    state: SceneTransitionLayoutState = remember { SceneTransitionLayoutState(currentScene) },
    edgeDetector: EdgeDetector = DefaultEdgeDetector,
    @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f,
    scenes: SceneTransitionLayoutScope.() -> Unit,
) {
    val density = LocalDensity.current
@@ -63,6 +67,7 @@ fun SceneTransitionLayout(
            state = state,
            density = density,
            edgeDetector = edgeDetector,
            transitionInterceptionThreshold = transitionInterceptionThreshold,
            coroutineScope = coroutineScope,
        )
    }
@@ -71,6 +76,7 @@ fun SceneTransitionLayout(
    layoutImpl.transitions = transitions
    layoutImpl.density = density
    layoutImpl.edgeDetector = edgeDetector
    layoutImpl.transitionInterceptionThreshold = transitionInterceptionThreshold

    layoutImpl.setScenes(scenes)
    layoutImpl.setCurrentScene(currentScene)
+2 −0
Original line number Diff line number Diff line
@@ -50,6 +50,7 @@ class SceneTransitionLayoutImpl(
    internal val state: SceneTransitionLayoutState,
    density: Density,
    edgeDetector: EdgeDetector,
    transitionInterceptionThreshold: Float,
    coroutineScope: CoroutineScope,
) {
    internal val scenes = SnapshotStateMap<SceneKey, Scene>()
@@ -62,6 +63,7 @@ class SceneTransitionLayoutImpl(
    internal var transitions by mutableStateOf(transitions)
    internal var density: Density by mutableStateOf(density)
    internal var edgeDetector by mutableStateOf(edgeDetector)
    internal var transitionInterceptionThreshold by mutableStateOf(transitionInterceptionThreshold)

    private val horizontalGestureHandler: SceneGestureHandler
    private val verticalGestureHandler: SceneGestureHandler
+75 −15
Original line number Diff line number Diff line
@@ -69,6 +69,8 @@ class SceneGestureHandlerTest {
            scene(SceneC) { Text("SceneC") }
        }

        val transitionInterceptionThreshold = 0.05f

        val sceneGestureHandler =
            SceneGestureHandler(
                layoutImpl =
@@ -79,6 +81,7 @@ class SceneGestureHandlerTest {
                            state = layoutState,
                            density = Density(1f),
                            edgeDetector = DefaultEdgeDetector,
                            transitionInterceptionThreshold = transitionInterceptionThreshold,
                            coroutineScope = coroutineScope,
                        )
                        .apply { setScenesTargetSizeForTest(LAYOUT_SIZE) },
@@ -107,6 +110,9 @@ class SceneGestureHandlerTest {
        val transitionState: TransitionState
            get() = layoutState.transitionState

        val progress: Float
            get() = (transitionState as Transition).progress

        fun advanceUntilIdle() {
            coroutineScope.testScheduler.advanceUntilIdle()
        }
@@ -145,13 +151,12 @@ class SceneGestureHandlerTest {
    fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest {
        draggable.onDragStarted()
        assertScene(currentScene = SceneA, isIdle = false)
        val transition = transitionState as Transition

        draggable.onDelta(pixels = deltaInPixels10)
        assertThat(transition.progress).isEqualTo(0.1f)
        assertThat(progress).isEqualTo(0.1f)

        draggable.onDelta(pixels = deltaInPixels10)
        assertThat(transition.progress).isEqualTo(0.2f)
        assertThat(progress).isEqualTo(0.2f)
    }

    @Test
@@ -257,8 +262,7 @@ class SceneGestureHandlerTest {
            )

        assertScene(currentScene = SceneA, isIdle = false)
        val transition = transitionState as Transition
        assertThat(transition.progress).isEqualTo(0.1f)
        assertThat(progress).isEqualTo(0.1f)
        assertThat(consumed).isEqualTo(offsetY10)
    }

@@ -282,13 +286,12 @@ class SceneGestureHandlerTest {
        nestedScroll.scroll(available = offsetY10)
        assertScene(currentScene = SceneA, isIdle = false)

        val transition = transitionState as Transition
        assertThat(transition.progress).isEqualTo(0.1f)
        assertThat(progress).isEqualTo(0.1f)

        // start intercept preScroll
        val consumed =
            nestedScroll.onPreScroll(available = offsetY10, source = NestedScrollSource.Drag)
        assertThat(transition.progress).isEqualTo(0.2f)
        assertThat(progress).isEqualTo(0.2f)

        // do nothing on postScroll
        nestedScroll.onPostScroll(
@@ -296,13 +299,71 @@ class SceneGestureHandlerTest {
            available = Offset.Zero,
            source = NestedScrollSource.Drag
        )
        assertThat(transition.progress).isEqualTo(0.2f)
        assertThat(progress).isEqualTo(0.2f)

        nestedScroll.scroll(available = offsetY10)
        assertThat(transition.progress).isEqualTo(0.3f)
        assertThat(progress).isEqualTo(0.3f)
        assertScene(currentScene = SceneA, isIdle = false)
    }

    private suspend fun TestGestureScope.preScrollAfterSceneTransition(
        firstScroll: Float,
        secondScroll: Float
    ) {
        val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithOverscroll)
        // start scene transition
        nestedScroll.scroll(available = Offset(0f, SCREEN_SIZE * firstScroll))

        // stop scene transition (start the "stop animation")
        nestedScroll.onPreFling(available = Velocity.Zero)

        // a pre scroll event, that could be intercepted by SceneGestureHandler
        nestedScroll.onPreScroll(Offset(0f, SCREEN_SIZE * secondScroll), NestedScrollSource.Drag)
    }

    // Float tolerance for comparisons
    private val tolerance = 0.00001f

    @Test
    fun scrollAndFling_scrollLessThanInterceptable_goToIdleOnCurrentScene() = runGestureTest {
        val first = transitionInterceptionThreshold - tolerance
        val second = 0.01f

        preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)

        assertScene(SceneA, isIdle = true)
    }

    @Test
    fun scrollAndFling_scrollMinInterceptable_interceptPreScrollEvents() = runGestureTest {
        val first = transitionInterceptionThreshold + tolerance
        val second = 0.01f

        preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)

        assertThat(progress).isWithin(tolerance).of(first + second)
    }

    @Test
    fun scrollAndFling_scrollMaxInterceptable_interceptPreScrollEvents() = runGestureTest {
        val first = 1f - transitionInterceptionThreshold - tolerance
        val second = 0.01f

        preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)

        assertThat(progress).isWithin(tolerance).of(first + second)
    }

    @Test
    fun scrollAndFling_scrollMoreThanInterceptable_goToIdleOnNextScene() = runGestureTest {
        val first = 1f - transitionInterceptionThreshold + tolerance
        val second = 0.01f

        preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)

        assertScene(SceneC, isIdle = true)
    }

    @Test
    fun onPreFling_velocityLowerThanThreshold_remainSameScene() = runGestureTest {
        val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithOverscroll)
@@ -444,24 +505,23 @@ class SceneGestureHandlerTest {
        val nestedScroll = nestedScrollConnection(nestedScrollBehavior = Always)
        draggable.onDragStarted()
        assertScene(currentScene = SceneA, isIdle = false)
        val transition = transitionState as Transition

        draggable.onDelta(deltaInPixels10)
        assertThat(transition.progress).isEqualTo(0.1f)
        assertThat(progress).isEqualTo(0.1f)

        // now we can intercept the scroll events
        nestedScroll.scroll(available = offsetY10)
        assertThat(transition.progress).isEqualTo(0.2f)
        assertThat(progress).isEqualTo(0.2f)

        // this should be ignored, we are scrolling now!
        draggable.onDragStopped(velocityThreshold)
        assertScene(currentScene = SceneA, isIdle = false)

        nestedScroll.scroll(available = offsetY10)
        assertThat(transition.progress).isEqualTo(0.3f)
        assertThat(progress).isEqualTo(0.3f)

        nestedScroll.scroll(available = offsetY10)
        assertThat(transition.progress).isEqualTo(0.4f)
        assertThat(progress).isEqualTo(0.4f)

        nestedScroll.onPreFling(available = Velocity(0f, velocityThreshold))
        assertScene(currentScene = SceneC, isIdle = false)