summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt19
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt44
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt60
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)
+ }
}