diff options
| -rw-r--r-- | packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SwipeToScene.kt | 197 |
1 files changed, 147 insertions, 50 deletions
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SwipeToScene.kt index 2f2b30c2ef3c..2069ebd32b81 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SwipeToScene.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SwipeToScene.kt @@ -257,35 +257,18 @@ private fun onDrag( // twice in a row to accelerate the transition and go from A => B then B => C really fast. maybeHandleAcceleratedSwipe(transition, orientation) - val fromScene = transition._fromScene - val upOrLeft = fromScene.upOrLeft(orientation) - val downOrRight = fromScene.downOrRight(orientation) val offset = transition.dragOffset + val fromScene = transition._fromScene // Compute the target scene depending on the current offset. - val targetSceneKey: SceneKey - val signedDistance: Float - when { - offset < 0f && upOrLeft != null -> { - targetSceneKey = upOrLeft - signedDistance = -transition.absoluteDistance - } - offset > 0f && downOrRight != null -> { - targetSceneKey = downOrRight - signedDistance = transition.absoluteDistance - } - else -> { - targetSceneKey = fromScene.key - signedDistance = 0f - } - } + val target = fromScene.findTargetSceneAndDistance(orientation, offset, layoutImpl) - if (transition._toScene.key != targetSceneKey) { - transition._toScene = layoutImpl.scenes.getValue(targetSceneKey) + if (transition._toScene.key != target.sceneKey) { + transition._toScene = layoutImpl.scenes.getValue(target.sceneKey) } - if (transition._distance != signedDistance) { - transition._distance = signedDistance + if (transition._distance != target.distance) { + transition._distance = target.distance } } @@ -321,6 +304,48 @@ private fun maybeHandleAcceleratedSwipe( // using fromScene and dragOffset. } +private data class TargetScene( + val sceneKey: SceneKey, + val distance: Float, +) + +private fun Scene.findTargetSceneAndDistance( + orientation: Orientation, + directionOffset: Float, + layoutImpl: SceneTransitionLayoutImpl, +): TargetScene { + val maxDistance = + when (orientation) { + Orientation.Horizontal -> layoutImpl.size.width + Orientation.Vertical -> layoutImpl.size.height + }.toFloat() + + val upOrLeft = upOrLeft(orientation) + val downOrRight = downOrRight(orientation) + + // Compute the target scene depending on the current offset. + return when { + directionOffset < 0f && upOrLeft != null -> { + TargetScene( + sceneKey = upOrLeft, + distance = -maxDistance, + ) + } + directionOffset > 0f && downOrRight != null -> { + TargetScene( + sceneKey = downOrRight, + distance = maxDistance, + ) + } + else -> { + TargetScene( + sceneKey = key, + distance = 0f, + ) + } + } +} + private fun CoroutineScope.onDragStopped( layoutImpl: SceneTransitionLayoutImpl, transition: SwipeTransition, @@ -372,31 +397,13 @@ private fun CoroutineScope.onDragStopped( layoutImpl.onChangeScene(targetScene.key) } - // Animate the offset. - transition.offsetAnimationJob = launch { - transition.offsetAnimatable.snapTo(offset) - transition.isAnimatingOffset = true - - transition.offsetAnimatable.animateTo( - targetOffset, - // TODO(b/290184746): Make this spring spec configurable. - spring( - stiffness = Spring.StiffnessMediumLow, - visibilityThreshold = OffsetVisibilityThreshold - ), - initialVelocity = velocity, - ) - - // Now that the animation is done, the state should be idle. Note that if the state was - // changed since this animation started, some external code changed it and we shouldn't do - // anything here. Note also that this job will be cancelled in the case where the user - // intercepts this swipe. - if (layoutImpl.state.transitionState == transition) { - layoutImpl.state.transitionState = TransitionState.Idle(targetScene.key) - } - - transition.offsetAnimationJob = null - } + animateOffset( + transition = transition, + layoutImpl = layoutImpl, + initialVelocity = velocity, + targetOffset = targetOffset, + targetScene = targetScene.key + ) } /** @@ -436,6 +443,90 @@ private fun shouldCommitSwipe( } } +private fun CoroutineScope.animateOffset( + transition: SwipeTransition, + layoutImpl: SceneTransitionLayoutImpl, + initialVelocity: Float, + targetOffset: Float, + targetScene: SceneKey, +) { + transition.offsetAnimationJob = launch { + if (!transition.isAnimatingOffset) { + transition.offsetAnimatable.snapTo(transition.dragOffset) + } + transition.isAnimatingOffset = true + + transition.offsetAnimatable.animateTo( + targetOffset, + // TODO(b/290184746): Make this spring spec configurable. + spring( + stiffness = Spring.StiffnessMediumLow, + visibilityThreshold = OffsetVisibilityThreshold + ), + initialVelocity = initialVelocity, + ) + + // Now that the animation is done, the state should be idle. Note that if the state was + // changed since this animation started, some external code changed it and we shouldn't do + // anything here. Note also that this job will be cancelled in the case where the user + // intercepts this swipe. + if (layoutImpl.state.transitionState == transition) { + layoutImpl.state.transitionState = TransitionState.Idle(targetScene) + } + + transition.offsetAnimationJob = null + } +} + +private fun CoroutineScope.animateOverscroll( + layoutImpl: SceneTransitionLayoutImpl, + transition: SwipeTransition, + velocity: Velocity, + orientation: Orientation, +): Velocity { + val velocityAmount = + when (orientation) { + Orientation.Vertical -> velocity.y + Orientation.Horizontal -> velocity.x + } + + if (velocityAmount == 0f) { + // There is no remaining velocity + return Velocity.Zero + } + + val fromScene = layoutImpl.scene(layoutImpl.state.transitionState.currentScene) + val target = fromScene.findTargetSceneAndDistance(orientation, velocityAmount, layoutImpl) + val isValidTarget = target.distance != 0f && target.sceneKey != fromScene.key + + if (!isValidTarget || layoutImpl.state.transitionState == transition) { + // We have not found a valid target or we are already in a transition + return Velocity.Zero + } + + transition._currentScene = fromScene + transition._fromScene = fromScene + transition._toScene = layoutImpl.scene(target.sceneKey) + transition._distance = target.distance + transition.absoluteDistance = target.distance.absoluteValue + transition.dragOffset = 0f + transition.isAnimatingOffset = false + transition.offsetAnimationJob = null + + layoutImpl.state.transitionState = transition + + animateOffset( + transition = transition, + layoutImpl = layoutImpl, + initialVelocity = velocityAmount, + targetOffset = 0f, + targetScene = fromScene.key + ) + + // The animateOffset animation consumes any remaining velocity. + return velocity +} + /** * The number of pixels below which there won't be a visible difference in the transition and from * which the animation can stop. @@ -543,8 +634,14 @@ private fun rememberSwipeToSceneNestedScrollConnection( velocityAvailable }, onPostFling = { velocityAvailable -> - // We will handle the overscroll here - Velocity.Zero + // If there is any velocity left, we can try running an overscroll animation + // between scenes. + coroutineScope.animateOverscroll( + layoutImpl = layoutImpl, + transition = transition, + velocity = velocityAvailable, + orientation = orientation + ) }, ) } |