diff options
author | 2025-01-07 07:55:15 -0800 | |
---|---|---|
committer | 2025-01-07 07:55:15 -0800 | |
commit | 9d1d2d2ca6fdd2c18ac4b3cd18b893cb5059e7a9 (patch) | |
tree | ee51d1290a653ba670533fe113bc2cb098d433b4 | |
parent | 9b3e41f48bdda50df692e5c9ec402ea565ad680e (diff) | |
parent | d85f0ba84540ede46752442ed2383cb7453b469e (diff) |
Merge "STL SwipeAnimation progress bounded between 0 and 1" into main
2 files changed, 66 insertions, 37 deletions
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 607e4fadc256..ba92f9bea07d 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 @@ -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 - } + val distance = 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) - } + ) - // 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() - } + // 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 -> diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt index 7c8c6e5f6c12..e580e3c40690 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt @@ -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) + } } |