diff options
6 files changed, 79 insertions, 14 deletions
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index ab3f6396e5c0..70ff47baa7a9 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -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? { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt index eee27b75a999..41279d338896 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt @@ -70,6 +70,7 @@ internal fun PredictiveBackHandler( distance = 1f, gestureContext = ProvidedGestureContext(dragOffset = 0f, direction = InputDirection.Max), + decayAnimationSpec = layoutImpl.decayAnimationSpec, ) animateProgress( diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index f6d40eef53a3..72bb82bd41bb 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -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) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index 585da0633131..53996d25afdb 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -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, /** diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt index 3bd37ad018b0..cb0d33cf5205 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt @@ -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 -> diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt index 6b439980cc68..969003cb92f3 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt @@ -18,7 +18,9 @@ package com.android.compose.animation.scene +import androidx.compose.animation.SplineBasedFloatDecayAnimationSpec import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.generateDecayAnimationSpec import androidx.compose.animation.core.spring import androidx.compose.foundation.overscroll import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -119,10 +121,11 @@ class DraggableHandlerTest { val transitionInterceptionThreshold = 0.05f val directionChangeSlop = 10f + private val density = Density(1f) private val layoutImpl = SceneTransitionLayoutImpl( state = layoutState, - density = Density(1f), + density = density, layoutDirection = LayoutDirection.Ltr, swipeSourceDetector = DefaultEdgeDetector, swipeDetector = DefaultSwipeDetector, @@ -134,6 +137,8 @@ class DraggableHandlerTest { animationScope = testScope, directionChangeSlop = directionChangeSlop, defaultEffectFactory = defaultEffectFactory, + decayAnimationSpec = + SplineBasedFloatDecayAnimationSpec(density).generateDecayAnimationSpec(), ) .apply { setContentsAndLayoutTargetSizeForTest(LAYOUT_SIZE) } |