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

Commit 3770e4e2 authored by Shawn Lee's avatar Shawn Lee
Browse files

[flexiglass] Added isUserInputOngoing to transition model

Adds the concept of isUserInputOngoing to the scene transition model. This can be used to determine whether user input is driving a transition. Consumers of flinging state in the old world can use this value if they were relying on NPVC's definition of "flinging" which was actually just animating without user input.

Bug: 303321199
Test: added to SwipeToScene test
Change-Id: Ie25e1a6c8d64f32b854de36c99fbefa9fb5f3402
parent 50622554
Loading
Loading
Loading
Loading
+19 −4
Original line number Diff line number Diff line
@@ -108,7 +108,7 @@ private fun CoroutineScope.animate(
) {
    val fromScene = layoutImpl.state.transitionState.currentScene
    val isUserInput =
        (layoutImpl.state.transitionState as? TransitionState.Transition)?.isUserInputDriven
        (layoutImpl.state.transitionState as? TransitionState.Transition)?.isInitiatedByUserInput
            ?: false

    val animationSpec = layoutImpl.transitions.transitionSpec(fromScene, target).spec
@@ -119,9 +119,23 @@ private fun CoroutineScope.animate(
    val targetProgress = if (reversed) 0f else 1f
    val transition =
        if (reversed) {
            OneOffTransition(target, fromScene, currentScene = target, isUserInput, animatable)
            OneOffTransition(
                fromScene = target,
                toScene = fromScene,
                currentScene = target,
                isUserInput,
                isUserInputOngoing = false,
                animatable,
            )
        } else {
            OneOffTransition(fromScene, target, currentScene = target, isUserInput, animatable)
            OneOffTransition(
                fromScene = fromScene,
                toScene = target,
                currentScene = target,
                isUserInput,
                isUserInputOngoing = false,
                animatable,
            )
        }

    // Change the current layout state to use this new transition.
@@ -142,7 +156,8 @@ private class OneOffTransition(
    override val fromScene: SceneKey,
    override val toScene: SceneKey,
    override val currentScene: SceneKey,
    override val isUserInputDriven: Boolean,
    override val isInitiatedByUserInput: Boolean,
    override val isUserInputOngoing: Boolean,
    private val animatable: Animatable<Float, AnimationVector1D>,
) : TransitionState.Transition {
    override val progress: Float
+10 −2
Original line number Diff line number Diff line
@@ -52,7 +52,14 @@ sealed class ObservableTransitionState {
         * scene, this value will remain true after the pointer is no longer touching the screen and
         * will be true in any transition created to animate back to the original position.
         */
        val isUserInputDriven: Boolean,
        val isInitiatedByUserInput: Boolean,

        /**
         * Whether user input is currently driving the transition. For example, if a user is
         * dragging a pointer, this emits true. Once they lift their finger, this emits false while
         * the transition completes/settles.
         */
        val isUserInputOngoing: Flow<Boolean>,
    ) : ObservableTransitionState()
}

@@ -73,7 +80,8 @@ fun SceneTransitionLayoutState.observableTransitionState(): Flow<ObservableTrans
                            fromScene = state.fromScene,
                            toScene = state.toScene,
                            progress = snapshotFlow { state.progress },
                            isUserInputDriven = state.isUserInputDriven,
                            isInitiatedByUserInput = state.isInitiatedByUserInput,
                            isUserInputOngoing = snapshotFlow { state.isUserInputOngoing },
                        )
                    }
                }
+4 −1
Original line number Diff line number Diff line
@@ -70,6 +70,9 @@ sealed interface TransitionState {
        val progress: Float

        /** Whether the transition was triggered by user input rather than being programmatic. */
        val isUserInputDriven: Boolean
        val isInitiatedByUserInput: Boolean

        /** Whether user input is currently driving the transition. */
        val isUserInputOngoing: Boolean
    }
}
+31 −31
Original line number Diff line number Diff line
@@ -66,7 +66,7 @@ internal fun Modifier.swipeToScene(
    // swipe in the other direction.
    val startDragImmediately =
        state == transition &&
            transition.isAnimatingOffset &&
            !transition.isUserInputOngoing &&
            !currentScene.shouldEnableSwipes(orientation.opposite())

    // The velocity threshold at which the intent of the user is to swipe up or down. It is the same
@@ -126,7 +126,7 @@ private class SwipeTransition(initialScene: Scene) : TransitionState.Transition

    override val progress: Float
        get() {
            val offset = if (isAnimatingOffset) offsetAnimatable.value else dragOffset
            val offset = if (isUserInputOngoing) dragOffset else offsetAnimatable.value
            if (distance == 0f) {
                // This can happen only if fromScene == toScene.
                error(
@@ -137,16 +137,15 @@ private class SwipeTransition(initialScene: Scene) : TransitionState.Transition
            return offset / distance
        }

    override val isUserInputDriven = true
    override val isInitiatedByUserInput = true

    var _isUserInputOngoing by mutableStateOf(false)
    override val isUserInputOngoing: Boolean
        get() = _isUserInputOngoing

    /** The current offset caused by the drag gesture. */
    var dragOffset by mutableFloatStateOf(0f)

    /**
     * Whether the offset is animated (the user lifted their finger) or if it is driven by gesture.
     */
    var isAnimatingOffset by mutableStateOf(false)

    /** The animatable used to animate the offset once the user lifted its finger. */
    val offsetAnimatable = Animatable(0f, visibilityThreshold = OffsetVisibilityThreshold)

@@ -209,9 +208,11 @@ private fun onDragStarted(
    transition: SwipeTransition,
    orientation: Orientation,
) {
    transition._isUserInputOngoing = true

    if (layoutImpl.state.transitionState == transition) {
        // This [transition] was already driving the animation: simply take over it.
        if (transition.isAnimatingOffset) {
        if (!transition.isUserInputOngoing) {
            // Stop animating and start from where the current offset. Setting the animation job to
            // `null` will effectively cancel the animation.
            transition.stopOffsetAnimation()
@@ -456,10 +457,10 @@ private fun CoroutineScope.animateOffset(
) {
    transition.startOffsetAnimation {
        launch {
                if (!transition.isAnimatingOffset) {
            if (transition.isUserInputOngoing) {
                transition.offsetAnimatable.snapTo(transition.dragOffset)
            }
                transition.isAnimatingOffset = true
            transition._isUserInputOngoing = false

            transition.offsetAnimatable.animateTo(
                targetOffset,
@@ -479,7 +480,6 @@ private fun CoroutineScope.animateOffset(
                layoutImpl.state.transitionState = TransitionState.Idle(targetScene)
            }
        }
            .also { it.invokeOnCompletion { transition.isAnimatingOffset = false } }
    }
}

+8 −4
Original line number Diff line number Diff line
@@ -122,7 +122,8 @@ class SwipeToSceneTest {
        assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
        assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
        assertThat(transition.progress).isEqualTo(55.dp / LayoutWidth)
        assertThat(transition.isUserInputDriven).isTrue()
        assertThat(transition.isInitiatedByUserInput).isTrue()
        assertThat(transition.isUserInputOngoing).isTrue()

        // Release the finger. We should now be animating back to A (currentScene = SceneA) given
        // that 55dp < positional threshold.
@@ -134,7 +135,8 @@ class SwipeToSceneTest {
        assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
        assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
        assertThat(transition.progress).isEqualTo(55.dp / LayoutWidth)
        assertThat(transition.isUserInputDriven).isTrue()
        assertThat(transition.isInitiatedByUserInput).isTrue()
        assertThat(transition.isUserInputOngoing).isFalse()

        // Wait for the animation to finish. We should now be in scene A.
        rule.waitForIdle()
@@ -156,7 +158,8 @@ class SwipeToSceneTest {
        assertThat(transition.toScene).isEqualTo(TestScenes.SceneC)
        assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
        assertThat(transition.progress).isEqualTo(56.dp / LayoutHeight)
        assertThat(transition.isUserInputDriven).isTrue()
        assertThat(transition.isInitiatedByUserInput).isTrue()
        assertThat(transition.isUserInputOngoing).isTrue()

        // Release the finger. We should now be animating to C (currentScene = SceneC) given
        // that 56dp >= positional threshold.
@@ -168,7 +171,8 @@ class SwipeToSceneTest {
        assertThat(transition.toScene).isEqualTo(TestScenes.SceneC)
        assertThat(transition.currentScene).isEqualTo(TestScenes.SceneC)
        assertThat(transition.progress).isEqualTo(56.dp / LayoutHeight)
        assertThat(transition.isUserInputDriven).isTrue()
        assertThat(transition.isInitiatedByUserInput).isTrue()
        assertThat(transition.isUserInputOngoing).isFalse()

        // Wait for the animation to finish. We should now be in scene C.
        rule.waitForIdle()
Loading