Merge "Refactor and add new SceneGestureHandler tests" into main
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
index 49ef31b..b84cb36 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
@@ -65,30 +65,53 @@
) {
Text("SceneA")
}
- scene(SceneB) { Text("SceneB") }
- scene(SceneC) { Text("SceneC") }
+ scene(
+ key = SceneB,
+ userActions = mapOf(Swipe.Up to SceneC, Swipe.Down to SceneA),
+ ) {
+ Text("SceneB")
+ }
+ scene(
+ key = SceneC,
+ userActions =
+ mapOf(
+ Swipe.Up to SceneB,
+ Swipe(SwipeDirection.Up, fromEdge = Edge.Bottom) to SceneA
+ ),
+ ) {
+ Text("SceneC")
+ }
}
val transitionInterceptionThreshold = 0.05f
+ private val layoutImpl =
+ SceneTransitionLayoutImpl(
+ onChangeScene = { internalCurrentScene = it },
+ builder = scenesBuilder,
+ transitions = EmptyTestTransitions,
+ state = layoutState,
+ density = Density(1f),
+ edgeDetector = DefaultEdgeDetector,
+ transitionInterceptionThreshold = transitionInterceptionThreshold,
+ coroutineScope = coroutineScope,
+ )
+ .apply { setScenesTargetSizeForTest(LAYOUT_SIZE) }
+
val sceneGestureHandler =
SceneGestureHandler(
- layoutImpl =
- SceneTransitionLayoutImpl(
- onChangeScene = { internalCurrentScene = it },
- builder = scenesBuilder,
- transitions = EmptyTestTransitions,
- state = layoutState,
- density = Density(1f),
- edgeDetector = DefaultEdgeDetector,
- transitionInterceptionThreshold = transitionInterceptionThreshold,
- coroutineScope = coroutineScope,
- )
- .apply { setScenesTargetSizeForTest(LAYOUT_SIZE) },
+ layoutImpl = layoutImpl,
orientation = Orientation.Vertical,
coroutineScope = coroutineScope,
)
+ val horizontalSceneGestureHandler =
+ SceneGestureHandler(
+ layoutImpl = layoutImpl,
+ orientation = Orientation.Horizontal,
+ coroutineScope = coroutineScope,
+ )
+
val draggable = sceneGestureHandler.draggable
fun nestedScrollConnection(nestedScrollBehavior: NestedScrollBehavior) =
@@ -101,11 +124,17 @@
val velocityThreshold = sceneGestureHandler.velocityThreshold
- // 10% of the screen
- val deltaInPixels10 = SCREEN_SIZE * 0.1f
+ fun down(fractionOfScreen: Float) =
+ if (fractionOfScreen < 0f) error("use up()") else SCREEN_SIZE * fractionOfScreen
+
+ fun up(fractionOfScreen: Float) =
+ if (fractionOfScreen < 0f) error("use down()") else -down(fractionOfScreen)
+
+ // Float tolerance for comparisons
+ val tolerance = 0.00001f
// Offset y: 10% of the screen
- val offsetY10 = Offset(x = 0f, y = deltaInPixels10)
+ val offsetY10 = Offset(x = 0f, y = down(0.1f))
val transitionState: TransitionState
get() = layoutState.transitionState
@@ -121,12 +150,39 @@
coroutineScope.testScheduler.runCurrent()
}
- fun assertScene(currentScene: SceneKey, isIdle: Boolean) {
- val idleMsg = if (isIdle) "MUST" else "MUST NOT"
- assertWithMessage("transitionState $idleMsg be Idle")
- .that(transitionState is Idle)
- .isEqualTo(isIdle)
- assertThat(transitionState.currentScene).isEqualTo(currentScene)
+ fun assertIdle(currentScene: SceneKey) {
+ assertWithMessage("transitionState must be Idle").that(transitionState is Idle).isTrue()
+ assertWithMessage("currentScene does not match")
+ .that(transitionState.currentScene)
+ .isEqualTo(currentScene)
+ }
+
+ fun assertTransition(
+ currentScene: SceneKey? = null,
+ fromScene: SceneKey? = null,
+ toScene: SceneKey? = null,
+ progress: Float? = null,
+ ) {
+ assertWithMessage("transitionState must be Transition")
+ .that(transitionState is Transition)
+ .isTrue()
+ if (currentScene != null)
+ assertWithMessage("currentScene does not match")
+ .that(transitionState.currentScene)
+ .isEqualTo(currentScene)
+ if (fromScene != null)
+ assertWithMessage("fromScene does not match")
+ .that((transitionState as? Transition)?.fromScene)
+ .isEqualTo(fromScene)
+ if (toScene != null)
+ assertWithMessage("toScene does not match")
+ .that((transitionState as? Transition)?.toScene)
+ .isEqualTo(toScene)
+ if (progress != null)
+ assertWithMessage("progress does not match")
+ .that((transitionState as? Transition)?.progress)
+ .isWithin(tolerance)
+ .of(progress)
}
}
@@ -135,82 +191,148 @@
runMonotonicClockTest { TestGestureScope(coroutineScope = this).block() }
}
- private fun DraggableHandler.onDragStarted() =
+ private fun DraggableHandler.onDragStarted() {
onDragStarted(layoutSize = LAYOUT_SIZE, startedPosition = Offset.Zero)
+ }
- @Test
- fun testPreconditions() = runGestureTest { assertScene(currentScene = SceneA, isIdle = true) }
+ @Test fun testPreconditions() = runGestureTest { assertIdle(currentScene = SceneA) }
@Test
fun onDragStarted_shouldStartATransition() = runGestureTest {
draggable.onDragStarted()
- assertScene(currentScene = SceneA, isIdle = false)
+ assertTransition(currentScene = SceneA)
}
@Test
fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest {
draggable.onDragStarted()
- assertScene(currentScene = SceneA, isIdle = false)
+ assertTransition(currentScene = SceneA)
- draggable.onDelta(pixels = deltaInPixels10)
+ draggable.onDelta(pixels = down(0.1f))
assertThat(progress).isEqualTo(0.1f)
- draggable.onDelta(pixels = deltaInPixels10)
+ draggable.onDelta(pixels = down(0.1f))
assertThat(progress).isEqualTo(0.2f)
}
@Test
fun onDragStoppedAfterDrag_velocityLowerThanThreshold_remainSameScene() = runGestureTest {
draggable.onDragStarted()
- assertScene(currentScene = SceneA, isIdle = false)
+ assertTransition(currentScene = SceneA)
- draggable.onDelta(pixels = deltaInPixels10)
- assertScene(currentScene = SceneA, isIdle = false)
+ draggable.onDelta(pixels = down(0.1f))
+ assertTransition(currentScene = SceneA)
draggable.onDragStopped(
velocity = velocityThreshold - 0.01f,
)
- assertScene(currentScene = SceneA, isIdle = false)
+ assertTransition(currentScene = SceneA)
// wait for the stop animation
advanceUntilIdle()
- assertScene(currentScene = SceneA, isIdle = true)
+ assertIdle(currentScene = SceneA)
}
@Test
fun onDragStoppedAfterDrag_velocityAtLeastThreshold_goToNextScene() = runGestureTest {
draggable.onDragStarted()
- assertScene(currentScene = SceneA, isIdle = false)
+ assertTransition(currentScene = SceneA)
- draggable.onDelta(pixels = deltaInPixels10)
- assertScene(currentScene = SceneA, isIdle = false)
+ draggable.onDelta(pixels = down(0.1f))
+ assertTransition(currentScene = SceneA)
- draggable.onDragStopped(
- velocity = velocityThreshold,
- )
- assertScene(currentScene = SceneC, isIdle = false)
+ draggable.onDragStopped(velocity = velocityThreshold)
+
+ assertTransition(currentScene = SceneC)
// wait for the stop animation
advanceUntilIdle()
- assertScene(currentScene = SceneC, isIdle = true)
+ assertIdle(currentScene = SceneC)
}
@Test
- fun onDragStoppedAfterStarted_returnImmediatelyToIdle() = runGestureTest {
+ fun onDragStoppedAfterStarted_returnToIdle() = runGestureTest {
draggable.onDragStarted()
- assertScene(currentScene = SceneA, isIdle = false)
+ assertTransition(currentScene = SceneA)
draggable.onDragStopped(velocity = 0f)
- assertScene(currentScene = SceneA, isIdle = true)
+ advanceUntilIdle()
+ assertIdle(currentScene = SceneA)
+ }
+
+ @Test
+ fun onDragReversedDirection_changeToScene() = runGestureTest {
+ // Drag A -> B with progress 0.6
+ draggable.onDragStarted()
+ draggable.onDelta(up(0.6f))
+ assertTransition(
+ currentScene = SceneA,
+ fromScene = SceneA,
+ toScene = SceneB,
+ progress = 0.6f
+ )
+
+ // Reverse direction such that A -> C now with 0.4
+ draggable.onDelta(down(1f))
+ assertTransition(
+ currentScene = SceneA,
+ fromScene = SceneA,
+ toScene = SceneC,
+ progress = 0.4f
+ )
+
+ // After the drag stopped scene C should be committed
+ draggable.onDragStopped(velocity = velocityThreshold)
+ assertTransition(currentScene = SceneC, fromScene = SceneA, toScene = SceneC)
+
+ // wait for the stop animation
+ advanceUntilIdle()
+ assertIdle(currentScene = SceneC)
+ }
+
+ @Test
+ fun onAccelaratedScroll_scrollToThirdScene() = runGestureTest {
+ // Drag A -> B with progress 0.2
+ draggable.onDragStarted()
+ draggable.onDelta(up(0.2f))
+ assertTransition(
+ currentScene = SceneA,
+ fromScene = SceneA,
+ toScene = SceneB,
+ progress = 0.2f
+ )
+
+ // Start animation A -> B with progress 0.2 -> 1.0
+ draggable.onDragStopped(velocity = -velocityThreshold)
+ assertTransition(currentScene = SceneB, fromScene = SceneA, toScene = SceneB)
+
+ // While at A -> B do a 100% screen drag (progress 1.2). This should go past B and change
+ // the transition to B -> C with progress 0.2
+ draggable.onDragStarted()
+ draggable.onDelta(up(1f))
+ assertTransition(
+ currentScene = SceneB,
+ fromScene = SceneB,
+ toScene = SceneC,
+ progress = 0.2f
+ )
+
+ // After the drag stopped scene C should be committed
+ draggable.onDragStopped(velocity = -velocityThreshold)
+ assertTransition(currentScene = SceneC, fromScene = SceneB, toScene = SceneC)
+
+ // wait for the stop animation
+ advanceUntilIdle()
+ assertIdle(currentScene = SceneC)
}
@Test
fun startGestureDuringAnimatingOffset_shouldImmediatelyStopTheAnimation() = runGestureTest {
draggable.onDragStarted()
- assertScene(currentScene = SceneA, isIdle = false)
+ assertTransition(currentScene = SceneA)
- draggable.onDelta(pixels = deltaInPixels10)
- assertScene(currentScene = SceneA, isIdle = false)
+ draggable.onDelta(pixels = down(0.1f))
+ assertTransition(currentScene = SceneA)
draggable.onDragStopped(
velocity = velocityThreshold,
@@ -223,7 +345,7 @@
assertThat(sceneGestureHandler.isAnimatingOffset).isTrue()
assertThat(sceneGestureHandler.isDrivingTransition).isTrue()
- assertScene(currentScene = SceneC, isIdle = false)
+ assertTransition(currentScene = SceneC)
// Start a new gesture while the offset is animating
draggable.onDragStarted()
@@ -234,7 +356,7 @@
fun onInitialPreScroll_EdgeWithOverscroll_doNotChangeState() = runGestureTest {
val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithOverscroll)
nestedScroll.onPreScroll(available = offsetY10, source = NestedScrollSource.Drag)
- assertScene(currentScene = SceneA, isIdle = true)
+ assertIdle(currentScene = SceneA)
}
@Test
@@ -247,7 +369,7 @@
source = NestedScrollSource.Drag
)
- assertScene(currentScene = SceneA, isIdle = true)
+ assertIdle(currentScene = SceneA)
assertThat(consumed).isEqualTo(Offset.Zero)
}
@@ -261,7 +383,7 @@
source = NestedScrollSource.Drag
)
- assertScene(currentScene = SceneA, isIdle = false)
+ assertTransition(currentScene = SceneA)
assertThat(progress).isEqualTo(0.1f)
assertThat(consumed).isEqualTo(offsetY10)
}
@@ -284,7 +406,7 @@
fun afterSceneTransitionIsStarted_interceptPreScrollEvents() = runGestureTest {
val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithOverscroll)
nestedScroll.scroll(available = offsetY10)
- assertScene(currentScene = SceneA, isIdle = false)
+ assertTransition(currentScene = SceneA)
assertThat(progress).isEqualTo(0.1f)
@@ -303,7 +425,7 @@
nestedScroll.scroll(available = offsetY10)
assertThat(progress).isEqualTo(0.3f)
- assertScene(currentScene = SceneA, isIdle = false)
+ assertTransition(currentScene = SceneA)
}
private suspend fun TestGestureScope.preScrollAfterSceneTransition(
@@ -321,9 +443,6 @@
nestedScroll.onPreScroll(Offset(0f, SCREEN_SIZE * secondScroll), NestedScrollSource.Drag)
}
- // Float tolerance for comparisons
- private val tolerance = 0.00001f
-
@Test
fun scrollAndFling_scrollLessThanInterceptable_goToIdleOnCurrentScene() = runGestureTest {
val first = transitionInterceptionThreshold - tolerance
@@ -331,7 +450,7 @@
preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)
- assertScene(SceneA, isIdle = true)
+ assertIdle(SceneA)
}
@Test
@@ -341,7 +460,7 @@
preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)
- assertThat(progress).isWithin(tolerance).of(first + second)
+ assertTransition(progress = first + second)
}
@Test
@@ -351,7 +470,7 @@
preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)
- assertThat(progress).isWithin(tolerance).of(first + second)
+ assertTransition(progress = first + second)
}
@Test
@@ -361,21 +480,21 @@
preScrollAfterSceneTransition(firstScroll = first, secondScroll = second)
- assertScene(SceneC, isIdle = true)
+ assertIdle(SceneC)
}
@Test
fun onPreFling_velocityLowerThanThreshold_remainSameScene() = runGestureTest {
val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithOverscroll)
nestedScroll.scroll(available = offsetY10)
- assertScene(currentScene = SceneA, isIdle = false)
+ assertTransition(currentScene = SceneA)
nestedScroll.onPreFling(available = Velocity.Zero)
- assertScene(currentScene = SceneA, isIdle = false)
+ assertTransition(currentScene = SceneA)
// wait for the stop animation
advanceUntilIdle()
- assertScene(currentScene = SceneA, isIdle = true)
+ assertIdle(currentScene = SceneA)
}
private suspend fun TestGestureScope.flingAfterScroll(
@@ -384,7 +503,7 @@
) {
val nestedScroll = nestedScrollConnection(nestedScrollBehavior = use)
nestedScroll.scroll(available = offsetY10)
- assertScene(currentScene = SceneA, isIdle = idleAfterScroll)
+ if (idleAfterScroll) assertIdle(SceneA) else assertTransition(SceneA)
nestedScroll.onPreFling(available = Velocity(0f, velocityThreshold))
}
@@ -393,40 +512,40 @@
fun flingAfterScroll_DuringTransitionBetweenScenes_doNothing() = runGestureTest {
flingAfterScroll(use = DuringTransitionBetweenScenes, idleAfterScroll = true)
- assertScene(currentScene = SceneA, isIdle = true)
+ assertIdle(currentScene = SceneA)
}
@Test
fun flingAfterScroll_EdgeNoOverscroll_goToNextScene() = runGestureTest {
flingAfterScroll(use = EdgeNoOverscroll, idleAfterScroll = false)
- assertScene(currentScene = SceneC, isIdle = false)
+ assertTransition(currentScene = SceneC)
// wait for the stop animation
advanceUntilIdle()
- assertScene(currentScene = SceneC, isIdle = true)
+ assertIdle(currentScene = SceneC)
}
@Test
fun flingAfterScroll_EdgeWithOverscroll_goToNextScene() = runGestureTest {
flingAfterScroll(use = EdgeWithOverscroll, idleAfterScroll = false)
- assertScene(currentScene = SceneC, isIdle = false)
+ assertTransition(currentScene = SceneC)
// wait for the stop animation
advanceUntilIdle()
- assertScene(currentScene = SceneC, isIdle = true)
+ assertIdle(currentScene = SceneC)
}
@Test
fun flingAfterScroll_Always_goToNextScene() = runGestureTest {
flingAfterScroll(use = Always, idleAfterScroll = false)
- assertScene(currentScene = SceneC, isIdle = false)
+ assertTransition(currentScene = SceneC)
// wait for the stop animation
advanceUntilIdle()
- assertScene(currentScene = SceneC, isIdle = true)
+ assertIdle(currentScene = SceneC)
}
/** we started the scroll in the scene, then fling with the velocityThreshold */
@@ -440,7 +559,7 @@
// scroll offsetY10 is all available for parents
nestedScroll.scroll(available = offsetY10)
- assertScene(currentScene = SceneA, isIdle = idleAfterScroll)
+ if (idleAfterScroll) assertIdle(SceneA) else assertTransition(SceneA)
nestedScroll.onPreFling(available = Velocity(0f, velocityThreshold))
}
@@ -449,64 +568,64 @@
fun flingAfterScrollStartedInScene_DuringTransitionBetweenScenes_doNothing() = runGestureTest {
flingAfterScrollStartedInScene(use = DuringTransitionBetweenScenes, idleAfterScroll = true)
- assertScene(currentScene = SceneA, isIdle = true)
+ assertIdle(currentScene = SceneA)
}
@Test
fun flingAfterScrollStartedInScene_EdgeNoOverscroll_doNothing() = runGestureTest {
flingAfterScrollStartedInScene(use = EdgeNoOverscroll, idleAfterScroll = true)
- assertScene(currentScene = SceneA, isIdle = true)
+ assertIdle(currentScene = SceneA)
}
@Test
fun flingAfterScrollStartedInScene_EdgeWithOverscroll_doOverscrollAnimation() = runGestureTest {
flingAfterScrollStartedInScene(use = EdgeWithOverscroll, idleAfterScroll = false)
- assertScene(currentScene = SceneA, isIdle = false)
+ assertTransition(currentScene = SceneA)
// wait for the stop animation
advanceUntilIdle()
- assertScene(currentScene = SceneA, isIdle = true)
+ assertIdle(currentScene = SceneA)
}
@Test
fun flingAfterScrollStartedInScene_Always_goToNextScene() = runGestureTest {
flingAfterScrollStartedInScene(use = Always, idleAfterScroll = false)
- assertScene(currentScene = SceneC, isIdle = false)
+ assertTransition(currentScene = SceneC)
// wait for the stop animation
advanceUntilIdle()
- assertScene(currentScene = SceneC, isIdle = true)
+ assertIdle(currentScene = SceneC)
}
@Test
fun beforeDraggableStart_drag_shouldBeIgnored() = runGestureTest {
- draggable.onDelta(deltaInPixels10)
- assertScene(currentScene = SceneA, isIdle = true)
+ draggable.onDelta(down(0.1f))
+ assertIdle(currentScene = SceneA)
}
@Test
fun beforeDraggableStart_stop_shouldBeIgnored() = runGestureTest {
draggable.onDragStopped(velocityThreshold)
- assertScene(currentScene = SceneA, isIdle = true)
+ assertIdle(currentScene = SceneA)
}
@Test
fun beforeNestedScrollStart_stop_shouldBeIgnored() = runGestureTest {
val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithOverscroll)
nestedScroll.onPreFling(Velocity(0f, velocityThreshold))
- assertScene(currentScene = SceneA, isIdle = true)
+ assertIdle(currentScene = SceneA)
}
@Test
fun startNestedScrollWhileDragging() = runGestureTest {
val nestedScroll = nestedScrollConnection(nestedScrollBehavior = Always)
draggable.onDragStarted()
- assertScene(currentScene = SceneA, isIdle = false)
+ assertTransition(currentScene = SceneA)
- draggable.onDelta(deltaInPixels10)
+ draggable.onDelta(down(0.1f))
assertThat(progress).isEqualTo(0.1f)
// now we can intercept the scroll events
@@ -515,7 +634,7 @@
// this should be ignored, we are scrolling now!
draggable.onDragStopped(velocityThreshold)
- assertScene(currentScene = SceneA, isIdle = false)
+ assertTransition(currentScene = SceneA)
nestedScroll.scroll(available = offsetY10)
assertThat(progress).isEqualTo(0.3f)
@@ -524,10 +643,10 @@
assertThat(progress).isEqualTo(0.4f)
nestedScroll.onPreFling(available = Velocity(0f, velocityThreshold))
- assertScene(currentScene = SceneC, isIdle = false)
+ assertTransition(currentScene = SceneC)
// wait for the stop animation
advanceUntilIdle()
- assertScene(currentScene = SceneC, isIdle = true)
+ assertIdle(currentScene = SceneC)
}
}