Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +1 −22 Original line number Diff line number Diff line Loading @@ -108,7 +108,7 @@ internal class DraggableHandlerImpl( swipes.updateSwipesResults(fromContent) val result = swipes.findUserActionResult(overSlop) (if (overSlop < 0f) swipes.upOrLeftResult else swipes.downOrRightResult) // As we were unable to locate a valid target scene, the initial SwipeAnimation // cannot be defined. Consequently, a simple NoOp Controller will be returned. ?: return NoOpDragController Loading Loading @@ -448,27 +448,6 @@ internal class Swipes(val upOrLeft: Swipe.Resolved, val downOrRight: Swipe.Resol this.upOrLeftResult = upOrLeftResult this.downOrRightResult = downOrRightResult } /** * Returns the [UserActionResult] in the direction of [directionOffset]. * * @param directionOffset signed float that indicates the direction. Positive is down or right * negative is up or left. * @return null when there are no targets in either direction. If one direction is null and you * drag into the null direction this function will return the opposite direction, assuming * that the users intention is to start the drag into the other direction eventually. If * [directionOffset] is 0f and both direction are available, it will default to * [upOrLeftResult]. */ fun findUserActionResult(directionOffset: Float): UserActionResult? { return when { upOrLeftResult == null && downOrRightResult == null -> null (directionOffset < 0f && upOrLeftResult != null) || downOrRightResult == null -> upOrLeftResult else -> downOrRightResult } } } internal class NestedScrollHandlerImpl( Loading packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +6 −117 Original line number Diff line number Diff line Loading @@ -19,7 +19,9 @@ package com.android.compose.animation.scene import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.overscroll import androidx.compose.material3.Text import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.UserInput Loading Loading @@ -102,7 +104,7 @@ class DraggableHandlerTest { userActions = mapOf(Swipe.Up to SceneB, Swipe.Up(fromSource = Edge.Bottom) to SceneA), ) { Text("SceneC") Text("SceneC", Modifier.overscroll(verticalOverscrollEffect)) } overlay( key = OverlayA, Loading Loading @@ -434,35 +436,12 @@ class DraggableHandlerTest { } @Test fun onDragIntoNoAction_startTransitionToOppositeDirection() = runGestureTest { fun onDragIntoNoAction_stayIdle() = runGestureTest { navigateToSceneC() // We are on SceneC which has no action in Down direction val dragController = onDragStarted(overSlop = 10f) assertTransition( currentScene = SceneC, fromScene = SceneC, toScene = SceneB, progress = -0.1f, ) // Reverse drag direction, it will consume the previous drag dragController.onDragDelta(pixels = -10f) assertTransition( currentScene = SceneC, fromScene = SceneC, toScene = SceneB, progress = 0.0f, ) // Continue reverse drag direction, it should record progress to Scene B dragController.onDragDelta(pixels = -10f) assertTransition( currentScene = SceneC, fromScene = SceneC, toScene = SceneB, progress = 0.1f, ) onDragStarted(overSlop = 10f, expectedConsumedOverSlop = 0f) assertIdle(currentScene = SceneC) } @Test Loading Loading @@ -941,30 +920,6 @@ class DraggableHandlerTest { assertIdle(SceneA) } @Test fun scrollFromIdleWithNoTargetScene_shouldUseOverscrollSpecIfAvailable() = runGestureTest { layoutState.transitions = transitions { overscroll(SceneC, Orientation.Vertical) { fade(TestElements.Foo) } } // Start at scene C. navigateToSceneC() val scene = layoutState.transitionState.currentScene // We should have overscroll spec for scene C assertThat(layoutState.transitions.overscrollSpec(scene, Orientation.Vertical)).isNotNull() assertThat(layoutState.currentTransition?.currentOverscrollSpec).isNull() val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways) nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f)) // We scrolled down, under scene C there is nothing, so we can use the overscroll spec assertThat(layoutState.currentTransition?.currentOverscrollSpec).isNotNull() assertThat(layoutState.currentTransition?.currentOverscrollSpec?.content).isEqualTo(SceneC) val transition = layoutState.currentTransition assertThat(transition).isNotNull() assertThat(transition!!.progress).isEqualTo(-0.1f) } @Test fun nestedScrollUseFromSourceInfo() = runGestureTest { // Start at scene C. Loading Loading @@ -1228,72 +1183,6 @@ class DraggableHandlerTest { assertThat(transition).hasOverscrollSpec() } @Test fun overscroll_releaseAtNegativePercent_up() = runGestureTest { // Make Scene A overscrollable. layoutState.transitions = transitions { from(SceneA, to = SceneB) { spec = spring(dampingRatio = Spring.DampingRatioNoBouncy) } overscroll(SceneA, Orientation.Vertical) { fade(TestElements.Foo) } } mutableUserActionsA = mapOf(Swipe.Up to UserActionResult(SceneB)) val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) val dragController = onDragStarted(pointersInfo = middle, overSlop = down(1f)) val transition = assertThat(transitionState).isSceneTransition() assertThat(transition).hasFromScene(SceneA) assertThat(transition).hasToScene(SceneB) assertThat(transition).hasProgress(-1f) // Release to A. dragController.onDragStoppedAnimateNow( velocity = 0f, onAnimationStart = { assertTransition(fromScene = SceneA, toScene = SceneB, progress = -1f) }, expectedConsumedVelocity = 0f, ) // We kept the overscroll at 100% so that the placement logic didn't change at the end of // the animation. assertIdle(SceneA) assertThat(transition).hasProgress(0f) assertThat(transition).hasOverscrollSpec() } @Test fun overscroll_releaseAtNegativePercent_down() = runGestureTest { // Make Scene A overscrollable. layoutState.transitions = transitions { from(SceneA, to = SceneC) { spec = spring(dampingRatio = Spring.DampingRatioNoBouncy) } overscroll(SceneA, Orientation.Vertical) { fade(TestElements.Foo) } } mutableUserActionsA = mapOf(Swipe.Down to UserActionResult(SceneC)) val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) val dragController = onDragStarted(pointersInfo = middle, overSlop = up(1f)) val transition = assertThat(transitionState).isSceneTransition() assertThat(transition).hasFromScene(SceneA) assertThat(transition).hasToScene(SceneC) assertThat(transition).hasProgress(-1f) // Release to A. dragController.onDragStoppedAnimateNow( velocity = 0f, onAnimationStart = { assertTransition(fromScene = SceneA, toScene = SceneC, progress = -1f) }, expectedConsumedVelocity = 0f, ) // We kept the overscroll at 100% so that the placement logic didn't change at the end of // the animation. assertIdle(SceneA) assertThat(transition).hasProgress(0f) assertThat(transition).hasOverscrollSpec() } @Test fun requireFullDistanceSwipe() = runGestureTest { mutableUserActionsA += Loading packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +25 −28 Original line number Diff line number Diff line Loading @@ -1006,77 +1006,74 @@ class ElementTest { @Test fun elementTransitionDuringNestedScrollOverscroll() { lateinit var density: Density // 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 overscrollTranslateY = 10.dp val layoutWidth = 200.dp val layoutHeight = 400.dp val state = rule.runOnUiThread { MutableSceneTransitionLayoutState( initialScene = SceneB, transitions = transitions { overscroll(SceneB, Orientation.Vertical) { progressConverter = ProgressConverter.linear() translate(TestElements.Foo, y = overscrollTranslateY) } }, initialScene = SceneA, transitions = transitions { overscrollDisabled(SceneB, Orientation.Vertical) }, ) as MutableSceneTransitionLayoutStateImpl } rule.setContent { density = LocalDensity.current touchSlop = LocalViewConfiguration.current.touchSlop SceneTransitionLayout( state = state, modifier = Modifier.size(layoutWidth, layoutHeight), ) { scene(SceneA) { Spacer(Modifier.fillMaxSize()) } scene(SceneB, userActions = mapOf(Swipe.Up to SceneA)) { scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) { Box( Modifier // A scrollable that does not consume the scroll gesture .scrollable( rememberScrollableState(consumeScrollDelta = { 0f }), Orientation.Vertical, state = rememberScrollableState(consumeScrollDelta = { 0f }), orientation = Orientation.Vertical, ) .fillMaxSize() ) { Spacer(Modifier.element(TestElements.Foo).fillMaxSize()) ) } scene(SceneB) { Spacer( Modifier.overscroll(verticalOverscrollEffect) .element(TestElements.Foo) .fillMaxSize() ) } } } assertThat(state.transitionState).isIdle() val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag) fooElement.assertTopPositionInRootIsEqualTo(0.dp) rule.onNodeWithTag(TestElements.Foo.testTag).assertDoesNotExist() // Swipe by half of verticalSwipeDistance. rule.onRoot().performTouchInput { val middleTop = Offset((layoutWidth / 2).toPx(), 0f) down(middleTop) // Scroll 50% // Scroll 50%. moveBy(Offset(0f, touchSlop + layoutHeight.toPx() * 0.5f), delayMillis = 1_000) } val transition = assertThat(state.transitionState).isSceneTransition() assertThat(transition).hasOverscrollSpec() assertThat(transition).hasProgress(-0.5f) fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 0.5f) assertThat(transition).hasProgress(0.5f) rule.onNodeWithTag(TestElements.Foo.testTag).assertTopPositionInRootIsEqualTo(0.dp) rule.onRoot().performTouchInput { // Scroll another 100% // Scroll another 100%. moveBy(Offset(0f, layoutHeight.toPx()), delayMillis = 1_000) } // Scroll 150% (Scene B overscroll by 50%) assertThat(transition).hasProgress(-1.5f) assertThat(transition).hasOverscrollSpec() fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 1.5f) // Scroll 150% (Scene B overscroll by 50%). assertThat(transition).hasProgress(1f) rule .onNodeWithTag(TestElements.Foo.testTag) .assertTopPositionInRootIsEqualTo(expectedOffset(layoutHeight * 0.5f, density)) } @Test Loading Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +1 −22 Original line number Diff line number Diff line Loading @@ -108,7 +108,7 @@ internal class DraggableHandlerImpl( swipes.updateSwipesResults(fromContent) val result = swipes.findUserActionResult(overSlop) (if (overSlop < 0f) swipes.upOrLeftResult else swipes.downOrRightResult) // As we were unable to locate a valid target scene, the initial SwipeAnimation // cannot be defined. Consequently, a simple NoOp Controller will be returned. ?: return NoOpDragController Loading Loading @@ -448,27 +448,6 @@ internal class Swipes(val upOrLeft: Swipe.Resolved, val downOrRight: Swipe.Resol this.upOrLeftResult = upOrLeftResult this.downOrRightResult = downOrRightResult } /** * Returns the [UserActionResult] in the direction of [directionOffset]. * * @param directionOffset signed float that indicates the direction. Positive is down or right * negative is up or left. * @return null when there are no targets in either direction. If one direction is null and you * drag into the null direction this function will return the opposite direction, assuming * that the users intention is to start the drag into the other direction eventually. If * [directionOffset] is 0f and both direction are available, it will default to * [upOrLeftResult]. */ fun findUserActionResult(directionOffset: Float): UserActionResult? { return when { upOrLeftResult == null && downOrRightResult == null -> null (directionOffset < 0f && upOrLeftResult != null) || downOrRightResult == null -> upOrLeftResult else -> downOrRightResult } } } internal class NestedScrollHandlerImpl( Loading
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +6 −117 Original line number Diff line number Diff line Loading @@ -19,7 +19,9 @@ package com.android.compose.animation.scene import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.overscroll import androidx.compose.material3.Text import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.UserInput Loading Loading @@ -102,7 +104,7 @@ class DraggableHandlerTest { userActions = mapOf(Swipe.Up to SceneB, Swipe.Up(fromSource = Edge.Bottom) to SceneA), ) { Text("SceneC") Text("SceneC", Modifier.overscroll(verticalOverscrollEffect)) } overlay( key = OverlayA, Loading Loading @@ -434,35 +436,12 @@ class DraggableHandlerTest { } @Test fun onDragIntoNoAction_startTransitionToOppositeDirection() = runGestureTest { fun onDragIntoNoAction_stayIdle() = runGestureTest { navigateToSceneC() // We are on SceneC which has no action in Down direction val dragController = onDragStarted(overSlop = 10f) assertTransition( currentScene = SceneC, fromScene = SceneC, toScene = SceneB, progress = -0.1f, ) // Reverse drag direction, it will consume the previous drag dragController.onDragDelta(pixels = -10f) assertTransition( currentScene = SceneC, fromScene = SceneC, toScene = SceneB, progress = 0.0f, ) // Continue reverse drag direction, it should record progress to Scene B dragController.onDragDelta(pixels = -10f) assertTransition( currentScene = SceneC, fromScene = SceneC, toScene = SceneB, progress = 0.1f, ) onDragStarted(overSlop = 10f, expectedConsumedOverSlop = 0f) assertIdle(currentScene = SceneC) } @Test Loading Loading @@ -941,30 +920,6 @@ class DraggableHandlerTest { assertIdle(SceneA) } @Test fun scrollFromIdleWithNoTargetScene_shouldUseOverscrollSpecIfAvailable() = runGestureTest { layoutState.transitions = transitions { overscroll(SceneC, Orientation.Vertical) { fade(TestElements.Foo) } } // Start at scene C. navigateToSceneC() val scene = layoutState.transitionState.currentScene // We should have overscroll spec for scene C assertThat(layoutState.transitions.overscrollSpec(scene, Orientation.Vertical)).isNotNull() assertThat(layoutState.currentTransition?.currentOverscrollSpec).isNull() val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways) nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f)) // We scrolled down, under scene C there is nothing, so we can use the overscroll spec assertThat(layoutState.currentTransition?.currentOverscrollSpec).isNotNull() assertThat(layoutState.currentTransition?.currentOverscrollSpec?.content).isEqualTo(SceneC) val transition = layoutState.currentTransition assertThat(transition).isNotNull() assertThat(transition!!.progress).isEqualTo(-0.1f) } @Test fun nestedScrollUseFromSourceInfo() = runGestureTest { // Start at scene C. Loading Loading @@ -1228,72 +1183,6 @@ class DraggableHandlerTest { assertThat(transition).hasOverscrollSpec() } @Test fun overscroll_releaseAtNegativePercent_up() = runGestureTest { // Make Scene A overscrollable. layoutState.transitions = transitions { from(SceneA, to = SceneB) { spec = spring(dampingRatio = Spring.DampingRatioNoBouncy) } overscroll(SceneA, Orientation.Vertical) { fade(TestElements.Foo) } } mutableUserActionsA = mapOf(Swipe.Up to UserActionResult(SceneB)) val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) val dragController = onDragStarted(pointersInfo = middle, overSlop = down(1f)) val transition = assertThat(transitionState).isSceneTransition() assertThat(transition).hasFromScene(SceneA) assertThat(transition).hasToScene(SceneB) assertThat(transition).hasProgress(-1f) // Release to A. dragController.onDragStoppedAnimateNow( velocity = 0f, onAnimationStart = { assertTransition(fromScene = SceneA, toScene = SceneB, progress = -1f) }, expectedConsumedVelocity = 0f, ) // We kept the overscroll at 100% so that the placement logic didn't change at the end of // the animation. assertIdle(SceneA) assertThat(transition).hasProgress(0f) assertThat(transition).hasOverscrollSpec() } @Test fun overscroll_releaseAtNegativePercent_down() = runGestureTest { // Make Scene A overscrollable. layoutState.transitions = transitions { from(SceneA, to = SceneC) { spec = spring(dampingRatio = Spring.DampingRatioNoBouncy) } overscroll(SceneA, Orientation.Vertical) { fade(TestElements.Foo) } } mutableUserActionsA = mapOf(Swipe.Down to UserActionResult(SceneC)) val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) val dragController = onDragStarted(pointersInfo = middle, overSlop = up(1f)) val transition = assertThat(transitionState).isSceneTransition() assertThat(transition).hasFromScene(SceneA) assertThat(transition).hasToScene(SceneC) assertThat(transition).hasProgress(-1f) // Release to A. dragController.onDragStoppedAnimateNow( velocity = 0f, onAnimationStart = { assertTransition(fromScene = SceneA, toScene = SceneC, progress = -1f) }, expectedConsumedVelocity = 0f, ) // We kept the overscroll at 100% so that the placement logic didn't change at the end of // the animation. assertIdle(SceneA) assertThat(transition).hasProgress(0f) assertThat(transition).hasOverscrollSpec() } @Test fun requireFullDistanceSwipe() = runGestureTest { mutableUserActionsA += Loading
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +25 −28 Original line number Diff line number Diff line Loading @@ -1006,77 +1006,74 @@ class ElementTest { @Test fun elementTransitionDuringNestedScrollOverscroll() { lateinit var density: Density // 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 overscrollTranslateY = 10.dp val layoutWidth = 200.dp val layoutHeight = 400.dp val state = rule.runOnUiThread { MutableSceneTransitionLayoutState( initialScene = SceneB, transitions = transitions { overscroll(SceneB, Orientation.Vertical) { progressConverter = ProgressConverter.linear() translate(TestElements.Foo, y = overscrollTranslateY) } }, initialScene = SceneA, transitions = transitions { overscrollDisabled(SceneB, Orientation.Vertical) }, ) as MutableSceneTransitionLayoutStateImpl } rule.setContent { density = LocalDensity.current touchSlop = LocalViewConfiguration.current.touchSlop SceneTransitionLayout( state = state, modifier = Modifier.size(layoutWidth, layoutHeight), ) { scene(SceneA) { Spacer(Modifier.fillMaxSize()) } scene(SceneB, userActions = mapOf(Swipe.Up to SceneA)) { scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) { Box( Modifier // A scrollable that does not consume the scroll gesture .scrollable( rememberScrollableState(consumeScrollDelta = { 0f }), Orientation.Vertical, state = rememberScrollableState(consumeScrollDelta = { 0f }), orientation = Orientation.Vertical, ) .fillMaxSize() ) { Spacer(Modifier.element(TestElements.Foo).fillMaxSize()) ) } scene(SceneB) { Spacer( Modifier.overscroll(verticalOverscrollEffect) .element(TestElements.Foo) .fillMaxSize() ) } } } assertThat(state.transitionState).isIdle() val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag) fooElement.assertTopPositionInRootIsEqualTo(0.dp) rule.onNodeWithTag(TestElements.Foo.testTag).assertDoesNotExist() // Swipe by half of verticalSwipeDistance. rule.onRoot().performTouchInput { val middleTop = Offset((layoutWidth / 2).toPx(), 0f) down(middleTop) // Scroll 50% // Scroll 50%. moveBy(Offset(0f, touchSlop + layoutHeight.toPx() * 0.5f), delayMillis = 1_000) } val transition = assertThat(state.transitionState).isSceneTransition() assertThat(transition).hasOverscrollSpec() assertThat(transition).hasProgress(-0.5f) fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 0.5f) assertThat(transition).hasProgress(0.5f) rule.onNodeWithTag(TestElements.Foo.testTag).assertTopPositionInRootIsEqualTo(0.dp) rule.onRoot().performTouchInput { // Scroll another 100% // Scroll another 100%. moveBy(Offset(0f, layoutHeight.toPx()), delayMillis = 1_000) } // Scroll 150% (Scene B overscroll by 50%) assertThat(transition).hasProgress(-1.5f) assertThat(transition).hasOverscrollSpec() fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 1.5f) // Scroll 150% (Scene B overscroll by 50%). assertThat(transition).hasProgress(1f) rule .onNodeWithTag(TestElements.Foo.testTag) .assertTopPositionInRootIsEqualTo(expectedOffset(layoutHeight * 0.5f, density)) } @Test Loading