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

Commit d85f0ba8 authored by omarmt's avatar omarmt
Browse files

STL SwipeAnimation progress bounded between 0 and 1

Updates SwipeAnimation to explicitly bound the progress value between 0
and 1, ensuring that the animation behaves as expected even when the
user swipes beyond the target.

Test: atest SceneTransitionLayoutTest
Bug: 378470603
Flag: com.android.systemui.scene_container
Change-Id: I454e7e935a952de412e9bb1fe84736fe6f3af894
parent 74c4087d
Loading
Loading
Loading
Loading
+24 −37
Original line number Diff line number Diff line
@@ -315,16 +315,10 @@ internal class SwipeAnimation<T : ContentKey>(
        val skipAnimation =
            hasReachedTargetContent && !contentTransition.isWithinProgressRange(initialProgress)

        val targetOffset =
            if (targetContent == fromContent) {
                0f
            } else {
        val distance = distance()
                check(distance != DistanceUnspecified) {
                    "distance is equal to $DistanceUnspecified"
                }
                distance
            }
        check(distance != DistanceUnspecified) { "distance is equal to $DistanceUnspecified" }

        val targetOffset = if (targetContent == fromContent) 0f else distance

        // If the effective current content changed, it should be reflected right now in the
        // current state, even before the settle animation is ongoing. That way all the
@@ -343,7 +337,16 @@ internal class SwipeAnimation<T : ContentKey>(
            }

        val animatable =
            Animatable(initialOffset, OffsetVisibilityThreshold).also { offsetAnimation = it }
            Animatable(initialOffset, OffsetVisibilityThreshold).also {
                offsetAnimation = it

                // We should animate when the progress value is between [0, 1].
                if (distance > 0) {
                    it.updateBounds(0f, distance)
                } else {
                    it.updateBounds(distance, 0f)
                }
            }

        check(isAnimatingOffset())

@@ -370,42 +373,26 @@ internal class SwipeAnimation<T : ContentKey>(
        val velocityConsumed = CompletableDeferred<Float>()

        offsetAnimationRunnable.complete {
            try {
            val result =
                animatable.animateTo(
                    targetValue = targetOffset,
                    animationSpec = swipeSpec,
                    initialVelocity = initialVelocity,
                ) {
                    // Immediately stop this transition if we are bouncing on a content that
                    // does not bounce.
                    if (!contentTransition.isWithinProgressRange(progress)) {
                        // We are no longer able to consume the velocity, the rest can be
                        // consumed by another component in the hierarchy.
                        velocityConsumed.complete(initialVelocity - velocity)
                        throw SnapException()
                    }
                }
            } catch (_: SnapException) {
                /* Ignore. */
            } finally {
                if (!velocityConsumed.isCompleted) {
                    // The animation consumed the whole available velocity
                    velocityConsumed.complete(initialVelocity)
                }
                )

            // We are no longer able to consume the velocity, the rest can be consumed by another
            // component in the hierarchy.
            velocityConsumed.complete(initialVelocity - result.endState.velocity)

            // Wait for overscroll to finish so that the transition is removed from the STLState
            // only after the overscroll is done, to avoid dropping frame right when the user
            // lifts their finger and overscroll is animated to 0.
            overscrollCompletable?.await()
        }
        }

        return velocityConsumed.await()
    }

    /** An exception thrown during the animation to stop it immediately. */
    private class SnapException : Exception()

    private fun canChangeContent(targetContent: ContentKey): Boolean {
        return when (val transition = contentTransition) {
            is TransitionState.Transition.ChangeScene ->
+42 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
@@ -33,6 +34,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertHeightIsEqualTo
@@ -43,6 +45,9 @@ import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onChild
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeDown
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.IntOffset
@@ -469,4 +474,41 @@ class SceneTransitionLayoutTest {

        assertThat(layoutImpl.overlaysOrNullForTest()).isNull()
    }

    @Test
    fun transitionProgressBoundedBetween0And1() {
        val layoutWidth = 200.dp
        val layoutHeight = 400.dp

        // 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 state = rule.runOnUiThread { MutableSceneTransitionLayoutState(initialScene = SceneA) }
        rule.setContent {
            touchSlop = LocalViewConfiguration.current.touchSlop
            SceneTransitionLayout(state, Modifier.size(layoutWidth, layoutHeight)) {
                scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
                    Spacer(Modifier.fillMaxSize())
                }
                scene(SceneB) { Spacer(Modifier.fillMaxSize()) }
            }
        }
        assertThat(state.transitionState).isIdle()

        rule.mainClock.autoAdvance = false

        // Swipe the verticalSwipeDistance.
        rule.onRoot().performTouchInput {
            swipeDown(endY = bottom + touchSlop, durationMillis = 50)
        }

        rule.mainClock.advanceTimeBy(16)
        val transition = assertThat(state.transitionState).isSceneTransition()
        assertThat(transition).isNotNull()
        assertThat(transition).hasProgress(1f, tolerance = 0.01f)

        rule.mainClock.advanceTimeBy(16)
        // Fling animation, we are overscrolling now. Progress should always be between [0, 1].
        assertThat(transition).hasProgress(1f)
    }
}