Merge "Make nested scrolling tests depend less on implementation" into main
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 def8323..5a9edba 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
@@ -137,13 +137,6 @@
var pointerInfoOwner: () -> PointersInfo = { pointersDown() }
- fun nestedScrollConnection() =
- NestedScrollHandlerImpl(
- draggableHandler = draggableHandler,
- pointersInfoOwner = { pointerInfoOwner() },
- )
- .connection
-
val velocityThreshold = draggableHandler.velocityThreshold
fun down(fractionOfScreen: Float) =
@@ -607,57 +600,6 @@
}
@Test
- fun nestedScrollUseFromSourceInfo() = runGestureTest {
- // Start at scene C.
- navigateToSceneC()
- val nestedScroll = nestedScrollConnection()
-
- // Drag from the **top** of the screen
- pointerInfoOwner = { pointersDown() }
- assertIdle(currentScene = SceneC)
-
- nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
- assertTransition(
- currentScene = SceneC,
- fromScene = SceneC,
- // userAction: Swipe.Up to SceneB
- toScene = SceneB,
- progress = 0.1f,
- )
-
- // Reset to SceneC
- nestedScroll.preFling(Velocity.Zero)
- advanceUntilIdle()
-
- // Drag from the **bottom** of the screen
- pointerInfoOwner = { pointersDown(startedPosition = Offset(0f, SCREEN_SIZE)) }
- assertIdle(currentScene = SceneC)
-
- nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
- assertTransition(
- currentScene = SceneC,
- fromScene = SceneC,
- // userAction: Swipe.Up(fromSource = Edge.Bottom) to SceneA
- toScene = SceneA,
- progress = 0.1f,
- )
- }
-
- @Test
- fun ignoreMouseWheel() = runGestureTest {
- // Start at scene C.
- navigateToSceneC()
- val nestedScroll = nestedScrollConnection()
-
- // Use mouse wheel
- pointerInfoOwner = { PointersInfo.MouseWheel }
- assertIdle(currentScene = SceneC)
-
- nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
- assertIdle(currentScene = SceneC)
- }
-
- @Test
fun transitionIsImmediatelyUpdatedWhenReleasingFinger() = runGestureTest {
// Swipe up from the middle to transition to scene B.
val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f))
@@ -689,24 +631,6 @@
}
@Test
- fun scrollKeepPriorityEvenIfWeCanNoLongerScrollOnThatDirection() = runGestureTest {
- val nestedScroll = nestedScrollConnection()
-
- // Overscroll is disabled, it will scroll up to 100%
- nestedScroll.scroll(available = upOffset(fractionOfScreen = 2f))
- assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f)
-
- // We need to maintain scroll priority even if the scene transition can no longer consume
- // the scroll gesture.
- nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f))
- assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f)
-
- // A scroll gesture in the opposite direction allows us to return to the previous scene.
- nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.5f))
- assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.5f)
- }
-
- @Test
fun overscroll_releaseBetween0And100Percent_up() = runGestureTest {
// Make scene B overscrollable.
layoutState.transitions = transitions {
@@ -944,33 +868,4 @@
assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayB)
}
-
- @Test
- fun replaceOverlayNestedScroll() = runGestureTest {
- layoutState.showOverlay(OverlayA, animationScope = testScope)
- advanceUntilIdle()
-
- // Initial state.
- assertThat(layoutState.transitionState).isIdle()
- assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
- assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayA)
-
- // Swipe down to replace overlay A by overlay B.
-
- val nestedScroll = nestedScrollConnection()
- nestedScroll.scroll(downOffset(0.1f))
- val transition = assertThat(layoutState.transitionState).isReplaceOverlayTransition()
- assertThat(transition).hasCurrentScene(SceneA)
- assertThat(transition).hasFromOverlay(OverlayA)
- assertThat(transition).hasToOverlay(OverlayB)
- assertThat(transition).hasCurrentOverlays(OverlayA)
- assertThat(transition).hasProgress(0.1f)
-
- nestedScroll.preFling(Velocity(0f, velocityThreshold))
- advanceUntilIdle()
- // Commit the gesture. The overlays are instantly swapped in the set of current overlays.
- assertThat(layoutState.transitionState).isIdle()
- assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
- assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayB)
- }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index e80805a..0355a30 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -40,6 +40,7 @@
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.ScrollWheel
+import androidx.compose.ui.test.TouchInjectionScope
import androidx.compose.ui.test.assertPositionInRootIsEqualTo
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.createComposeRule
@@ -55,6 +56,8 @@
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestOverlays.OverlayA
+import com.android.compose.animation.scene.TestOverlays.OverlayB
import com.android.compose.animation.scene.TestScenes.SceneA
import com.android.compose.animation.scene.TestScenes.SceneB
import com.android.compose.animation.scene.TestScenes.SceneC
@@ -977,4 +980,164 @@
rule.waitForIdle()
assertThat(state.transitionState).isSceneTransition()
}
+
+ @Test
+ fun nestedScroll_useFromSourceInfo() {
+ val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
+ var touchSlop = 0f
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ SceneTransitionLayout(state) {
+ scene(
+ SceneA,
+ userActions =
+ mapOf(Swipe.Down to SceneB, Swipe.Down(fromSource = Edge.Top) to SceneC),
+ ) {
+ // Use a fullscreen nested scrollable to use the nested scroll connection.
+ Box(
+ Modifier.fillMaxSize()
+ .scrollable(rememberScrollableState { 0f }, Orientation.Vertical)
+ )
+ }
+ scene(SceneB) { Box(Modifier.fillMaxSize()) }
+ scene(SceneC) { Box(Modifier.fillMaxSize()) }
+ }
+ }
+
+ // Swiping down from the middle of the screen leads to B.
+ rule.onRoot().performTouchInput {
+ down(center)
+ moveBy(Offset(0f, touchSlop + 1f))
+ }
+
+ var transition = assertThat(state.transitionState).isSceneTransition()
+ assertThat(transition).hasFromScene(SceneA)
+ assertThat(transition).hasToScene(SceneB)
+
+ // Release finger and wait to settle back to A.
+ rule.onRoot().performTouchInput { up() }
+ rule.waitForIdle()
+ assertThat(state.transitionState).isIdle()
+ assertThat(state.transitionState).hasCurrentScene(SceneA)
+
+ // Swiping down from the top of the screen leads to B.
+ rule.onRoot().performTouchInput {
+ down(center.copy(y = 0f))
+ moveBy(Offset(0f, touchSlop + 1f))
+ }
+
+ transition = assertThat(state.transitionState).isSceneTransition()
+ assertThat(transition).hasFromScene(SceneA)
+ assertThat(transition).hasToScene(SceneC)
+ }
+
+ @Test
+ fun nestedScroll_ignoreMouseWheel() {
+ val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
+ var touchSlop = 0f
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ SceneTransitionLayout(state) {
+ scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
+ // Use a fullscreen nested scrollable to use the nested scroll connection.
+ Box(
+ Modifier.fillMaxSize()
+ .scrollable(rememberScrollableState { 0f }, Orientation.Vertical)
+ )
+ }
+ scene(SceneB) { Box(Modifier.fillMaxSize()) }
+ }
+ }
+
+ rule.onRoot().performMouseInput {
+ scroll(-touchSlop - 1f, scrollWheel = ScrollWheel.Vertical)
+ }
+ assertThat(state.transitionState).isIdle()
+ }
+
+ @Test
+ fun nestedScroll_keepPriorityEvenIfWeCanNoLongerScrollOnThatDirection() {
+ val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
+ var touchSlop = 0f
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ SceneTransitionLayout(state) {
+ scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
+ // Use a fullscreen nested scrollable to use the nested scroll connection.
+ Box(
+ Modifier.fillMaxSize()
+ .scrollable(rememberScrollableState { 0f }, Orientation.Vertical)
+ )
+ }
+ scene(SceneB) { Box(Modifier.fillMaxSize()) }
+ }
+ }
+
+ fun TouchInjectionScope.height() = bottom
+ fun TouchInjectionScope.halfHeight() = height() / 2f
+
+ rule.onRoot().performTouchInput {
+ down(center.copy(y = 0f))
+ moveBy(Offset(0f, touchSlop + halfHeight()))
+ }
+ val transition = assertThat(state.transitionState).isSceneTransition()
+ assertThat(transition).hasProgress(0.5f, tolerance = 0.01f)
+
+ // The progress should never go above 100%.
+ rule.onRoot().performTouchInput { moveBy(Offset(0f, height())) }
+ assertThat(transition).hasProgress(1f, tolerance = 0.01f)
+
+ // Because the overscroll effect of scene B is not attached, swiping in the opposite
+ // direction will directly decrease the progress.
+ rule.onRoot().performTouchInput { moveBy(Offset(0f, -halfHeight())) }
+ assertThat(transition).hasProgress(0.5f, tolerance = 0.01f)
+ }
+
+ @Test
+ fun nestedScroll_replaceOverlay() {
+ val state =
+ rule.runOnUiThread {
+ MutableSceneTransitionLayoutState(SceneA, initialOverlays = setOf(OverlayA))
+ }
+ var touchSlop = 0f
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ SceneTransitionLayout(state) {
+ scene(SceneA) { Box(Modifier.fillMaxSize()) }
+ overlay(
+ OverlayA,
+ mapOf(Swipe.Down to UserActionResult.ReplaceByOverlay(OverlayB)),
+ ) {
+ Box(
+ Modifier.fillMaxSize()
+ .scrollable(rememberScrollableState { 0f }, Orientation.Vertical)
+ )
+ }
+ overlay(OverlayB) { Box(Modifier.fillMaxSize()) }
+ }
+ }
+
+ // Swipe down 100% to replace A by B.
+ rule.onRoot().performTouchInput {
+ down(center.copy(y = 0f))
+ moveBy(Offset(0f, touchSlop + bottom))
+ }
+
+ val transition = assertThat(state.transitionState).isReplaceOverlayTransition()
+ assertThat(transition).hasCurrentScene(SceneA)
+ assertThat(transition).hasFromOverlay(OverlayA)
+ assertThat(transition).hasToOverlay(OverlayB)
+ assertThat(transition).hasCurrentOverlays(OverlayA)
+ assertThat(transition).hasProgress(1f, tolerance = 0.01f)
+
+ // Commit the gesture. The overlays are instantly swapped in the set of current overlays.
+ rule.onRoot().performTouchInput { up() }
+ assertThat(transition).hasCurrentScene(SceneA)
+ assertThat(transition).hasCurrentOverlays(OverlayB)
+
+ rule.waitForIdle()
+ assertThat(state.transitionState).isIdle()
+ assertThat(state.transitionState).hasCurrentScene(SceneA)
+ assertThat(state.transitionState).hasCurrentOverlays(OverlayB)
+ }
}