diff options
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 + } + } + }, ) ) } |