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

Commit 8b8e1317 authored by omarmt's avatar omarmt
Browse files

Overscroll DSL is also used when bouncing around the target scene

The Overscroll DSL is typically used when the progress value exceeds 1
or falls below 0. This change allows the same DSL to be used during the
bouncing animation, where the progress value fluctuates around the
target value.

Test: atest ElementTest
Bug: 327257459
Flag: NA
Change-Id: Icf6c904458dbb14bad37b73be249507dd55d9518
parent 4739ecf6
Loading
Loading
Loading
Loading
+24 −5
Original line number Diff line number Diff line
@@ -605,6 +605,8 @@ private class SwipeTransition(

    override val isInitiatedByUserInput = true

    override var bouncingScene: SceneKey? = null

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

@@ -694,14 +696,31 @@ private class SwipeTransition(
    ): OffsetAnimation {
        return startOffsetAnimation {
            val animatable = Animatable(dragOffset, OffsetVisibilityThreshold)
            val isTargetGreater = targetOffset > animatable.value
            val job =
                coroutineScope
                    .launch {
                        try {
                            animatable.animateTo(
                                targetValue = targetOffset,
                                animationSpec = swipeSpec,
                                initialVelocity = initialVelocity,
                        )
                            ) {
                                if (bouncingScene == null) {
                                    val isBouncing =
                                        if (isTargetGreater) {
                                            value > targetOffset
                                        } else {
                                            value < targetOffset
                                        }
                                    if (isBouncing) {
                                        bouncingScene = targetScene
                                    }
                                }
                            }
                        } finally {
                            bouncingScene = null
                        }
                    }
                    // Make sure that we settle to target scene at the end of the animation or if
                    // the animation is cancelled.
+2 −1
Original line number Diff line number Diff line
@@ -588,7 +588,8 @@ private inline fun <T> computeValue(
            // TODO(b/290184746): Make sure that we don't overflow transformations associated to a
            // range.
            val directionSign = if (transition.isUpOrLeft) -1 else 1
            val overscrollProgress = transition.progress.let { if (it > 1f) it - 1f else it }
            val isToScene = overscroll.scene == transition.toScene
            val overscrollProgress = transition.progress.let { if (isToScene) it - 1f else it }
            val progress = directionSign * overscrollProgress
            val rangeProgress = propertySpec.range?.progress(progress) ?: progress

+9 −2
Original line number Diff line number Diff line
@@ -255,6 +255,12 @@ sealed interface TransitionState {
         */
        val overscrollScope: OverscrollScope

        /**
         * The scene around which the transition is currently bouncing. When not `null`, this
         * transition is currently oscillating around this scene and will soon settle to that scene.
         */
        val bouncingScene: SceneKey?

        companion object {
            const val DistanceUnspecified = 0f
        }
@@ -287,9 +293,10 @@ internal abstract class BaseSceneTransitionLayoutState(
            val transition = currentTransition ?: return null
            if (transition !is TransitionState.HasOverscrollProperties) return null
            val progress = transition.progress
            val bouncingScene = transition.bouncingScene
            return when {
                progress < 0f -> fromOverscrollSpec
                progress > 1f -> toOverscrollSpec
                progress < 0f || bouncingScene == transition.fromScene -> fromOverscrollSpec
                progress > 1f || bouncingScene == transition.toScene -> toOverscrollSpec
                else -> null
            }
        }
+53 −0
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.compose.animation.scene

import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation
@@ -752,4 +754,55 @@ class ElementTest {
        assertThat(state.currentOverscrollSpec).isNotNull()
        fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 1.5f)
    }

    @Test
    fun elementTransitionWithDistanceDuringOverscrollBouncing() {
        val layoutWidth = 200.dp
        val layoutHeight = 400.dp
        val state =
            setupOverscrollScenario(
                layoutWidth = layoutWidth,
                layoutHeight = layoutHeight,
                sceneTransitions = {
                    defaultSwipeSpec =
                        spring(
                            dampingRatio = Spring.DampingRatioMediumBouncy,
                            stiffness = Spring.StiffnessLow,
                        )

                    overscroll(TestScenes.SceneB, Orientation.Vertical) {
                        // On overscroll 100% -> Foo should translate by layoutHeight
                        translate(TestElements.Foo, y = { absoluteDistance })
                    }
                },
                firstScroll = 1f, // 100% scroll
            )

        val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true)
        fooElement.assertTopPositionInRootIsEqualTo(0.dp)

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

        val transition = state.currentTransition
        assertThat(transition).isNotNull()
        transition as TransitionState.HasOverscrollProperties

        // Scroll 150% (100% scroll + 50% overscroll)
        assertThat(transition.progress).isEqualTo(1.5f)
        assertThat(state.currentOverscrollSpec).isNotNull()
        fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * (transition.progress - 1f))

        // finger raised
        rule.onRoot().performTouchInput { up() }

        // The target value is 1f, but the spring (defaultSwipeSpec) allows you to go to a lower
        // value.
        rule.waitUntil(timeoutMillis = 10_000) { transition.progress < 1f }

        assertThat(state.currentOverscrollSpec).isNotNull()
        assertThat(transition.bouncingScene).isEqualTo(transition.toScene)
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ fun transition(
    isInitiatedByUserInput: Boolean = false,
    isUserInputOngoing: Boolean = false,
    isUpOrLeft: Boolean = false,
    bouncingScene: SceneKey? = null,
    orientation: Orientation = Orientation.Horizontal,
): TransitionState.Transition {
    return object : TransitionState.Transition(from, to), TransitionState.HasOverscrollProperties {
@@ -37,6 +38,7 @@ fun transition(
        override val isInitiatedByUserInput: Boolean = isInitiatedByUserInput
        override val isUserInputOngoing: Boolean = isUserInputOngoing
        override val isUpOrLeft: Boolean = isUpOrLeft
        override val bouncingScene: SceneKey? = bouncingScene
        override val orientation: Orientation = orientation
        override val overscrollScope: OverscrollScope =
            object : OverscrollScope {