diff options
3 files changed, 104 insertions, 19 deletions
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index bf7f16e18174..0f7e3eaf75ad 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.round +import androidx.compose.ui.util.fastCoerceIn import com.android.compose.animation.scene.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified import com.android.compose.animation.scene.content.Scene import com.android.compose.nestedscroll.PriorityNestedScrollConnection @@ -288,7 +289,8 @@ private class DragControllerImpl( val toScene = swipeTransition._toScene val distance = swipeTransition.distance() - val desiredOffset = swipeTransition.dragOffset + delta + val previousOffset = swipeTransition.dragOffset + val desiredOffset = previousOffset + delta fun hasReachedToSceneUpOrLeft() = distance < 0 && @@ -312,6 +314,7 @@ private class DragControllerImpl( val fromScene: Scene val currentTransitionOffset: Float val newOffset: Float + val consumedDelta: Float if (hasReachedToScene) { // The new transition will start from the current toScene fromScene = toScene @@ -319,11 +322,21 @@ private class DragControllerImpl( currentTransitionOffset = distance // The next transition will start with the remaining offset newOffset = desiredOffset - distance + consumedDelta = delta } else { fromScene = swipeTransition._fromScene - currentTransitionOffset = desiredOffset + val desiredProgress = swipeTransition.computeProgress(desiredOffset) + // note: the distance could be negative if fromScene is aboveOrLeft of toScene. + currentTransitionOffset = + when { + distance == DistanceUnspecified || + swipeTransition.isWithinProgressRange(desiredProgress) -> desiredOffset + distance > 0f -> desiredOffset.fastCoerceIn(0f, distance) + else -> desiredOffset.fastCoerceIn(distance, 0f) + } // If there is a new transition, we will use the same offset newOffset = currentTransitionOffset + consumedDelta = newOffset - previousOffset } swipeTransition.dragOffset = currentTransitionOffset @@ -363,7 +376,7 @@ private class DragControllerImpl( updateTransition(newSwipeTransition) } - return delta + return consumedDelta } override fun onStop(velocity: Float, canChangeScene: Boolean): Float { 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 c8bbb149a042..1e12d2ca5a5c 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 @@ -195,10 +195,17 @@ class DraggableHandlerTest { startedPosition: Offset = Offset.Zero, overSlop: Float, pointersDown: Int = 1, + expectedConsumedOverSlop: Float = overSlop, ): DragController { // overSlop should be 0f only if the drag gesture starts with startDragImmediately if (overSlop == 0f) error("Consider using onDragStartedImmediately()") - return onDragStarted(draggableHandler, startedPosition, overSlop, pointersDown) + return onDragStarted( + draggableHandler = draggableHandler, + startedPosition = startedPosition, + overSlop = overSlop, + pointersDown = pointersDown, + expectedConsumedOverSlop = expectedConsumedOverSlop, + ) } fun onDragStartedImmediately( @@ -213,7 +220,7 @@ class DraggableHandlerTest { startedPosition: Offset = Offset.Zero, overSlop: Float = 0f, pointersDown: Int = 1, - expectedConsumed: Boolean = true, + expectedConsumedOverSlop: Float = overSlop, ): DragController { val dragController = draggableHandler.onDragStarted( @@ -223,14 +230,14 @@ class DraggableHandlerTest { ) // MultiPointerDraggable will always call onDelta with the initial overSlop right after - dragController.onDragDelta(pixels = overSlop, expectedConsumed = expectedConsumed) + dragController.onDragDelta(pixels = overSlop, expectedConsumedOverSlop) return dragController } - fun DragController.onDragDelta(pixels: Float, expectedConsumed: Boolean = true) { + fun DragController.onDragDelta(pixels: Float, expectedConsumed: Float = pixels) { val consumed = onDrag(delta = pixels) - assertThat(consumed).isEqualTo(if (expectedConsumed) pixels else 0f) + assertThat(consumed).isEqualTo(expectedConsumed) } fun DragController.onDragStopped( @@ -370,14 +377,14 @@ class DraggableHandlerTest { onDragStarted( horizontalDraggableHandler, overSlop = up(fractionOfScreen = 0.3f), - expectedConsumed = false, + expectedConsumedOverSlop = 0f, ) assertIdle(currentScene = SceneA) onDragStarted( horizontalDraggableHandler, overSlop = down(fractionOfScreen = 0.3f), - expectedConsumed = false, + expectedConsumedOverSlop = 0f, ) assertIdle(currentScene = SceneA) } @@ -504,19 +511,19 @@ class DraggableHandlerTest { // start accelaratedScroll and scroll over to B -> null val dragController2 = onDragStartedImmediately() - dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = false) - dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = false) + dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = 0f) + dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = 0f) // here onDragStopped is already triggered, but subsequent onDelta/onDragStopped calls may // still be called. Make sure that they don't crash or change the scene - dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = false) + dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = 0f) dragController2.onDragStopped(velocity = 0f) advanceUntilIdle() assertIdle(SceneB) // These events can still come in after the animation has settled - dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = false) + dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = 0f) dragController2.onDragStopped(velocity = 0f) assertIdle(SceneB) } @@ -1051,8 +1058,16 @@ class DraggableHandlerTest { // Swipe up to scene B at progress = 200%. val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f) - val dragController = onDragStarted(startedPosition = middle, overSlop = up(2f)) - val transition = assertTransition(fromScene = SceneA, toScene = SceneB, progress = 2f) + val dragController = + onDragStarted( + startedPosition = middle, + overSlop = up(2f), + // Overscroll is disabled, it will scroll up to 100% + expectedConsumedOverSlop = up(1f), + ) + + // The progress value is coerced in `[0..1]` + assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f) // Release the finger. dragController.onDragStopped(velocity = -velocityThreshold) @@ -1061,9 +1076,6 @@ class DraggableHandlerTest { // 100% and that the overscroll on scene B is doing nothing, we are already idle. runCurrent() assertIdle(SceneB) - - // Progress is snapped to 100%. - assertThat(transition).hasProgress(1f) } @Test 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 0766e00bfccc..d9fd932199cf 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 @@ -29,6 +29,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue 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 +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.testTag @@ -786,4 +789,61 @@ class SwipeToSceneTest { .onNode(isElement(SceneB.rootElementKey)) .assertPositionInRootIsEqualTo(-layoutSize, 0.dp) } + + @Test + fun whenOverscrollIsDisabled_dragGestureShouldNotBeConsumed() { + val swipeDistance = 100.dp + + var availableOnPostScroll = Float.MIN_VALUE + val connection = + object : NestedScrollConnection { + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource + ): Offset { + availableOnPostScroll = available.y + return super.onPostScroll(consumed, available, source) + } + } + val state = + rule.runOnUiThread { + MutableSceneTransitionLayoutState( + SceneA, + transitions { + from(SceneA, to = SceneB) { distance = FixedDistance(swipeDistance) } + overscroll(SceneB, Orientation.Vertical) + } + ) + } + val layoutSize = 200.dp + var touchSlop = 0f + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + SceneTransitionLayout(state, Modifier.size(layoutSize).nestedScroll(connection)) { + scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) { + Box(Modifier.fillMaxSize()) + } + scene(SceneB) { Box(Modifier.element(TestElements.Foo).fillMaxSize()) } + } + } + + // Swipe down by the swipe distance so that we are on scene B. + rule.onRoot().performTouchInput { + val middle = (layoutSize / 2).toPx() + down(Offset(middle, middle)) + moveBy(Offset(0f, touchSlop + (swipeDistance).toPx()), delayMillis = 1_000) + } + val transition = state.currentTransition + assertThat(transition).isNotNull() + assertThat(transition!!.progress).isEqualTo(1f) + assertThat(availableOnPostScroll).isEqualTo(0f) + + // Overscrolling on Scene B + val ovescrollPx = 100f + rule.onRoot().performTouchInput { moveBy(Offset(0f, ovescrollPx), delayMillis = 1_000) } + // Overscroll is disabled on Scene B + assertThat(transition.progress).isEqualTo(1f) + assertThat(availableOnPostScroll).isEqualTo(ovescrollPx) + } } |