diff options
| author | 2024-02-27 13:04:03 +0000 | |
|---|---|---|
| committer | 2024-02-27 16:24:23 +0000 | |
| commit | eb24089326dd00481e090bb01adf0d792a45d950 (patch) | |
| tree | 15ba7eb005a97ece826c9738800c90e8dd244c30 | |
| parent | 91b5d8c435d93b6e1847c8151ecc7bf99c5bff8d (diff) | |
DragController manage the scene transitions, when active
This refactoring removes the currentSource and eliminates the caller's
need to be aware of it.
Instead, we introduce a onDragStarted method that returns the
DragController.
The DragController provides control over the transition between two
scenes through the onDrag and onStop methods.
One of the goals of this refactor is to be able to remove the source
that indicates whether a gesture can be consumed.
The DragController can check whether it is currently driving the
transition without knowing what the source is, for example the
`SceneGestureHandlerTest.startNestedScrollWhileDragging()` test).
Test: atest DragHandlerImplTest
Bug: 317063114
Flag: NA
Change-Id: I9496e1f52643576cee873c2ddee861c265cafa6a
8 files changed, 368 insertions, 360 deletions
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/DraggableHandler.kt index 187d82a9e626..b94e49bb0edc 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/DraggableHandler.kt @@ -36,44 +36,38 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch -internal class SceneGestureHandler( - internal val layoutImpl: SceneTransitionLayoutImpl, - internal val orientation: Orientation, - private val coroutineScope: CoroutineScope, -) { - private val layoutState = layoutImpl.state - val draggable: DraggableHandler = SceneDraggableHandler(this) - - private var _swipeTransition: SwipeTransition? = null - private var swipeTransition: SwipeTransition - get() = _swipeTransition ?: error("SwipeTransition needs to be initialized") - set(value) { - _swipeTransition = value - } +interface DraggableHandler { + /** + * Start a drag in the given [startedPosition], with the given [overSlop] and number of + * [pointersDown]. + * + * The returned [DragController] should be used to continue or stop the drag. + */ + fun onDragStarted(startedPosition: Offset?, overSlop: Float, pointersDown: Int): DragController +} - private fun updateTransition(newTransition: SwipeTransition, force: Boolean = false) { - if (isDrivingTransition || force) { - layoutState.startTransition(newTransition, newTransition.key) +/** + * The [DragController] provides control over the transition between two scenes through the [onDrag] + * and [onStop] methods. + */ +interface DragController { + /** Drag the current scene by [delta] pixels. */ + fun onDrag(delta: Float) - // Initialize SwipeTransition.transformationSpec and .swipeSpec. Note that this must be - // called right after layoutState.startTransition() is called, because it computes the - // current layoutState.transformationSpec(). - val transformationSpec = layoutState.transformationSpec - newTransition.transformationSpec = transformationSpec - newTransition.swipeSpec = - transformationSpec.swipeSpec ?: layoutState.transitions.defaultSwipeSpec - } else { - // We were not driving the transition and we don't force the update, so the specs won't - // be used and it doesn't matter which ones we set here. - newTransition.transformationSpec = TransformationSpec.Empty - newTransition.swipeSpec = SceneTransitions.DefaultSwipeSpec - } + /** Starts a transition to a target scene. */ + fun onStop(velocity: Float, canChangeScene: Boolean) +} - swipeTransition = newTransition - } +internal class DraggableHandlerImpl( + internal val layoutImpl: SceneTransitionLayoutImpl, + internal val orientation: Orientation, + internal val coroutineScope: CoroutineScope, +) : DraggableHandler { + /** The [DraggableHandler] can only have one active [DragController] at a time. */ + private var dragController: DragControllerImpl? = null - internal val isDrivingTransition - get() = layoutState.transitionState == _swipeTransition + internal val isDrivingTransition: Boolean + get() = dragController?.isDrivingTransition == true /** * The velocity threshold at which the intent of the user is to swipe up or down. It is the same @@ -86,14 +80,9 @@ internal class SceneGestureHandler( * The positional threshold at which the intent of the user is to swipe to the next scene. It is * the same as SwipeableV2Defaults.PositionalThreshold. */ - private val positionalThreshold + internal val positionalThreshold get() = with(layoutImpl.density) { 56.dp.toPx() } - internal var currentSource: Any? = null - - /** The [Swipes] associated to the current gesture. */ - private var swipes: Swipes? = null - /** * Whether we should immediately intercept a gesture. * @@ -102,35 +91,52 @@ internal class SceneGestureHandler( */ internal fun shouldImmediatelyIntercept(startedPosition: Offset?): Boolean { // We don't intercept the touch if we are not currently driving the transition. - if (!isDrivingTransition) { + val dragController = dragController + if (dragController?.isDrivingTransition != true) { return false } // Only intercept the current transition if one of the 2 swipes results is also a transition // between the same pair of scenes. + val swipeTransition = dragController.swipeTransition val fromScene = swipeTransition._currentScene val swipes = computeSwipes(fromScene, startedPosition, pointersDown = 1) - val (upOrLeft, downOrRight) = computeSwipesResults(fromScene, swipes) + val (upOrLeft, downOrRight) = swipes.computeSwipesResults(fromScene) return (upOrLeft != null && swipeTransition.isTransitioningBetween(fromScene.key, upOrLeft.toScene)) || (downOrRight != null && swipeTransition.isTransitioningBetween(fromScene.key, downOrRight.toScene)) } - internal fun onDragStarted(pointersDown: Int, startedPosition: Offset?, overSlop: Float) { + override fun onDragStarted( + startedPosition: Offset?, + overSlop: Float, + pointersDown: Int, + ): DragController { if (overSlop == 0f) { - check(isDrivingTransition) { - "onDragStarted() called while isDrivingTransition=false overSlop=0f" + val oldDragController = dragController + check(oldDragController != null && oldDragController.isDrivingTransition) { + val isActive = oldDragController?.isDrivingTransition + "onDragStarted(overSlop=0f) requires an active dragController, but was $isActive" } // This [transition] was already driving the animation: simply take over it. // Stop animating and start from where the current offset. - swipeTransition.cancelOffsetAnimation() - swipes!!.updateSwipesResults(swipeTransition._fromScene) - return + oldDragController.swipeTransition.cancelOffsetAnimation() + + // We need to recompute the swipe results since this is a new gesture, and the + // fromScene.userActions may have changed. + val swipes = oldDragController.swipes + swipes.updateSwipesResults(oldDragController.swipeTransition._fromScene) + + // A new gesture should always create a new SwipeTransition. This way there cannot be + // different gestures controlling the same transition. + val swipeTransition = SwipeTransition(oldDragController.swipeTransition) + swipes.updateSwipesResults(fromScene = swipeTransition._fromScene) + return updateDragController(swipes, swipeTransition) } - val transitionState = layoutState.transitionState + val transitionState = layoutImpl.state.transitionState if (transitionState is TransitionState.Transition) { // TODO(b/290184746): Better handle interruptions here if state != idle. Log.w( @@ -142,24 +148,27 @@ internal class SceneGestureHandler( } val fromScene = layoutImpl.scene(transitionState.currentScene) - val newSwipes = computeSwipes(fromScene, startedPosition, pointersDown) - swipes = newSwipes - val result = newSwipes.findUserActionResult(fromScene, overSlop, true) + val swipes = computeSwipes(fromScene, startedPosition, pointersDown) + val result = swipes.findUserActionResult(fromScene, overSlop, true) // As we were unable to locate a valid target scene, the initial SwipeTransition cannot be - // defined. - if (result == null) return + // defined. Consequently, a simple NoOp Controller will be returned. + if (result == null) return NoOpDragController - val newSwipeTransition = - SwipeTransition( - fromScene = fromScene, - result = result, - swipes = newSwipes, - layoutImpl = layoutImpl, - orientation = orientation - ) + return updateDragController( + swipes = swipes, + swipeTransition = SwipeTransition(fromScene, result, swipes, layoutImpl, orientation) + ) + } - updateTransition(newSwipeTransition, force = true) + private fun updateDragController( + swipes: Swipes, + swipeTransition: SwipeTransition + ): DragController { + val newDragController = DragControllerImpl(this, swipes, swipeTransition) + newDragController.updateTransition(swipeTransition, force = true) + dragController = newDragController + return newDragController } private fun computeSwipes( @@ -216,7 +225,58 @@ internal class SceneGestureHandler( } } - internal fun onDrag(delta: Float) { + companion object { + private const val TAG = "DraggableHandlerImpl" + } +} + +/** @param swipes The [Swipes] associated to the current gesture. */ +private class DragControllerImpl( + private val draggableHandler: DraggableHandlerImpl, + val swipes: Swipes, + var swipeTransition: SwipeTransition, +) : DragController { + val layoutState = draggableHandler.layoutImpl.state + + /** + * Whether this handle is active. If this returns false, calling [onDrag] and [onStop] will do + * nothing. We should have only one active controller at a time + */ + val isDrivingTransition: Boolean + get() = layoutState.transitionState == swipeTransition + + init { + check(!isDrivingTransition) { "Multiple controllers with the same SwipeTransition" } + } + + fun updateTransition(newTransition: SwipeTransition, force: Boolean = false) { + if (isDrivingTransition || force) { + layoutState.startTransition(newTransition, newTransition.key) + + // Initialize SwipeTransition.transformationSpec and .swipeSpec. Note that this must be + // called right after layoutState.startTransition() is called, because it computes the + // current layoutState.transformationSpec(). + val transformationSpec = layoutState.transformationSpec + newTransition.transformationSpec = transformationSpec + newTransition.swipeSpec = + transformationSpec.swipeSpec ?: layoutState.transitions.defaultSwipeSpec + } else { + // We were not driving the transition and we don't force the update, so the specs won't + // be used and it doesn't matter which ones we set here. + newTransition.transformationSpec = TransformationSpec.Empty + newTransition.swipeSpec = SceneTransitions.DefaultSwipeSpec + } + + swipeTransition = newTransition + } + + /** + * We receive a [delta] that can be consumed to change the offset of the current + * [SwipeTransition]. + * + * @return the consumed delta + */ + override fun onDrag(delta: Float) { if (delta == 0f || !isDrivingTransition) return swipeTransition.dragOffset += delta @@ -225,14 +285,14 @@ internal class SceneGestureHandler( val isNewFromScene = fromScene.key != swipeTransition.fromScene val result = - swipes!!.findUserActionResult( + swipes.findUserActionResult( fromScene = fromScene, directionOffset = swipeTransition.dragOffset, updateSwipesResults = isNewFromScene ) if (result == null) { - onDragStopped(velocity = delta, canChangeScene = true) + onStop(velocity = delta, canChangeScene = true) return } @@ -243,34 +303,18 @@ internal class SceneGestureHandler( result.toScene != swipeTransition.toScene || result.transitionKey != swipeTransition.key ) { - val newSwipeTransition = + val swipeTransition = SwipeTransition( fromScene = fromScene, result = result, - swipes = swipes!!, - layoutImpl = layoutImpl, - orientation = orientation + swipes = swipes, + layoutImpl = draggableHandler.layoutImpl, + orientation = draggableHandler.orientation, ) .apply { dragOffset = swipeTransition.dragOffset } - updateTransition(newSwipeTransition) - } - } - - private fun computeSwipesResults( - fromScene: Scene, - swipes: Swipes - ): Pair<UserActionResult?, UserActionResult?> { - val userActions = fromScene.userActions - fun sceneToSwipePair(swipe: Swipe?): UserActionResult? { - return userActions[swipe ?: return null] + updateTransition(swipeTransition) } - - val upOrLeftResult = - sceneToSwipePair(swipes.upOrLeft) ?: sceneToSwipePair(swipes.upOrLeftNoSource) - val downOrRightResult = - sceneToSwipePair(swipes.downOrRight) ?: sceneToSwipePair(swipes.downOrRightNoSource) - return Pair(upOrLeftResult, downOrRightResult) } /** @@ -302,18 +346,22 @@ internal class SceneGestureHandler( // to the next screen or go back to the previous one. val offset = swipeTransition.dragOffset val absoluteDistance = distance.absoluteValue - return if (offset <= -absoluteDistance && swipes!!.upOrLeftResult?.toScene == toScene.key) { + return if (offset <= -absoluteDistance && swipes.upOrLeftResult?.toScene == toScene.key) { toScene to absoluteDistance - } else if ( - offset >= absoluteDistance && swipes!!.downOrRightResult?.toScene == toScene.key - ) { + } else if (offset >= absoluteDistance && swipes.downOrRightResult?.toScene == toScene.key) { toScene to -absoluteDistance } else { fromScene to 0f } } - internal fun onDragStopped(velocity: Float, canChangeScene: Boolean) { + private fun snapToScene(scene: SceneKey) { + if (!isDrivingTransition) return + swipeTransition.cancelOffsetAnimation() + layoutState.finishTransition(swipeTransition, idleScene = scene) + } + + override fun onStop(velocity: Float, canChangeScene: Boolean) { // The state was changed since the drag started; don't do anything. if (!isDrivingTransition) { return @@ -332,16 +380,16 @@ internal class SceneGestureHandler( // immediately go back B => A. if (targetScene != swipeTransition._currentScene) { swipeTransition._currentScene = targetScene - with(layoutImpl.state) { coroutineScope.onChangeScene(targetScene.key) } + with(draggableHandler.layoutImpl.state) { + draggableHandler.coroutineScope.onChangeScene(targetScene.key) + } } swipeTransition.animateOffset( - coroutineScope = coroutineScope, + coroutineScope = draggableHandler.coroutineScope, initialVelocity = velocity, targetOffset = targetOffset, - onAnimationCompleted = { - layoutState.finishTransition(swipeTransition, idleScene = targetScene.key) - } + onAnimationCompleted = { snapToScene(targetScene.key) } ) } @@ -400,10 +448,10 @@ internal class SceneGestureHandler( if (startFromIdlePosition) { // If there is a target scene, we start the overscroll animation. - val result = swipes!!.findUserActionResultStrict(velocity) + val result = swipes.findUserActionResultStrict(velocity) if (result == null) { // We will not animate - layoutState.finishTransition(swipeTransition, idleScene = fromScene.key) + snapToScene(fromScene.key) return } @@ -411,9 +459,9 @@ internal class SceneGestureHandler( SwipeTransition( fromScene = fromScene, result = result, - swipes = swipes!!, - layoutImpl = layoutImpl, - orientation = orientation + swipes = swipes, + layoutImpl = draggableHandler.layoutImpl, + orientation = draggableHandler.orientation, ) .apply { _currentScene = swipeTransition._currentScene } @@ -440,6 +488,9 @@ internal class SceneGestureHandler( return (offset - distance).absoluteValue < offset.absoluteValue } + val velocityThreshold = draggableHandler.velocityThreshold + val positionalThreshold = draggableHandler.positionalThreshold + // Swiping up or left. if (distance < 0f) { return if (offset > 0f || velocity >= velocityThreshold) { @@ -460,10 +511,6 @@ internal class SceneGestureHandler( isCloserToTarget() } } - - companion object { - private const val TAG = "SceneGestureHandler" - } } private fun SwipeTransition( @@ -492,11 +539,26 @@ private fun SwipeTransition( ) } +private fun SwipeTransition(old: SwipeTransition): SwipeTransition { + return SwipeTransition( + key = old.key, + _fromScene = old._fromScene, + _toScene = old._toScene, + userActionDistanceScope = old.userActionDistanceScope, + orientation = old.orientation, + isUpOrLeft = old.isUpOrLeft + ) + .apply { + _currentScene = old._currentScene + dragOffset = old.dragOffset + } +} + private class SwipeTransition( val key: TransitionKey?, val _fromScene: Scene, val _toScene: Scene, - private val userActionDistanceScope: UserActionDistanceScope, + val userActionDistanceScope: UserActionDistanceScope, override val orientation: Orientation, override val isUpOrLeft: Boolean, ) : @@ -730,40 +792,16 @@ private class Swipes( } } -private class SceneDraggableHandler( - private val gestureHandler: SceneGestureHandler, -) : DraggableHandler { - private val source = this - - override fun onDragStarted(startedPosition: Offset, overSlop: Float, pointersDown: Int) { - gestureHandler.currentSource = source - gestureHandler.onDragStarted(pointersDown, startedPosition, overSlop) - } - - override fun onDelta(pixels: Float) { - if (gestureHandler.currentSource == source) { - gestureHandler.onDrag(delta = pixels) - } - } - - override fun onDragStopped(velocity: Float) { - if (gestureHandler.currentSource == source) { - gestureHandler.currentSource = null - gestureHandler.onDragStopped(velocity = velocity, canChangeScene = true) - } - } -} - -internal class SceneNestedScrollHandler( +internal class NestedScrollHandlerImpl( private val layoutImpl: SceneTransitionLayoutImpl, private val orientation: Orientation, private val topOrLeftBehavior: NestedScrollBehavior, private val bottomOrRightBehavior: NestedScrollBehavior, -) : NestedScrollHandler { +) { private val layoutState = layoutImpl.state - private val gestureHandler = layoutImpl.gestureHandler(orientation) + private val draggableHandler = layoutImpl.draggableHandler(orientation) - override val connection: PriorityNestedScrollConnection = nestedScrollConnection() + val connection: PriorityNestedScrollConnection = nestedScrollConnection() private fun nestedScrollConnection(): PriorityNestedScrollConnection { // If we performed a long gesture before entering priority mode, we would have to avoid @@ -808,7 +846,7 @@ internal class SceneNestedScrollHandler( return overscrollSpec != null } - val source = this + var dragController: DragController? = null var isIntercepting = false return PriorityNestedScrollConnection( @@ -819,7 +857,7 @@ internal class SceneNestedScrollHandler( val canInterceptSwipeTransition = canChangeScene && offsetAvailable != 0f && - gestureHandler.shouldImmediatelyIntercept(startedPosition = null) + draggableHandler.shouldImmediatelyIntercept(startedPosition = null) if (!canInterceptSwipeTransition) return@PriorityNestedScrollConnection false val threshold = layoutImpl.transitionInterceptionThreshold @@ -893,34 +931,28 @@ internal class SceneNestedScrollHandler( canContinueScroll = { true }, canScrollOnFling = false, onStart = { offsetAvailable -> - gestureHandler.currentSource = source - gestureHandler.onDragStarted( - pointersDown = 1, - startedPosition = null, - overSlop = if (isIntercepting) 0f else offsetAvailable, - ) + dragController = + draggableHandler.onDragStarted( + pointersDown = 1, + startedPosition = null, + overSlop = if (isIntercepting) 0f else offsetAvailable, + ) }, onScroll = { offsetAvailable -> - if (gestureHandler.currentSource != source) { - return@PriorityNestedScrollConnection 0f - } + val controller = dragController ?: error("Should be called after onStart") // TODO(b/297842071) We should handle the overscroll or slow drag if the gesture is // initiated in a nested child. - gestureHandler.onDrag(offsetAvailable) + controller.onDrag(delta = offsetAvailable) offsetAvailable }, onStop = { velocityAvailable -> - if (gestureHandler.currentSource != source) { - return@PriorityNestedScrollConnection 0f - } + val controller = dragController ?: error("Should be called after onStart") - gestureHandler.onDragStopped( - velocity = velocityAvailable, - canChangeScene = canChangeScene - ) + controller.onStop(velocity = velocityAvailable, canChangeScene = canChangeScene) + dragController = null // The onDragStopped animation consumes any remaining velocity. velocityAvailable }, @@ -935,3 +967,9 @@ internal class SceneNestedScrollHandler( // TODO(b/290184746): Have a better default visibility threshold which takes the swipe distance into // account instead. internal const val OffsetVisibilityThreshold = 0.5f + +private object NoOpDragController : DragController { + override fun onDrag(delta: Float) {} + + override fun onStop(velocity: Float, canChangeScene: Boolean) {} +} 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 deleted file mode 100644 index 58052cd60f39..000000000000 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.android.compose.animation.scene - -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection - -interface DraggableHandler { - fun onDragStarted(startedPosition: Offset, overSlop: Float, pointersDown: Int = 1) - fun onDelta(pixels: Float) - fun onDragStopped(velocity: Float) -} - -interface NestedScrollHandler { - val connection: NestedScrollConnection -} 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 3ff869b5fdad..05dd5cc09dbf 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 @@ -24,7 +24,6 @@ import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation import androidx.compose.foundation.gestures.horizontalDrag import androidx.compose.foundation.gestures.verticalDrag import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.pointer.PointerEvent @@ -33,7 +32,6 @@ import androidx.compose.ui.input.pointer.PointerId import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerInputScope import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.positionChange import androidx.compose.ui.input.pointer.util.VelocityTracker import androidx.compose.ui.input.pointer.util.addPointerInputChange @@ -69,9 +67,7 @@ internal fun Modifier.multiPointerDraggable( orientation: Orientation, enabled: () -> Boolean, startDragImmediately: (startedPosition: Offset) -> Boolean, - onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit, - onDragDelta: (delta: Float) -> Unit, - onDragStopped: (velocity: Float) -> Unit, + onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, ): Modifier = this.then( MultiPointerDraggableElement( @@ -79,8 +75,6 @@ internal fun Modifier.multiPointerDraggable( enabled, startDragImmediately, onDragStarted, - onDragDelta, - onDragStopped, ) ) @@ -89,9 +83,7 @@ private data class MultiPointerDraggableElement( private val enabled: () -> Boolean, private val startDragImmediately: (startedPosition: Offset) -> Boolean, private val onDragStarted: - (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit, - private val onDragDelta: (Float) -> Unit, - private val onDragStopped: (velocity: Float) -> Unit, + (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, ) : ModifierNodeElement<MultiPointerDraggableNode>() { override fun create(): MultiPointerDraggableNode = MultiPointerDraggableNode( @@ -99,8 +91,6 @@ private data class MultiPointerDraggableElement( enabled = enabled, startDragImmediately = startDragImmediately, onDragStarted = onDragStarted, - onDragDelta = onDragDelta, - onDragStopped = onDragStopped, ) override fun update(node: MultiPointerDraggableNode) { @@ -108,8 +98,6 @@ private data class MultiPointerDraggableElement( node.enabled = enabled node.startDragImmediately = startDragImmediately node.onDragStarted = onDragStarted - node.onDragDelta = onDragDelta - node.onDragStopped = onDragStopped } } @@ -117,9 +105,8 @@ internal class MultiPointerDraggableNode( orientation: Orientation, enabled: () -> Boolean, var startDragImmediately: (startedPosition: Offset) -> Boolean, - var onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit, - var onDragDelta: (Float) -> Unit, - var onDragStopped: (velocity: Float) -> Unit, + var onDragStarted: + (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, ) : PointerInputModifierNode, DelegatingNode(), @@ -176,40 +163,33 @@ internal class MultiPointerDraggableNode( return } - val onDragStart: (Offset, Float, Int) -> Unit = { startedPosition, overSlop, pointersDown -> - velocityTracker.resetTracking() - onDragStarted(startedPosition, overSlop, pointersDown) - } - - val onDragCancel: () -> Unit = { onDragStopped(/* velocity= */ 0f) } - - val onDragEnd: () -> Unit = { - val maxFlingVelocity = - currentValueOf(LocalViewConfiguration).maximumFlingVelocity.let { max -> - Velocity(max, max) - } - - val velocity = velocityTracker.calculateVelocity(maxFlingVelocity) - onDragStopped( - when (orientation) { - Orientation.Horizontal -> velocity.x - Orientation.Vertical -> velocity.y - } - ) - } - - val onDrag: (change: PointerInputChange, dragAmount: Float) -> Unit = { change, amount -> - velocityTracker.addPointerInputChange(change) - onDragDelta(amount) - } - detectDragGestures( orientation = orientation, startDragImmediately = startDragImmediately, - onDragStart = onDragStart, - onDragEnd = onDragEnd, - onDragCancel = onDragCancel, - onDrag = onDrag, + onDragStart = { startedPosition, overSlop, pointersDown -> + velocityTracker.resetTracking() + onDragStarted(startedPosition, overSlop, pointersDown) + }, + onDrag = { controller, change, amount -> + velocityTracker.addPointerInputChange(change) + controller.onDrag(amount) + }, + onDragEnd = { controller -> + val viewConfiguration = currentValueOf(LocalViewConfiguration) + val maxVelocity = viewConfiguration.maximumFlingVelocity.let { Velocity(it, it) } + val velocity = velocityTracker.calculateVelocity(maxVelocity) + controller.onStop( + velocity = + when (orientation) { + Orientation.Horizontal -> velocity.x + Orientation.Vertical -> velocity.y + }, + canChangeScene = true, + ) + }, + onDragCancel = { controller -> + controller.onStop(velocity = 0f, canChangeScene = true) + }, ) } } @@ -225,10 +205,10 @@ internal class MultiPointerDraggableNode( private suspend fun PointerInputScope.detectDragGestures( orientation: Orientation, startDragImmediately: (startedPosition: Offset) -> Boolean, - onDragStart: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit, - onDragEnd: () -> Unit, - onDragCancel: () -> Unit, - onDrag: (change: PointerInputChange, dragAmount: Float) -> Unit, + onDragStart: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, + onDragEnd: (controller: DragController) -> Unit, + onDragCancel: (controller: DragController) -> Unit, + onDrag: (controller: DragController, change: PointerInputChange, dragAmount: Float) -> Unit, ) { awaitEachGesture { val initialDown = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial) @@ -282,34 +262,34 @@ private suspend fun PointerInputScope.detectDragGestures( } } - onDragStart(drag.position, overSlop, pressed.size) + val controller = onDragStart(drag.position, overSlop, pressed.size) val successful: Boolean try { - onDrag(drag, overSlop) + onDrag(controller, drag, overSlop) successful = when (orientation) { Orientation.Horizontal -> horizontalDrag(drag.id) { - onDrag(it, it.positionChange().x) + onDrag(controller, it, it.positionChange().x) it.consume() } Orientation.Vertical -> verticalDrag(drag.id) { - onDrag(it, it.positionChange().y) + onDrag(controller, it, it.positionChange().y) it.consume() } } } catch (t: Throwable) { - onDragCancel() + onDragCancel(controller) throw t } if (successful) { - onDragEnd() + onDragEnd(controller) } else { - onDragCancel() + onDragCancel(controller) } } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt index e78f3266d664..5a2f85ad163c 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt @@ -178,7 +178,7 @@ private fun scenePriorityNestedScrollConnection( topOrLeftBehavior: NestedScrollBehavior, bottomOrRightBehavior: NestedScrollBehavior, ) = - SceneNestedScrollHandler( + NestedScrollHandlerImpl( layoutImpl = layoutImpl, orientation = orientation, topOrLeftBehavior = topOrLeftBehavior, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index 3093d477a24c..1670e9cee731 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -102,8 +102,8 @@ internal class SceneTransitionLayoutImpl( .also { _sharedValues = it } // TODO(b/317958526): Lazily allocate scene gesture handlers the first time they are needed. - private val horizontalGestureHandler: SceneGestureHandler - private val verticalGestureHandler: SceneGestureHandler + private val horizontalDraggableHandler: DraggableHandlerImpl + private val verticalDraggableHandler: DraggableHandlerImpl private var _userActionDistanceScope: UserActionDistanceScope? = null internal val userActionDistanceScope: UserActionDistanceScope @@ -116,27 +116,27 @@ internal class SceneTransitionLayoutImpl( init { updateScenes(builder) - // SceneGestureHandler must wait for the scenes to be initialized, in order to access the + // DraggableHandlerImpl must wait for the scenes to be initialized, in order to access the // current scene (required for SwipeTransition). - horizontalGestureHandler = - SceneGestureHandler( + horizontalDraggableHandler = + DraggableHandlerImpl( layoutImpl = this, orientation = Orientation.Horizontal, coroutineScope = coroutineScope, ) - verticalGestureHandler = - SceneGestureHandler( + verticalDraggableHandler = + DraggableHandlerImpl( layoutImpl = this, orientation = Orientation.Vertical, coroutineScope = coroutineScope, ) } - internal fun gestureHandler(orientation: Orientation): SceneGestureHandler = + internal fun draggableHandler(orientation: Orientation): DraggableHandlerImpl = when (orientation) { - Orientation.Vertical -> verticalGestureHandler - Orientation.Horizontal -> horizontalGestureHandler + Orientation.Vertical -> verticalDraggableHandler + Orientation.Horizontal -> horizontalDraggableHandler } internal fun scene(key: SceneKey): Scene { @@ -192,8 +192,8 @@ internal class SceneTransitionLayoutImpl( // Handle horizontal and vertical swipes on this layout. // Note: order here is important and will give a slight priority to the vertical // swipes. - .swipeToScene(horizontalGestureHandler) - .swipeToScene(verticalGestureHandler) + .swipeToScene(horizontalDraggableHandler) + .swipeToScene(verticalDraggableHandler) .then(LayoutElement(layoutImpl = this)) ) { LookaheadScope { 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 61f497818c89..b618369c2369 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 @@ -31,39 +31,39 @@ import androidx.compose.ui.unit.IntSize * Configures the swipeable behavior of a [SceneTransitionLayout] depending on the current state. */ @Stable -internal fun Modifier.swipeToScene(gestureHandler: SceneGestureHandler): Modifier { - return this.then(SwipeToSceneElement(gestureHandler)) +internal fun Modifier.swipeToScene(draggableHandler: DraggableHandlerImpl): Modifier { + return this.then(SwipeToSceneElement(draggableHandler)) } private data class SwipeToSceneElement( - val gestureHandler: SceneGestureHandler, + val draggableHandler: DraggableHandlerImpl, ) : ModifierNodeElement<SwipeToSceneNode>() { - override fun create(): SwipeToSceneNode = SwipeToSceneNode(gestureHandler) + override fun create(): SwipeToSceneNode = SwipeToSceneNode(draggableHandler) override fun update(node: SwipeToSceneNode) { - node.gestureHandler = gestureHandler + node.draggableHandler = draggableHandler } } private class SwipeToSceneNode( - gestureHandler: SceneGestureHandler, + draggableHandler: DraggableHandlerImpl, ) : DelegatingNode(), PointerInputModifierNode { private val delegate = delegate( MultiPointerDraggableNode( - orientation = gestureHandler.orientation, + orientation = draggableHandler.orientation, enabled = ::enabled, startDragImmediately = ::startDragImmediately, - onDragStarted = gestureHandler.draggable::onDragStarted, - onDragDelta = gestureHandler.draggable::onDelta, - onDragStopped = gestureHandler.draggable::onDragStopped, + onDragStarted = draggableHandler::onDragStarted, ) ) - var gestureHandler: SceneGestureHandler = gestureHandler + private var _draggableHandler = draggableHandler + var draggableHandler: DraggableHandlerImpl + get() = _draggableHandler set(value) { - if (value != field) { - field = value + if (_draggableHandler != value) { + _draggableHandler = value // Make sure to update the delegate orientation. Note that this will automatically // reset the underlying pointer input handler, so previous gestures will be @@ -81,12 +81,12 @@ private class SwipeToSceneNode( override fun onCancelPointerInput() = delegate.onCancelPointerInput() private fun enabled(): Boolean { - return gestureHandler.isDrivingTransition || - currentScene().shouldEnableSwipes(gestureHandler.orientation) + return draggableHandler.isDrivingTransition || + currentScene().shouldEnableSwipes(delegate.orientation) } private fun currentScene(): Scene { - val layoutImpl = gestureHandler.layoutImpl + val layoutImpl = draggableHandler.layoutImpl return layoutImpl.scene(layoutImpl.state.transitionState.currentScene) } @@ -98,12 +98,12 @@ private class SwipeToSceneNode( private fun startDragImmediately(startedPosition: Offset): Boolean { // Immediately start the drag if the user can't swipe in the other direction and the gesture // handler can intercept it. - return !canOppositeSwipe() && gestureHandler.shouldImmediatelyIntercept(startedPosition) + return !canOppositeSwipe() && draggableHandler.shouldImmediatelyIntercept(startedPosition) } private fun canOppositeSwipe(): Boolean { val oppositeOrientation = - when (gestureHandler.orientation) { + when (draggableHandler.orientation) { Orientation.Vertical -> Orientation.Horizontal Orientation.Horizontal -> Orientation.Vertical } 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/DraggableHandlerTest.kt index d28ac6ad546e..eb9b4280aacb 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/DraggableHandlerTest.kt @@ -47,7 +47,7 @@ private const val SCREEN_SIZE = 100f private val LAYOUT_SIZE = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt()) @RunWith(AndroidJUnit4::class) -class SceneGestureHandlerTest { +class DraggableHandlerTest { private class TestGestureScope( private val testScope: MonotonicClockTestScope, ) { @@ -99,19 +99,19 @@ class SceneGestureHandlerTest { ) .apply { setScenesTargetSizeForTest(LAYOUT_SIZE) } - val sceneGestureHandler = layoutImpl.gestureHandler(Orientation.Vertical) - val horizontalSceneGestureHandler = layoutImpl.gestureHandler(Orientation.Horizontal) + val draggableHandler = layoutImpl.draggableHandler(Orientation.Vertical) + val horizontalDraggableHandler = layoutImpl.draggableHandler(Orientation.Horizontal) fun nestedScrollConnection(nestedScrollBehavior: NestedScrollBehavior) = - SceneNestedScrollHandler( + NestedScrollHandlerImpl( layoutImpl = layoutImpl, - orientation = sceneGestureHandler.orientation, + orientation = draggableHandler.orientation, topOrLeftBehavior = nestedScrollBehavior, bottomOrRightBehavior = nestedScrollBehavior, ) .connection - val velocityThreshold = sceneGestureHandler.velocityThreshold + val velocityThreshold = draggableHandler.velocityThreshold fun down(fractionOfScreen: Float) = if (fractionOfScreen < 0f) error("use up()") else SCREEN_SIZE * fractionOfScreen @@ -190,20 +190,18 @@ class SceneGestureHandlerTest { fun onDragStarted( startedPosition: Offset = Offset.Zero, overSlop: Float, - pointersDown: Int = 1 - ) { + pointersDown: Int = 1, + ): DragController { // overSlop should be 0f only if the drag gesture starts with startDragImmediately if (overSlop == 0f) error("Consider using onDragStartedImmediately()") - onDragStarted(sceneGestureHandler.draggable, startedPosition, overSlop, pointersDown) + return onDragStarted(draggableHandler, startedPosition, overSlop, pointersDown) } - fun onDragStartedImmediately(startedPosition: Offset = Offset.Zero, pointersDown: Int = 1) { - onDragStarted( - sceneGestureHandler.draggable, - startedPosition, - overSlop = 0f, - pointersDown - ) + fun onDragStartedImmediately( + startedPosition: Offset = Offset.Zero, + pointersDown: Int = 1, + ): DragController { + return onDragStarted(draggableHandler, startedPosition, overSlop = 0f, pointersDown) } fun onDragStarted( @@ -211,24 +209,26 @@ class SceneGestureHandlerTest { startedPosition: Offset = Offset.Zero, overSlop: Float = 0f, pointersDown: Int = 1 - ) { - draggableHandler.onDragStarted( - startedPosition = startedPosition, - overSlop = overSlop, - pointersDown = pointersDown, - ) + ): DragController { + val dragController = + draggableHandler.onDragStarted( + startedPosition = startedPosition, + overSlop = overSlop, + pointersDown = pointersDown, + ) // MultiPointerDraggable will always call onDelta with the initial overSlop right after - onDelta(pixels = overSlop) + dragController.onDragDelta(pixels = overSlop) + + return dragController } - fun onDelta(pixels: Float) { - sceneGestureHandler.draggable.onDelta(pixels = pixels) + fun DragController.onDragDelta(pixels: Float) { + onDrag(delta = pixels) } - fun onDragStopped(velocity: Float) { - sceneGestureHandler.draggable.onDragStopped(velocity = velocity) - runCurrent() + fun DragController.onDragStopped(velocity: Float, canChangeScene: Boolean = true) { + onStop(velocity, canChangeScene) } fun NestedScrollConnection.scroll( @@ -281,20 +281,20 @@ class SceneGestureHandlerTest { @Test fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest { - onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) + val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) assertThat(progress).isEqualTo(0.1f) - onDelta(pixels = down(fractionOfScreen = 0.1f)) + dragController.onDragDelta(pixels = down(fractionOfScreen = 0.1f)) assertThat(progress).isEqualTo(0.2f) } @Test fun onDragStoppedAfterDrag_velocityLowerThanThreshold_remainSameScene() = runGestureTest { - onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) + val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) - onDragStopped(velocity = velocityThreshold - 0.01f) + dragController.onDragStopped(velocity = velocityThreshold - 0.01f) assertTransition(currentScene = SceneA) // wait for the stop animation @@ -304,10 +304,10 @@ class SceneGestureHandlerTest { @Test fun onDragStoppedAfterDrag_velocityAtLeastThreshold_goToNextScene() = runGestureTest { - onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) + val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) - onDragStopped(velocity = velocityThreshold) + dragController.onDragStopped(velocity = velocityThreshold) assertTransition(currentScene = SceneC) // wait for the stop animation @@ -317,10 +317,10 @@ class SceneGestureHandlerTest { @Test fun onDragStoppedAfterStarted_returnToIdle() = runGestureTest { - onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) + val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) - onDragStopped(velocity = 0f) + dragController.onDragStopped(velocity = 0f) advanceUntilIdle() assertIdle(currentScene = SceneA) } @@ -328,7 +328,7 @@ class SceneGestureHandlerTest { @Test fun onDragReversedDirection_changeToScene() = runGestureTest { // Drag A -> B with progress 0.6 - onDragStarted(overSlop = -60f) + val dragController = onDragStarted(overSlop = -60f) assertTransition( currentScene = SceneA, fromScene = SceneA, @@ -337,7 +337,7 @@ class SceneGestureHandlerTest { ) // Reverse direction such that A -> C now with 0.4 - onDelta(pixels = 100f) + dragController.onDragDelta(pixels = 100f) assertTransition( currentScene = SceneA, fromScene = SceneA, @@ -346,7 +346,7 @@ class SceneGestureHandlerTest { ) // After the drag stopped scene C should be committed - onDragStopped(velocity = velocityThreshold) + dragController.onDragStopped(velocity = velocityThreshold) assertTransition(currentScene = SceneC, fromScene = SceneA, toScene = SceneC) // wait for the stop animation @@ -356,8 +356,6 @@ class SceneGestureHandlerTest { @Test fun onDragStartedWithoutActionsInBothDirections_stayIdle() = runGestureTest { - val horizontalDraggableHandler = horizontalSceneGestureHandler.draggable - onDragStarted(horizontalDraggableHandler, overSlop = up(fractionOfScreen = 0.3f)) assertIdle(currentScene = SceneA) @@ -370,7 +368,7 @@ class SceneGestureHandlerTest { navigateToSceneC() // We are on SceneC which has no action in Down direction - onDragStarted(overSlop = 10f) + val dragController = onDragStarted(overSlop = 10f) assertTransition( currentScene = SceneC, fromScene = SceneC, @@ -379,7 +377,7 @@ class SceneGestureHandlerTest { ) // Reverse drag direction, it will consume the previous drag - onDelta(pixels = -10f) + dragController.onDragDelta(pixels = -10f) assertTransition( currentScene = SceneC, fromScene = SceneC, @@ -388,7 +386,7 @@ class SceneGestureHandlerTest { ) // Continue reverse drag direction, it should record progress to Scene B - onDelta(pixels = -10f) + dragController.onDragDelta(pixels = -10f) assertTransition( currentScene = SceneC, fromScene = SceneC, @@ -416,14 +414,14 @@ class SceneGestureHandlerTest { @Test fun onDragToExactlyZero_toSceneIsSet() = runGestureTest { - onDragStarted(overSlop = down(fractionOfScreen = 0.3f)) + val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.3f)) assertTransition( currentScene = SceneA, fromScene = SceneA, toScene = SceneC, progress = 0.3f ) - onDelta(pixels = up(fractionOfScreen = 0.3f)) + dragController.onDragDelta(pixels = up(fractionOfScreen = 0.3f)) assertTransition( currentScene = SceneA, fromScene = SceneA, @@ -434,8 +432,8 @@ class SceneGestureHandlerTest { private fun TestGestureScope.navigateToSceneC() { assertIdle(currentScene = SceneA) - onDragStarted(overSlop = down(fractionOfScreen = 1f)) - onDragStopped(velocity = 0f) + val dragController = onDragStarted(overSlop = down(fractionOfScreen = 1f)) + dragController.onDragStopped(velocity = 0f) advanceUntilIdle() assertIdle(currentScene = SceneC) } @@ -443,7 +441,7 @@ class SceneGestureHandlerTest { @Test fun onAccelaratedScroll_scrollToThirdScene() = runGestureTest { // Drag A -> B with progress 0.2 - onDragStarted(overSlop = up(fractionOfScreen = 0.2f)) + val dragController1 = onDragStarted(overSlop = up(fractionOfScreen = 0.2f)) assertTransition( currentScene = SceneA, fromScene = SceneA, @@ -452,13 +450,13 @@ class SceneGestureHandlerTest { ) // Start animation A -> B with progress 0.2 -> 1.0 - onDragStopped(velocity = -velocityThreshold) + dragController1.onDragStopped(velocity = -velocityThreshold) assertTransition(currentScene = SceneB, fromScene = SceneA, toScene = SceneB) // While at A -> B do a 100% screen drag (progress 1.2). This should go past B and change // the transition to B -> C with progress 0.2 - onDragStartedImmediately() - onDelta(pixels = up(fractionOfScreen = 1f)) + val dragController2 = onDragStartedImmediately() + dragController2.onDragDelta(pixels = up(fractionOfScreen = 1f)) assertTransition( currentScene = SceneB, fromScene = SceneB, @@ -467,7 +465,7 @@ class SceneGestureHandlerTest { ) // After the drag stopped scene C should be committed - onDragStopped(velocity = -velocityThreshold) + dragController2.onDragStopped(velocity = -velocityThreshold) assertTransition(currentScene = SceneC, fromScene = SceneB, toScene = SceneC) // wait for the stop animation @@ -477,9 +475,9 @@ class SceneGestureHandlerTest { @Test fun onAccelaratedScrollBothTargetsBecomeNull_settlesToIdle() = runGestureTest { - onDragStarted(overSlop = up(fractionOfScreen = 0.2f)) - onDelta(pixels = up(fractionOfScreen = 0.2f)) - onDragStopped(velocity = -velocityThreshold) + val dragController1 = onDragStarted(overSlop = up(fractionOfScreen = 0.2f)) + dragController1.onDragDelta(pixels = up(fractionOfScreen = 0.2f)) + dragController1.onDragStopped(velocity = -velocityThreshold) assertTransition(currentScene = SceneB, fromScene = SceneA, toScene = SceneB) mutableUserActionsA.remove(Swipe.Up) @@ -488,34 +486,34 @@ class SceneGestureHandlerTest { mutableUserActionsB.remove(Swipe.Down) // start accelaratedScroll and scroll over to B -> null - onDragStartedImmediately() - onDelta(pixels = up(fractionOfScreen = 0.5f)) - onDelta(pixels = up(fractionOfScreen = 0.5f)) + val dragController2 = onDragStartedImmediately() + dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f)) + dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f)) // 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 - onDelta(pixels = up(fractionOfScreen = 0.5f)) - onDragStopped(velocity = 0f) + dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f)) + dragController2.onDragStopped(velocity = 0f) advanceUntilIdle() assertIdle(SceneB) // These events can still come in after the animation has settled - onDelta(pixels = up(fractionOfScreen = 0.5f)) - onDragStopped(velocity = 0f) + dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f)) + dragController2.onDragStopped(velocity = 0f) assertIdle(SceneB) } @Test fun onDragTargetsChanged_targetStaysTheSame() = runGestureTest { - onDragStarted(overSlop = up(fractionOfScreen = 0.1f)) + val dragController1 = onDragStarted(overSlop = up(fractionOfScreen = 0.1f)) assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.1f) mutableUserActionsA[Swipe.Up] = UserActionResult(SceneC) - onDelta(pixels = up(fractionOfScreen = 0.1f)) + dragController1.onDragDelta(pixels = up(fractionOfScreen = 0.1f)) // target stays B even though UserActions changed assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.2f) - onDragStopped(velocity = down(fractionOfScreen = 0.1f)) + dragController1.onDragStopped(velocity = down(fractionOfScreen = 0.1f)) advanceUntilIdle() // now target changed to C for new drag @@ -525,25 +523,26 @@ class SceneGestureHandlerTest { @Test fun onDragTargetsChanged_targetsChangeWhenStartingNewDrag() = runGestureTest { - onDragStarted(overSlop = up(fractionOfScreen = 0.1f)) + val dragController1 = onDragStarted(overSlop = up(fractionOfScreen = 0.1f)) assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.1f) mutableUserActionsA[Swipe.Up] = UserActionResult(SceneC) - onDelta(pixels = up(fractionOfScreen = 0.1f)) - onDragStopped(velocity = down(fractionOfScreen = 0.1f)) + dragController1.onDragDelta(pixels = up(fractionOfScreen = 0.1f)) + dragController1.onDragStopped(velocity = down(fractionOfScreen = 0.1f)) // now target changed to C for new drag that started before previous drag settled to Idle - onDragStartedImmediately() - onDelta(pixels = up(fractionOfScreen = 0.1f)) + val dragController2 = onDragStartedImmediately() + dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.1f)) assertTransition(fromScene = SceneA, toScene = SceneC, progress = 0.3f) } @Test fun startGestureDuringAnimatingOffset_shouldImmediatelyStopTheAnimation() = runGestureTest { - onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) + val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) - onDragStopped(velocity = velocityThreshold) + dragController.onDragStopped(velocity = velocityThreshold) + runCurrent() assertTransition(currentScene = SceneC) assertThat(isUserInputOngoing).isFalse() @@ -632,7 +631,7 @@ class SceneGestureHandlerTest { // stop scene transition (start the "stop animation") nestedScroll.preFling(available = Velocity.Zero) - // a pre scroll event, that could be intercepted by SceneGestureHandler + // a pre scroll event, that could be intercepted by DraggableHandlerImpl nestedScroll.onPreScroll( available = Offset(0f, secondScroll), source = NestedScrollSource.Drag @@ -801,18 +800,6 @@ class SceneGestureHandlerTest { } @Test - fun beforeDraggableStart_drag_shouldBeIgnored() = runGestureTest { - onDelta(pixels = down(fractionOfScreen = 0.1f)) - assertIdle(currentScene = SceneA) - } - - @Test - fun beforeDraggableStart_stop_shouldBeIgnored() = runGestureTest { - onDragStopped(velocity = velocityThreshold) - assertIdle(currentScene = SceneA) - } - - @Test fun beforeNestedScrollStart_stop_shouldBeIgnored() = runGestureTest { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview) nestedScroll.preFling(available = Velocity(0f, velocityThreshold)) @@ -826,7 +813,7 @@ class SceneGestureHandlerTest { val offsetY10 = downOffset(fractionOfScreen = 0.1f) // Start a drag and then stop it, given that - onDragStarted(overSlop = up(0.1f)) + val dragController = onDragStarted(overSlop = up(0.1f)) assertTransition(currentScene = SceneA) assertThat(progress).isEqualTo(0.1f) @@ -836,7 +823,7 @@ class SceneGestureHandlerTest { assertThat(progress).isEqualTo(0.2f) // this should be ignored, we are scrolling now! - onDragStopped(-velocityThreshold) + dragController.onDragStopped(-velocityThreshold) assertTransition(currentScene = SceneA) nestedScroll.scroll(available = -offsetY10) @@ -865,6 +852,7 @@ class SceneGestureHandlerTest { currentScene = SceneC, fromScene = SceneC, toScene = SceneB, + progress = 0.1f, isUserInputOngoing = true, ) @@ -873,18 +861,25 @@ class SceneGestureHandlerTest { // During the current gesture, start a new gesture, still in the middle of the screen. We // should intercept it. Because it is intercepted, the overSlop passed to onDragStarted() // should be 0f. - assertThat(sceneGestureHandler.shouldImmediatelyIntercept(middle)).isTrue() + assertThat(draggableHandler.shouldImmediatelyIntercept(middle)).isTrue() onDragStartedImmediately(startedPosition = middle) // We should have intercepted the transition, so the transition should be the same object. - assertTransition(currentScene = SceneC, fromScene = SceneC, toScene = SceneB) - assertThat(transitionState).isSameInstanceAs(firstTransition) + assertTransition( + currentScene = SceneC, + fromScene = SceneC, + toScene = SceneB, + progress = 0.1f, + isUserInputOngoing = true, + ) + // We should have a new transition + assertThat(transitionState).isNotSameInstanceAs(firstTransition) // Start a new gesture from the bottom of the screen. Because swiping up from the bottom of // C leads to scene A (and not B), the previous transitions is *not* intercepted and we // instead animate from C to A. val bottom = Offset(SCREEN_SIZE / 2, SCREEN_SIZE) - assertThat(sceneGestureHandler.shouldImmediatelyIntercept(bottom)).isFalse() + assertThat(draggableHandler.shouldImmediatelyIntercept(bottom)).isFalse() onDragStarted(startedPosition = bottom, overSlop = up(0.1f)) assertTransition( @@ -901,12 +896,12 @@ class SceneGestureHandlerTest { assertIdle(SceneA) // Swipe up to scene B. - onDragStarted(overSlop = up(0.1f)) + val dragController = onDragStarted(overSlop = up(0.1f)) assertTransition(currentScene = SceneA, fromScene = SceneA, toScene = SceneB) // Block the transition when the user release their finger. canChangeScene = { false } - onDragStopped(velocity = -velocityThreshold) + dragController.onDragStopped(velocity = -velocityThreshold) advanceUntilIdle() assertIdle(SceneA) } @@ -916,18 +911,18 @@ class SceneGestureHandlerTest { assertIdle(SceneA) // Swipe up to B. - onDragStarted(overSlop = up(0.1f)) + val dragController1 = onDragStarted(overSlop = up(0.1f)) assertTransition(currentScene = SceneA, fromScene = SceneA, toScene = SceneB) - onDragStopped(velocity = -velocityThreshold) + dragController1.onDragStopped(velocity = -velocityThreshold) assertTransition(currentScene = SceneB, fromScene = SceneA, toScene = SceneB) // Intercept the transition and swipe down back to scene A. - assertThat(sceneGestureHandler.shouldImmediatelyIntercept(startedPosition = null)).isTrue() - onDragStartedImmediately() + assertThat(draggableHandler.shouldImmediatelyIntercept(startedPosition = null)).isTrue() + val dragController2 = onDragStartedImmediately() // Block the transition when the user release their finger. canChangeScene = { false } - onDragStopped(velocity = velocityThreshold) + dragController2.onDragStopped(velocity = velocityThreshold) advanceUntilIdle() assertIdle(SceneB) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt index cd99d05158cd..d8cf1c12989b 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt @@ -59,9 +59,18 @@ class MultiPointerDraggableTest { orientation = Orientation.Vertical, enabled = { enabled }, startDragImmediately = { false }, - onDragStarted = { _, _, _ -> started = true }, - onDragDelta = { _ -> dragged = true }, - onDragStopped = { stopped = true }, + onDragStarted = { _, _, _ -> + started = true + object : DragController { + override fun onDrag(delta: Float) { + dragged = true + } + + override fun onStop(velocity: Float, canChangeScene: Boolean) { + stopped = true + } + } + }, ) ) } |