summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt3
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt16
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt301
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt2
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt8
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt95
6 files changed, 237 insertions, 188 deletions
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
index 5d8eaf7f3d15..58052cd60f39 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
@@ -2,10 +2,9 @@ package com.android.compose.animation.scene
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import androidx.compose.ui.unit.IntSize
interface DraggableHandler {
- fun onDragStarted(layoutSize: IntSize, startedPosition: Offset, pointersDown: Int = 1)
+ fun onDragStarted(startedPosition: Offset, overSlop: Float, pointersDown: Int = 1)
fun onDelta(pixels: Float)
fun onDragStopped(velocity: Float)
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index a0fba8076517..38738782c889 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -66,8 +66,8 @@ internal fun Modifier.multiPointerDraggable(
orientation: Orientation,
enabled: Boolean,
startDragImmediately: Boolean,
- onDragStarted: (layoutSize: IntSize, startedPosition: Offset, pointersDown: Int) -> Unit,
- onDragDelta: (Float) -> Unit,
+ onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit,
+ onDragDelta: (delta: Float) -> Unit,
onDragStopped: (velocity: Float) -> Unit,
): Modifier =
this.then(
@@ -86,7 +86,7 @@ private data class MultiPointerDraggableElement(
private val enabled: Boolean,
private val startDragImmediately: Boolean,
private val onDragStarted:
- (layoutSize: IntSize, startedPosition: Offset, pointersDown: Int) -> Unit,
+ (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit,
private val onDragDelta: (Float) -> Unit,
private val onDragStopped: (velocity: Float) -> Unit,
) : ModifierNodeElement<MultiPointerDraggableNode>() {
@@ -114,7 +114,7 @@ private class MultiPointerDraggableNode(
orientation: Orientation,
enabled: Boolean,
var startDragImmediately: Boolean,
- var onDragStarted: (layoutSize: IntSize, startedPosition: Offset, pointersDown: Int) -> Unit,
+ var onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit,
var onDragDelta: (Float) -> Unit,
var onDragStopped: (velocity: Float) -> Unit,
) : PointerInputModifierNode, DelegatingNode(), CompositionLocalConsumerModifierNode {
@@ -153,9 +153,9 @@ private class MultiPointerDraggableNode(
return
}
- val onDragStart: (Offset, Int) -> Unit = { startedPosition, pointersDown ->
+ val onDragStart: (Offset, Float, Int) -> Unit = { startedPosition, overSlop, pointersDown ->
velocityTracker.resetTracking()
- onDragStarted(size, startedPosition, pointersDown)
+ onDragStarted(startedPosition, overSlop, pointersDown)
}
val onDragCancel: () -> Unit = { onDragStopped(/* velocity= */ 0f) }
@@ -203,7 +203,7 @@ private class MultiPointerDraggableNode(
private suspend fun PointerInputScope.detectDragGestures(
orientation: Orientation,
startDragImmediately: () -> Boolean,
- onDragStart: (startedPosition: Offset, pointersDown: Int) -> Unit,
+ onDragStart: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit,
onDragEnd: () -> Unit,
onDragCancel: () -> Unit,
onDrag: (change: PointerInputChange, dragAmount: Float) -> Unit,
@@ -241,7 +241,7 @@ private suspend fun PointerInputScope.detectDragGestures(
}
}
- onDragStart(drag.position, pressed.size)
+ onDragStart(drag.position, overSlop, pressed.size)
onDrag(drag, overSlop)
val successful =
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
index b00c88612269..661ac9adfe80 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:Suppress("NOTHING_TO_INLINE")
+
package com.android.compose.animation.scene
import android.util.Log
@@ -26,7 +28,6 @@ import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
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 com.android.compose.nestedscroll.PriorityNestedScrollConnection
@@ -48,14 +49,13 @@ internal class SceneGestureHandler(
layoutImpl.state.transitionState = value
}
- /**
- * The transition controlled by this gesture handler. It will be set as the [transitionState] in
- * the [SceneTransitionLayoutImpl] whenever this handler is driving the current transition.
- *
- * Note: the initialScene here does not matter, it's only used for initializing the transition
- * and will be replaced when a drag event starts.
- */
- internal val swipeTransition = SwipeTransition(initialScene = currentScene)
+ internal var swipeTransition: SwipeTransition = SwipeTransition(currentScene, currentScene, 1f)
+ private set
+
+ private fun updateTransition(newTransition: SwipeTransition, force: Boolean = false) {
+ if (isDrivingTransition || force) transitionState = newTransition
+ swipeTransition = newTransition
+ }
internal val currentScene: Scene
get() = layoutImpl.scene(transitionState.currentScene)
@@ -63,15 +63,6 @@ internal class SceneGestureHandler(
internal val isDrivingTransition
get() = transitionState == swipeTransition
- internal var isAnimatingOffset
- get() = swipeTransition.isAnimatingOffset
- private set(value) {
- swipeTransition.isAnimatingOffset = value
- }
-
- internal val swipeTransitionToScene
- get() = swipeTransition._toScene
-
/**
* The velocity threshold at which the intent of the user is to swipe up or down. It is the same
* as SwipeableV2Defaults.VelocityThreshold.
@@ -86,11 +77,17 @@ internal class SceneGestureHandler(
internal var gestureWithPriority: Any? = null
- internal fun onDragStarted(pointersDown: Int, layoutSize: IntSize, startedPosition: Offset?) {
+ /** The [UserAction]s associated to the current swipe. */
+ private var actionUpOrLeft: UserAction? = null
+ private var actionDownOrRight: UserAction? = null
+ private var actionUpOrLeftNoEdge: UserAction? = null
+ private var actionDownOrRightNoEdge: UserAction? = null
+
+ internal fun onDragStarted(pointersDown: Int, startedPosition: Offset?, overSlop: Float) {
if (isDrivingTransition) {
// This [transition] was already driving the animation: simply take over it.
// Stop animating and start from where the current offset.
- swipeTransition.stopOffsetAnimation()
+ swipeTransition.cancelOffsetAnimation()
return
}
@@ -106,37 +103,29 @@ internal class SceneGestureHandler(
}
val fromScene = currentScene
+ setCurrentActions(fromScene, startedPosition, pointersDown)
- swipeTransition._currentScene = fromScene
- swipeTransition._fromScene = fromScene
-
- // We don't know where we are transitioning to yet given that the drag just started, so set
- // it to fromScene, which will effectively be treated the same as Idle(fromScene).
- swipeTransition._toScene = fromScene
+ if (fromScene.upOrLeft() == null && fromScene.downOrRight() == null) {
+ return
+ }
- swipeTransition.stopOffsetAnimation()
- swipeTransition.dragOffset = 0f
+ val (targetScene, distance) = fromScene.findTargetSceneAndDistance(overSlop)
- // Use the layout size in the swipe orientation for swipe distance.
- // TODO(b/290184746): Also handle custom distances for transitions. With smaller distances,
- // we will also have to make sure that we correctly handle overscroll.
- swipeTransition.absoluteDistance =
- when (orientation) {
- Orientation.Horizontal -> layoutSize.width
- Orientation.Vertical -> layoutSize.height
- }.toFloat()
+ updateTransition(SwipeTransition(fromScene, targetScene, distance), force = true)
+ }
+ private fun setCurrentActions(fromScene: Scene, startedPosition: Offset?, pointersDown: Int) {
val fromEdge =
startedPosition?.let { position ->
layoutImpl.edgeDetector.edge(
- layoutSize,
+ fromScene.targetSize,
position.round(),
layoutImpl.density,
orientation,
)
}
- swipeTransition.actionUpOrLeft =
+ val upOrLeft =
Swipe(
direction =
when (orientation) {
@@ -147,7 +136,7 @@ internal class SceneGestureHandler(
fromEdge = fromEdge,
)
- swipeTransition.actionDownOrRight =
+ val downOrRight =
Swipe(
direction =
when (orientation) {
@@ -159,108 +148,114 @@ internal class SceneGestureHandler(
)
if (fromEdge == null) {
- swipeTransition.actionUpOrLeftNoEdge = null
- swipeTransition.actionDownOrRightNoEdge = null
+ actionUpOrLeft = null
+ actionDownOrRight = null
+ actionUpOrLeftNoEdge = upOrLeft
+ actionDownOrRightNoEdge = downOrRight
} else {
- swipeTransition.actionUpOrLeftNoEdge =
- (swipeTransition.actionUpOrLeft as Swipe).copy(fromEdge = null)
- swipeTransition.actionDownOrRightNoEdge =
- (swipeTransition.actionDownOrRight as Swipe).copy(fromEdge = null)
+ actionUpOrLeft = upOrLeft
+ actionDownOrRight = downOrRight
+ actionUpOrLeftNoEdge = upOrLeft.copy(fromEdge = null)
+ actionDownOrRightNoEdge = downOrRight.copy(fromEdge = null)
}
+ }
- if (swipeTransition.absoluteDistance > 0f) {
- transitionState = swipeTransition
- }
+ /**
+ * Use the layout size in the swipe orientation for swipe distance.
+ *
+ * TODO(b/290184746): Also handle custom distances for transitions. With smaller distances, we
+ * will also have to make sure that we correctly handle overscroll.
+ */
+ private fun Scene.getAbsoluteDistance(): Float {
+ return when (orientation) {
+ Orientation.Horizontal -> targetSize.width
+ Orientation.Vertical -> targetSize.height
+ }.toFloat()
}
internal fun onDrag(delta: Float) {
- if (delta == 0f) return
-
+ if (delta == 0f || !isDrivingTransition) return
swipeTransition.dragOffset += delta
- // First check transition.fromScene should be changed for the case where the user quickly
- // swiped twice in a row to accelerate the transition and go from A => B then B => C really
- // fast.
- maybeHandleAcceleratedSwipe()
-
- val offset = swipeTransition.dragOffset
- val fromScene = swipeTransition._fromScene
+ val (fromScene, acceleratedOffset) =
+ computeFromSceneConsideringAcceleratedSwipe(swipeTransition)
+ swipeTransition.dragOffset += acceleratedOffset
// Compute the target scene depending on the current offset.
- val target = fromScene.findTargetSceneAndDistance(offset)
+ val (targetScene, distance) =
+ fromScene.findTargetSceneAndDistance(swipeTransition.dragOffset)
- if (swipeTransition._toScene.key != target.sceneKey) {
- swipeTransition._toScene = layoutImpl.scenes.getValue(target.sceneKey)
- }
-
- if (swipeTransition._distance != target.distance) {
- swipeTransition._distance = target.distance
+ // TODO(b/290184746): support long scroll A => B => C? especially for non fullscreen scenes
+ if (
+ fromScene.key != swipeTransition.fromScene || targetScene.key != swipeTransition.toScene
+ ) {
+ updateTransition(
+ SwipeTransition(fromScene, targetScene, distance).apply {
+ this.dragOffset = swipeTransition.dragOffset
+ }
+ )
}
}
/**
* Change fromScene in the case where the user quickly swiped multiple times in the same
* direction to accelerate the transition from A => B then B => C.
+ *
+ * @return the new fromScene and a dragOffset to be added in case the scene has changed
+ *
+ * TODO(b/290184746): the second drag needs to pass B to work. Add support for flinging twice
+ * before B has been reached
*/
- private fun maybeHandleAcceleratedSwipe() {
+ private inline fun computeFromSceneConsideringAcceleratedSwipe(
+ swipeTransition: SwipeTransition,
+ ): Pair<Scene, Float> {
val toScene = swipeTransition._toScene
val fromScene = swipeTransition._fromScene
+ val absoluteDistance = swipeTransition.distance.absoluteValue
// If the swipe was not committed, don't do anything.
if (fromScene == toScene || swipeTransition._currentScene != toScene) {
- return
+ return Pair(fromScene, 0f)
}
// If the offset is past the distance then let's change fromScene so that the user can swipe
// to the next screen or go back to the previous one.
val offset = swipeTransition.dragOffset
- val absoluteDistance = swipeTransition.absoluteDistance
- if (offset <= -absoluteDistance && swipeTransition.upOrLeft(fromScene) == toScene.key) {
- swipeTransition.dragOffset += absoluteDistance
- swipeTransition._fromScene = toScene
- } else if (
- offset >= absoluteDistance && swipeTransition.downOrRight(fromScene) == toScene.key
- ) {
- swipeTransition.dragOffset -= absoluteDistance
- swipeTransition._fromScene = toScene
+ return if (offset <= -absoluteDistance && fromScene.upOrLeft() == toScene.key) {
+ Pair(toScene, absoluteDistance)
+ } else if (offset >= absoluteDistance && fromScene.downOrRight() == toScene.key) {
+ Pair(toScene, -absoluteDistance)
+ } else {
+ Pair(fromScene, 0f)
}
-
- // Important note: toScene and distance will be updated right after this function is called,
- // using fromScene and dragOffset.
}
- private class TargetScene(
- val sceneKey: SceneKey,
- val distance: Float,
- )
-
- private fun Scene.findTargetSceneAndDistance(directionOffset: Float): TargetScene {
- val upOrLeft = swipeTransition.upOrLeft(this)
- val downOrRight = swipeTransition.downOrRight(this)
+ // TODO(b/290184746): there are two bugs here:
+ // 1. if both upOrLeft and downOrRight become `null` during a transition this will crash
+ // 2. if one of them changes during a transition, the transition will jump cut to the new target
+ private inline fun Scene.findTargetSceneAndDistance(
+ directionOffset: Float
+ ): Pair<Scene, Float> {
+ val upOrLeft = upOrLeft()
+ val downOrRight = downOrRight()
+ val absoluteDistance = getAbsoluteDistance()
// Compute the target scene depending on the current offset.
- return when {
- directionOffset < 0f && upOrLeft != null -> {
- TargetScene(
- sceneKey = upOrLeft,
- distance = -swipeTransition.absoluteDistance,
- )
- }
- directionOffset > 0f && downOrRight != null -> {
- TargetScene(
- sceneKey = downOrRight,
- distance = swipeTransition.absoluteDistance,
- )
- }
- else -> {
- TargetScene(
- sceneKey = key,
- distance = 0f,
- )
- }
+ return if ((directionOffset < 0f && upOrLeft != null) || downOrRight == null) {
+ Pair(layoutImpl.scene(upOrLeft!!), -absoluteDistance)
+ } else {
+ Pair(layoutImpl.scene(downOrRight), absoluteDistance)
}
}
+ private fun Scene.upOrLeft(): SceneKey? {
+ return userActions[actionUpOrLeft] ?: userActions[actionUpOrLeftNoEdge]
+ }
+
+ private fun Scene.downOrRight(): SceneKey? {
+ return userActions[actionDownOrRight] ?: userActions[actionDownOrRightNoEdge]
+ }
+
internal fun onDragStopped(velocity: Float, canChangeScene: Boolean) {
// The state was changed since the drag started; don't do anything.
if (!isDrivingTransition) {
@@ -291,11 +286,6 @@ internal class SceneGestureHandler(
// velocity and offset of the transition, then we launch the animation.
val toScene = swipeTransition._toScene
- if (fromScene == toScene) {
- // We were not animating.
- transitionState = TransitionState.Idle(fromScene.key)
- return
- }
// Compute the destination scene (and therefore offset) to settle in.
val offset = swipeTransition.dragOffset
@@ -322,12 +312,14 @@ internal class SceneGestureHandler(
if (startFromIdlePosition) {
// If there is a next scene, we start the overscroll animation.
- val target = fromScene.findTargetSceneAndDistance(velocity)
- val isValidTarget = target.distance != 0f && target.sceneKey != fromScene.key
+ val (targetScene, distance) = fromScene.findTargetSceneAndDistance(velocity)
+ val isValidTarget = distance != 0f && targetScene.key != fromScene.key
if (isValidTarget) {
- swipeTransition._toScene = layoutImpl.scene(target.sceneKey)
- swipeTransition._distance = target.distance
-
+ updateTransition(
+ SwipeTransition(fromScene, targetScene, distance).apply {
+ _currentScene = swipeTransition._currentScene
+ }
+ )
animateTo(targetScene = fromScene, targetOffset = 0f)
} else {
// We will not animate
@@ -382,10 +374,10 @@ internal class SceneGestureHandler(
) {
swipeTransition.startOffsetAnimation {
coroutineScope.launch {
- if (!isAnimatingOffset) {
+ if (!swipeTransition.isAnimatingOffset) {
swipeTransition.offsetAnimatable.snapTo(swipeTransition.dragOffset)
}
- isAnimatingOffset = true
+ swipeTransition.isAnimatingOffset = true
swipeTransition.offsetAnimatable.animateTo(
targetOffset,
@@ -397,7 +389,7 @@ internal class SceneGestureHandler(
initialVelocity = initialVelocity,
)
- isAnimatingOffset = false
+ swipeTransition.finishOffsetAnimation()
// Now that the animation is done, the state should be idle. Note that if the state
// was changed since this animation started, some external code changed it and we
@@ -410,29 +402,26 @@ internal class SceneGestureHandler(
}
}
- internal class SwipeTransition(initialScene: Scene) : TransitionState.Transition {
- var _currentScene by mutableStateOf(initialScene)
+ internal class SwipeTransition(
+ val _fromScene: Scene,
+ val _toScene: Scene,
+ /**
+ * The signed distance between [fromScene] and [toScene]. It is negative if [fromScene] is
+ * above or to the left of [toScene].
+ */
+ val distance: Float
+ ) : TransitionState.Transition {
+ var _currentScene by mutableStateOf(_fromScene)
override val currentScene: SceneKey
get() = _currentScene.key
- var _fromScene by mutableStateOf(initialScene)
- override val fromScene: SceneKey
- get() = _fromScene.key
+ override val fromScene: SceneKey = _fromScene.key
- var _toScene by mutableStateOf(initialScene)
- override val toScene: SceneKey
- get() = _toScene.key
+ override val toScene: SceneKey = _toScene.key
override val progress: Float
get() {
val offset = if (isAnimatingOffset) offsetAnimatable.value else dragOffset
- if (distance == 0f) {
- // This can happen only if fromScene == toScene.
- error(
- "Transition.progress should be called only when Transition.fromScene != " +
- "Transition.toScene"
- )
- }
return offset / distance
}
@@ -459,46 +448,22 @@ internal class SceneGestureHandler(
/** Ends any previous [offsetAnimationJob] and runs the new [job]. */
fun startOffsetAnimation(job: () -> Job) {
- stopOffsetAnimation()
+ cancelOffsetAnimation()
offsetAnimationJob = job()
}
- /** Stops any ongoing offset animation. */
- fun stopOffsetAnimation() {
+ /** Cancel any ongoing offset animation. */
+ fun cancelOffsetAnimation() {
offsetAnimationJob?.cancel()
+ finishOffsetAnimation()
+ }
+ fun finishOffsetAnimation() {
if (isAnimatingOffset) {
isAnimatingOffset = false
dragOffset = offsetAnimatable.value
}
}
-
- /** The absolute distance between [fromScene] and [toScene]. */
- var absoluteDistance = 0f
-
- /**
- * The signed distance between [fromScene] and [toScene]. It is negative if [fromScene] is
- * above or to the left of [toScene].
- */
- var _distance by mutableFloatStateOf(0f)
- val distance: Float
- get() = _distance
-
- /** The [UserAction]s associated to this swipe. */
- var actionUpOrLeft: UserAction = Back
- var actionDownOrRight: UserAction = Back
- var actionUpOrLeftNoEdge: UserAction? = null
- var actionDownOrRightNoEdge: UserAction? = null
-
- fun upOrLeft(scene: Scene): SceneKey? {
- return scene.userActions[actionUpOrLeft]
- ?: actionUpOrLeftNoEdge?.let { scene.userActions[it] }
- }
-
- fun downOrRight(scene: Scene): SceneKey? {
- return scene.userActions[actionDownOrRight]
- ?: actionDownOrRightNoEdge?.let { scene.userActions[it] }
- }
}
companion object {
@@ -509,9 +474,9 @@ internal class SceneGestureHandler(
private class SceneDraggableHandler(
private val gestureHandler: SceneGestureHandler,
) : DraggableHandler {
- override fun onDragStarted(layoutSize: IntSize, startedPosition: Offset, pointersDown: Int) {
+ override fun onDragStarted(startedPosition: Offset, overSlop: Float, pointersDown: Int) {
gestureHandler.gestureWithPriority = this
- gestureHandler.onDragStarted(pointersDown, layoutSize, startedPosition)
+ gestureHandler.onDragStarted(pointersDown, startedPosition, overSlop)
}
override fun onDelta(pixels: Float) {
@@ -589,7 +554,7 @@ internal class SceneNestedScrollHandler(
// The progress value can go beyond this range in the case of overscroll.
val shouldSnapToIdle = isProgressCloseTo(0f) || isProgressCloseTo(1f)
if (shouldSnapToIdle) {
- gestureHandler.swipeTransition.stopOffsetAnimation()
+ gestureHandler.swipeTransition.cancelOffsetAnimation()
gestureHandler.transitionState =
TransitionState.Idle(gestureHandler.swipeTransition.currentScene)
}
@@ -639,12 +604,12 @@ internal class SceneNestedScrollHandler(
behavior.canStartOnPostFling && hasNextScene(velocityAvailable)
},
canContinueScroll = { true },
- onStart = {
+ onStart = { offsetAvailable ->
gestureHandler.gestureWithPriority = this
gestureHandler.onDragStarted(
pointersDown = 1,
- layoutSize = gestureHandler.currentScene.targetSize,
startedPosition = null,
+ overSlop = offsetAvailable,
)
},
onScroll = { offsetAvailable ->
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index 2c78dee56bbc..116a66673d0a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -46,7 +46,7 @@ internal fun Modifier.swipeToScene(gestureHandler: SceneGestureHandler): Modifie
// user can't swipe in the other direction.
startDragImmediately =
gestureHandler.isDrivingTransition &&
- gestureHandler.isAnimatingOffset &&
+ gestureHandler.swipeTransition.isAnimatingOffset &&
!canOppositeSwipe,
onDragStarted = gestureHandler.draggable::onDragStarted,
onDragDelta = gestureHandler.draggable::onDelta,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
index a5fd1bfb72e6..c49a2b8bbe32 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
@@ -38,7 +38,7 @@ class PriorityNestedScrollConnection(
private val canStartPostScroll: (offsetAvailable: Offset, offsetBeforeStart: Offset) -> Boolean,
private val canStartPostFling: (velocityAvailable: Velocity) -> Boolean,
private val canContinueScroll: () -> Boolean,
- private val onStart: () -> Unit,
+ private val onStart: (offsetAvailable: Offset) -> Unit,
private val onScroll: (offsetAvailable: Offset) -> Offset,
private val onStop: (velocityAvailable: Velocity) -> Velocity,
) : NestedScrollConnection {
@@ -131,7 +131,7 @@ class PriorityNestedScrollConnection(
// Note: onStop will be called if we cannot continue to scroll (step 3a), or the finger is
// lifted (step 3b), or this object has been destroyed (step 3c).
- onStart()
+ onStart(available)
return onScroll(available)
}
@@ -156,7 +156,7 @@ fun PriorityNestedScrollConnection(
canStartPostScroll: (offsetAvailable: Float, offsetBeforeStart: Float) -> Boolean,
canStartPostFling: (velocityAvailable: Float) -> Boolean,
canContinueScroll: () -> Boolean,
- onStart: () -> Unit,
+ onStart: (offsetAvailable: Float) -> Unit,
onScroll: (offsetAvailable: Float) -> Float,
onStop: (velocityAvailable: Float) -> Float,
) =
@@ -172,7 +172,7 @@ fun PriorityNestedScrollConnection(
canStartPostFling(velocityAvailable.toFloat())
},
canContinueScroll = canContinueScroll,
- onStart = onStart,
+ onStart = { offsetAvailable -> onStart(offsetAvailable.toFloat()) },
onScroll = { offsetAvailable: Offset ->
onScroll(offsetAvailable.toFloat()).toOffset()
},
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 b84cb360d760..b40716555a2a 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
@@ -191,8 +191,13 @@ class SceneGestureHandlerTest {
runMonotonicClockTest { TestGestureScope(coroutineScope = this).block() }
}
- private fun DraggableHandler.onDragStarted() {
- onDragStarted(layoutSize = LAYOUT_SIZE, startedPosition = Offset.Zero)
+ private fun DraggableHandler.onDragStarted(
+ overSlop: Float = 0f,
+ startedPosition: Offset = Offset.Zero,
+ ) {
+ onDragStarted(startedPosition, overSlop)
+ // MultiPointerDraggable will always call onDelta with the initial overSlop right after
+ onDelta(overSlop)
}
@Test fun testPreconditions() = runGestureTest { assertIdle(currentScene = SceneA) }
@@ -291,6 +296,86 @@ class SceneGestureHandlerTest {
}
@Test
+ fun onDragStartedWithoutActionsInBothDirections_stayIdle() = runGestureTest {
+ horizontalSceneGestureHandler.draggable.onDragStarted(up(0.3f))
+ assertIdle(currentScene = SceneA)
+ horizontalSceneGestureHandler.draggable.onDragStarted(down(0.3f))
+ assertIdle(currentScene = SceneA)
+ }
+
+ @Test
+ fun onDragIntoNoAction_startTransitionToOppositeDirection() = runGestureTest {
+ navigateToSceneC()
+
+ // We are on SceneC which has no action in Down direction
+ draggable.onDragStarted(down(0.1f))
+ assertTransition(
+ currentScene = SceneC,
+ fromScene = SceneC,
+ toScene = SceneB,
+ progress = -0.1f
+ )
+
+ // Reverse drag direction, it will consume the previous drag
+ draggable.onDelta(up(0.1f))
+ assertTransition(
+ currentScene = SceneC,
+ fromScene = SceneC,
+ toScene = SceneB,
+ progress = 0.0f
+ )
+
+ // Continue reverse drag direction, it should record progress to Scene B
+ draggable.onDelta(up(0.1f))
+ assertTransition(
+ currentScene = SceneC,
+ fromScene = SceneC,
+ toScene = SceneB,
+ progress = 0.1f
+ )
+ }
+
+ @Test
+ fun onDragFromEdge_startTransitionToEdgeAction() = runGestureTest {
+ navigateToSceneC()
+
+ // Start dragging from the bottom
+ draggable.onDragStarted(up(0.1f), Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE))
+ assertTransition(
+ currentScene = SceneC,
+ fromScene = SceneC,
+ toScene = SceneA,
+ progress = 0.1f
+ )
+ }
+
+ @Test
+ fun onDragToExactlyZero_toSceneIsSet() = runGestureTest {
+ draggable.onDragStarted(down(0.3f))
+ assertTransition(
+ currentScene = SceneA,
+ fromScene = SceneA,
+ toScene = SceneC,
+ progress = 0.3f
+ )
+ draggable.onDelta(up(0.3f))
+ assertTransition(
+ currentScene = SceneA,
+ fromScene = SceneA,
+ toScene = SceneC,
+ progress = 0.0f
+ )
+ }
+
+ private fun TestGestureScope.navigateToSceneC() {
+ assertIdle(currentScene = SceneA)
+ draggable.onDragStarted(down(1f))
+ draggable.onDragStopped(0f)
+ advanceUntilIdle()
+ assertIdle(currentScene = SceneC)
+ }
+
+ @Test
fun onAccelaratedScroll_scrollToThirdScene() = runGestureTest {
// Drag A -> B with progress 0.2
draggable.onDragStarted()
@@ -339,17 +424,17 @@ class SceneGestureHandlerTest {
)
// The stop animation is not started yet
- assertThat(sceneGestureHandler.isAnimatingOffset).isFalse()
+ assertThat(sceneGestureHandler.swipeTransition.isAnimatingOffset).isFalse()
runCurrent()
- assertThat(sceneGestureHandler.isAnimatingOffset).isTrue()
+ assertThat(sceneGestureHandler.swipeTransition.isAnimatingOffset).isTrue()
assertThat(sceneGestureHandler.isDrivingTransition).isTrue()
assertTransition(currentScene = SceneC)
// Start a new gesture while the offset is animating
draggable.onDragStarted()
- assertThat(sceneGestureHandler.isAnimatingOffset).isFalse()
+ assertThat(sceneGestureHandler.swipeTransition.isAnimatingOffset).isFalse()
}
@Test