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

Commit 1ce9e365 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Use DecayAnimationSpec to animate to SwipeAnimation targetOffset

This CL makes SwipeAnimation.animateOffset() to use a DecaySpecAnimation
rather than a normal target animation after releasing a drag gesture
if the decay animation is enough to get to the target offset. This makes
the animation consume less velocity and makes the transition overscroll
behave more consistently w.r.t. overscrolling after a nested scrollable
is flung.

Bug: 378470603
Test: atest PlatformComposeSceneTransitionLayoutTests
Flag: com.android.systemui.scene_container
Change-Id: I38152f770942bf04cad57f3d98acd971ebe63d4d
parent bc25f13a
Loading
Loading
Loading
Loading
+8 −1
Original line number Diff line number Diff line
@@ -127,7 +127,14 @@ internal class DraggableHandler(
                directionChangeSlop = layoutImpl.directionChangeSlop,
            )

        return createSwipeAnimation(layoutImpl, result, isUpOrLeft, orientation, gestureContext)
        return createSwipeAnimation(
            layoutImpl,
            result,
            isUpOrLeft,
            orientation,
            gestureContext,
            layoutImpl.decayAnimationSpec,
        )
    }

    private fun resolveSwipeSource(startedPosition: Offset): SwipeSource.Resolved? {
+1 −0
Original line number Diff line number Diff line
@@ -70,6 +70,7 @@ internal fun PredictiveBackHandler(
                distance = 1f,
                gestureContext =
                    ProvidedGestureContext(dragOffset = 0f, direction = InputDirection.Max),
                decayAnimationSpec = layoutImpl.decayAnimationSpec,
            )

        animateProgress(
+4 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.compose.animation.scene

import androidx.annotation.FloatRange
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.LocalOverscrollFactory
import androidx.compose.foundation.OverscrollEffect
import androidx.compose.foundation.OverscrollFactory
@@ -747,6 +748,7 @@ internal fun SceneTransitionLayoutForTesting(
    val layoutDirection = LocalLayoutDirection.current
    val defaultEffectFactory = checkNotNull(LocalOverscrollFactory.current)
    val animationScope = rememberCoroutineScope()
    val decayAnimationSpec = rememberSplineBasedDecay<Float>()
    val layoutImpl = remember {
        SceneTransitionLayoutImpl(
                state = state as MutableSceneTransitionLayoutStateImpl,
@@ -762,6 +764,7 @@ internal fun SceneTransitionLayoutForTesting(
                lookaheadScope = lookaheadScope,
                directionChangeSlop = directionChangeSlop,
                defaultEffectFactory = defaultEffectFactory,
                decayAnimationSpec = decayAnimationSpec,
            )
            .also { onLayoutImpl?.invoke(it) }
    }
@@ -801,6 +804,7 @@ internal fun SceneTransitionLayoutForTesting(
        layoutImpl.swipeSourceDetector = swipeSourceDetector
        layoutImpl.swipeDetector = swipeDetector
        layoutImpl.transitionInterceptionThreshold = transitionInterceptionThreshold
        layoutImpl.decayAnimationSpec = decayAnimationSpec
    }

    layoutImpl.Content(modifier)
+2 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.compose.animation.scene

import androidx.annotation.VisibleForTesting
import androidx.compose.animation.core.DecayAnimationSpec
import androidx.compose.foundation.OverscrollFactory
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
@@ -82,6 +83,7 @@ internal class SceneTransitionLayoutImpl(
    internal var swipeSourceDetector: SwipeSourceDetector,
    internal var swipeDetector: SwipeDetector,
    internal var transitionInterceptionThreshold: Float,
    internal var decayAnimationSpec: DecayAnimationSpec<Float>,
    builder: SceneTransitionLayoutScope<InternalContentScope>.() -> Unit,

    /**
+58 −12
Original line number Diff line number Diff line
@@ -21,6 +21,8 @@ package com.android.compose.animation.scene
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.DecayAnimationSpec
import androidx.compose.animation.core.calculateTargetValue
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.getValue
@@ -42,6 +44,7 @@ internal fun createSwipeAnimation(
    orientation: Orientation,
    distance: Float,
    gestureContext: MutableDragOffsetGestureContext,
    decayAnimationSpec: DecayAnimationSpec<Float>,
): SwipeAnimation<*> {
    return createSwipeAnimation(
        layoutState,
@@ -53,6 +56,7 @@ internal fun createSwipeAnimation(
            error("Computing contentForUserActions requires a SceneTransitionLayoutImpl")
        },
        gestureContext = gestureContext,
        decayAnimationSpec = decayAnimationSpec,
    )
}

@@ -62,6 +66,7 @@ internal fun createSwipeAnimation(
    isUpOrLeft: Boolean,
    orientation: Orientation,
    gestureContext: MutableDragOffsetGestureContext,
    decayAnimationSpec: DecayAnimationSpec<Float>,
    distance: Float = DistanceUnspecified,
): SwipeAnimation<*> {
    var lastDistance = distance
@@ -106,6 +111,7 @@ internal fun createSwipeAnimation(
        distance = ::distance,
        contentForUserActions = { layoutImpl.contentForUserActions().key },
        gestureContext = gestureContext,
        decayAnimationSpec = decayAnimationSpec,
    )
}

@@ -117,6 +123,7 @@ private fun createSwipeAnimation(
    distance: (SwipeAnimation<*>) -> Float,
    contentForUserActions: () -> ContentKey,
    gestureContext: MutableDragOffsetGestureContext,
    decayAnimationSpec: DecayAnimationSpec<Float>,
): SwipeAnimation<*> {
    fun <T : ContentKey> swipeAnimation(fromContent: T, toContent: T): SwipeAnimation<T> {
        return SwipeAnimation(
@@ -128,6 +135,7 @@ private fun createSwipeAnimation(
            requiresFullDistanceSwipe = result.requiresFullDistanceSwipe,
            distance = distance,
            gestureContext = gestureContext,
            decayAnimationSpec = decayAnimationSpec,
        )
    }

@@ -201,6 +209,7 @@ internal class SwipeAnimation<T : ContentKey>(
    private val distance: (SwipeAnimation<T>) -> Float,
    currentContent: T = fromContent,
    private val gestureContext: MutableDragOffsetGestureContext,
    private val decayAnimationSpec: DecayAnimationSpec<Float>,
) : MutableDragOffsetGestureContext by gestureContext {
    /** The [TransitionState.Transition] whose implementation delegates to this [SwipeAnimation]. */
    lateinit var contentTransition: TransitionState.Transition
@@ -367,20 +376,10 @@ internal class SwipeAnimation<T : ContentKey>(

        check(isAnimatingOffset())

        val motionSpatialSpec = spec ?: layoutState.motionScheme.defaultSpatialSpec()

        val velocityConsumed = CompletableDeferred<Float>()
        offsetAnimationRunnable.complete {
            val result =
                animatable.animateTo(
                    targetValue = targetOffset,
                    animationSpec = motionSpatialSpec,
                    initialVelocity = 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)
            val consumed = animateOffset(animatable, targetOffset, initialVelocity, spec)
            velocityConsumed.complete(consumed)

            // 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
@@ -391,6 +390,53 @@ internal class SwipeAnimation<T : ContentKey>(
        return velocityConsumed.await()
    }

    private suspend fun animateOffset(
        animatable: Animatable<Float, AnimationVector1D>,
        targetOffset: Float,
        initialVelocity: Float,
        spec: AnimationSpec<Float>?,
    ): Float {
        val initialOffset = animatable.value
        val decayOffset =
            decayAnimationSpec.calculateTargetValue(
                initialVelocity = initialVelocity,
                initialValue = initialOffset,
            )

        val willDecayReachBounds =
            when {
                targetOffset > initialOffset -> decayOffset >= targetOffset
                targetOffset < initialOffset -> decayOffset <= targetOffset
                else -> true
            }

        if (willDecayReachBounds) {
            val result = animatable.animateDecay(initialVelocity, decayAnimationSpec)
            check(animatable.value == targetOffset) {
                buildString {
                    appendLine(
                        "animatable.value = ${animatable.value} != $targetOffset = targetOffset"
                    )
                    appendLine("  initialOffset=$initialOffset")
                    appendLine("  targetOffset=$targetOffset")
                    appendLine("  initialVelocity=$initialVelocity")
                    appendLine("  decayOffset=$decayOffset")
                }
            }
            return initialVelocity - result.endState.velocity
        }

        val motionSpatialSpec = spec ?: layoutState.motionScheme.defaultSpatialSpec()
        animatable.animateTo(
            targetValue = targetOffset,
            animationSpec = motionSpatialSpec,
            initialVelocity = initialVelocity,
        )

        // We consumed the whole velocity.
        return initialVelocity
    }

    private fun canChangeContent(targetContent: ContentKey): Boolean {
        return when (val transition = contentTransition) {
            is TransitionState.Transition.ChangeScene ->
Loading