Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt +24 −37 Original line number Diff line number Diff line Loading @@ -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 } 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 Loading @@ -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()) Loading @@ -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) } ) // 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 -> Loading packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt +42 −0 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading Loading @@ -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) } } Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt +24 −37 Original line number Diff line number Diff line Loading @@ -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 } 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 Loading @@ -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()) Loading @@ -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) } ) // 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 -> Loading
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt +42 −0 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading Loading @@ -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) } }