diff options
| author | 2024-10-31 15:57:01 +0000 | |
|---|---|---|
| committer | 2024-11-04 12:13:14 +0000 | |
| commit | f2825433e37293591ff2cc596a4489d3759575d1 (patch) | |
| tree | e210506581b12237b6a01623f5404c01019deed4 | |
| parent | 70e721f99793376dcb39e78064622c53960cff7b (diff) | |
STL introduce Content.findActionResultBestMatch(swipe)
This refactor lets us search for action results using more complex
conditions, like optionally specifying the pointer type (e.g., Mouse or
Touch). It also makes it easier to add other search conditions in the
future. We'll find gestures that perfectly match our requirements, or
use the closest match if needed.
Test: Just a refactor.
Bug: 371984715
Flag: com.android.systemui.scene_container
Change-Id: I7da1b608942c1065318911dffd1549055f8ccbbf
5 files changed, 265 insertions, 247 deletions
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index 7c7202a5c7f2..f4c52ebd24d9 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -35,12 +35,11 @@ internal typealias SuspendedValue<T> = suspend () -> T internal interface DraggableHandler { /** - * Start a drag in the given [startedPosition], with the given [overSlop] and number of - * [pointersDown]. + * Start a drag with the given [pointersInfo] and [overSlop]. * * The returned [DragController] should be used to continue or stop the drag. */ - fun onDragStarted(startedPosition: Offset?, overSlop: Float, pointersDown: Int): DragController + fun onDragStarted(pointersInfo: PointersInfo?, overSlop: Float): DragController } /** @@ -95,7 +94,7 @@ internal class DraggableHandlerImpl( * Note: if this returns true, then [onDragStarted] will be called with overSlop equal to 0f, * indicating that the transition should be intercepted. */ - internal fun shouldImmediatelyIntercept(startedPosition: Offset?): Boolean { + internal fun shouldImmediatelyIntercept(pointersInfo: PointersInfo?): Boolean { // We don't intercept the touch if we are not currently driving the transition. val dragController = dragController if (dragController?.isDrivingTransition != true) { @@ -106,7 +105,7 @@ internal class DraggableHandlerImpl( // Only intercept the current transition if one of the 2 swipes results is also a transition // between the same pair of contents. - val swipes = computeSwipes(startedPosition, pointersDown = 1) + val swipes = computeSwipes(pointersInfo) val fromContent = layoutImpl.content(swipeAnimation.currentContent) val (upOrLeft, downOrRight) = swipes.computeSwipesResults(fromContent) val currentScene = layoutImpl.state.currentScene @@ -123,11 +122,7 @@ internal class DraggableHandlerImpl( )) } - override fun onDragStarted( - startedPosition: Offset?, - overSlop: Float, - pointersDown: Int, - ): DragController { + override fun onDragStarted(pointersInfo: PointersInfo?, overSlop: Float): DragController { if (overSlop == 0f) { val oldDragController = dragController check(oldDragController != null && oldDragController.isDrivingTransition) { @@ -152,7 +147,7 @@ internal class DraggableHandlerImpl( return updateDragController(swipes, swipeAnimation) } - val swipes = computeSwipes(startedPosition, pointersDown) + val swipes = computeSwipes(pointersInfo) val fromContent = layoutImpl.contentForUserActions() swipes.updateSwipesResults(fromContent) @@ -189,8 +184,7 @@ internal class DraggableHandlerImpl( return createSwipeAnimation(layoutImpl, result, isUpOrLeft, orientation) } - internal fun resolveSwipeSource(startedPosition: Offset?): SwipeSource.Resolved? { - if (startedPosition == null) return null + internal fun resolveSwipeSource(startedPosition: Offset): SwipeSource.Resolved? { return layoutImpl.swipeSourceDetector.source( layoutSize = layoutImpl.lastSize, position = startedPosition.round(), @@ -199,57 +193,42 @@ internal class DraggableHandlerImpl( ) } - internal fun resolveSwipe( - pointersDown: Int, - fromSource: SwipeSource.Resolved?, - isUpOrLeft: Boolean, - ): Swipe.Resolved { - return Swipe.Resolved( - direction = - when (orientation) { - Orientation.Horizontal -> - if (isUpOrLeft) { - SwipeDirection.Resolved.Left - } else { - SwipeDirection.Resolved.Right - } - - Orientation.Vertical -> - if (isUpOrLeft) { - SwipeDirection.Resolved.Up - } else { - SwipeDirection.Resolved.Down - } - }, - pointerCount = pointersDown, - fromSource = fromSource, + private fun computeSwipes(pointersInfo: PointersInfo?): Swipes { + val fromSource = pointersInfo?.let { resolveSwipeSource(it.startedPosition) } + return Swipes( + upOrLeft = resolveSwipe(orientation, isUpOrLeft = true, pointersInfo, fromSource), + downOrRight = resolveSwipe(orientation, isUpOrLeft = false, pointersInfo, fromSource), ) } +} - private fun computeSwipes(startedPosition: Offset?, pointersDown: Int): Swipes { - val fromSource = resolveSwipeSource(startedPosition) - val upOrLeft = resolveSwipe(pointersDown, fromSource, isUpOrLeft = true) - val downOrRight = resolveSwipe(pointersDown, fromSource, isUpOrLeft = false) - return if (fromSource == null) { - Swipes( - upOrLeft = null, - downOrRight = null, - upOrLeftNoSource = upOrLeft, - downOrRightNoSource = downOrRight, - ) - } else { - Swipes( - upOrLeft = upOrLeft, - downOrRight = downOrRight, - upOrLeftNoSource = upOrLeft.copy(fromSource = null), - downOrRightNoSource = downOrRight.copy(fromSource = null), - ) - } - } +private fun resolveSwipe( + orientation: Orientation, + isUpOrLeft: Boolean, + pointersInfo: PointersInfo?, + fromSource: SwipeSource.Resolved?, +): Swipe.Resolved { + return Swipe.Resolved( + direction = + when (orientation) { + Orientation.Horizontal -> + if (isUpOrLeft) { + SwipeDirection.Resolved.Left + } else { + SwipeDirection.Resolved.Right + } - companion object { - private const val TAG = "DraggableHandlerImpl" - } + Orientation.Vertical -> + if (isUpOrLeft) { + SwipeDirection.Resolved.Up + } else { + SwipeDirection.Resolved.Down + } + }, + // If the number of pointers is not specified, 1 is assumed. + pointerCount = pointersInfo?.pointersDown ?: 1, + fromSource = fromSource, + ) } /** @param swipes The [Swipes] associated to the current gesture. */ @@ -497,24 +476,14 @@ private class DragControllerImpl( } /** The [Swipe] associated to a given fromScene, startedPosition and pointersDown. */ -internal class Swipes( - val upOrLeft: Swipe.Resolved?, - val downOrRight: Swipe.Resolved?, - val upOrLeftNoSource: Swipe.Resolved?, - val downOrRightNoSource: Swipe.Resolved?, -) { +internal class Swipes(val upOrLeft: Swipe.Resolved, val downOrRight: Swipe.Resolved) { /** The [UserActionResult] associated to up and down swipes. */ var upOrLeftResult: UserActionResult? = null var downOrRightResult: UserActionResult? = null fun computeSwipesResults(fromContent: Content): Pair<UserActionResult?, UserActionResult?> { - val userActions = fromContent.userActions - fun result(swipe: Swipe.Resolved?): UserActionResult? { - return userActions[swipe ?: return null] - } - - val upOrLeftResult = result(upOrLeft) ?: result(upOrLeftNoSource) - val downOrRightResult = result(downOrRight) ?: result(downOrRightNoSource) + val upOrLeftResult = fromContent.findActionResultBestMatch(swipe = upOrLeft) + val downOrRightResult = fromContent.findActionResultBestMatch(swipe = downOrRight) return upOrLeftResult to downOrRightResult } @@ -568,11 +537,13 @@ internal class NestedScrollHandlerImpl( val connection: PriorityNestedScrollConnection = nestedScrollConnection() - private fun PointersInfo.resolveSwipe(isUpOrLeft: Boolean): Swipe.Resolved { - return draggableHandler.resolveSwipe( - pointersDown = pointersDown, - fromSource = draggableHandler.resolveSwipeSource(startedPosition), + private fun resolveSwipe(isUpOrLeft: Boolean, pointersInfo: PointersInfo?): Swipe.Resolved { + return resolveSwipe( + orientation = draggableHandler.orientation, isUpOrLeft = isUpOrLeft, + pointersInfo = pointersInfo, + fromSource = + pointersInfo?.let { draggableHandler.resolveSwipeSource(it.startedPosition) }, ) } @@ -581,12 +552,7 @@ internal class NestedScrollHandlerImpl( // moving on to the next scene. var canChangeScene = false - var _lastPointersInfo: PointersInfo? = null - fun pointersInfo(): PointersInfo { - return checkNotNull(_lastPointersInfo) { - "PointersInfo should be initialized before the transition begins." - } - } + var lastPointersInfo: PointersInfo? = null fun hasNextScene(amount: Float): Boolean { val transitionState = layoutState.transitionState @@ -594,17 +560,11 @@ internal class NestedScrollHandlerImpl( val fromScene = layoutImpl.scene(scene) val resolvedSwipe = when { - amount < 0f -> pointersInfo().resolveSwipe(isUpOrLeft = true) - amount > 0f -> pointersInfo().resolveSwipe(isUpOrLeft = false) + amount < 0f -> resolveSwipe(isUpOrLeft = true, lastPointersInfo) + amount > 0f -> resolveSwipe(isUpOrLeft = false, lastPointersInfo) else -> null } - val nextScene = - resolvedSwipe?.let { - fromScene.userActions[it] - ?: if (it.fromSource != null) { - fromScene.userActions[it.copy(fromSource = null)] - } else null - } + val nextScene = resolvedSwipe?.let { fromScene.findActionResultBestMatch(it) } if (nextScene != null) return true if (transitionState !is TransitionState.Idle) return false @@ -618,13 +578,14 @@ internal class NestedScrollHandlerImpl( return PriorityNestedScrollConnection( orientation = orientation, canStartPreScroll = { offsetAvailable, offsetBeforeStart, _ -> + val pointersInfo = pointersInfoOwner.pointersInfo() canChangeScene = if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f val canInterceptSwipeTransition = canChangeScene && offsetAvailable != 0f && - draggableHandler.shouldImmediatelyIntercept(startedPosition = null) + draggableHandler.shouldImmediatelyIntercept(pointersInfo) if (!canInterceptSwipeTransition) return@PriorityNestedScrollConnection false val threshold = layoutImpl.transitionInterceptionThreshold @@ -635,13 +596,11 @@ internal class NestedScrollHandlerImpl( return@PriorityNestedScrollConnection false } - val pointersInfo = pointersInfoOwner.pointersInfo() - - if (pointersInfo.isMouseWheel) { + if (pointersInfo?.isMouseWheel == true) { // Do not support mouse wheel interactions return@PriorityNestedScrollConnection false } - _lastPointersInfo = pointersInfo + lastPointersInfo = pointersInfo // If the current swipe transition is *not* closed to 0f or 1f, then we want the // scroll events to intercept the current transition to continue the scene @@ -661,11 +620,11 @@ internal class NestedScrollHandlerImpl( if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f val pointersInfo = pointersInfoOwner.pointersInfo() - if (pointersInfo.isMouseWheel) { + if (pointersInfo?.isMouseWheel == true) { // Do not support mouse wheel interactions return@PriorityNestedScrollConnection false } - _lastPointersInfo = pointersInfo + lastPointersInfo = pointersInfo val canStart = when (behavior) { @@ -703,11 +662,11 @@ internal class NestedScrollHandlerImpl( canChangeScene = false val pointersInfo = pointersInfoOwner.pointersInfo() - if (pointersInfo.isMouseWheel) { + if (pointersInfo?.isMouseWheel == true) { // Do not support mouse wheel interactions return@PriorityNestedScrollConnection false } - _lastPointersInfo = pointersInfo + lastPointersInfo = pointersInfo val canStart = behavior.canStartOnPostFling && hasNextScene(velocityAvailable) if (canStart) { @@ -717,12 +676,11 @@ internal class NestedScrollHandlerImpl( canStart }, onStart = { firstScroll -> - val pointersInfo = pointersInfo() + val pointersInfo = lastPointersInfo scrollController( dragController = draggableHandler.onDragStarted( - pointersDown = pointersInfo.pointersDown, - startedPosition = pointersInfo.startedPosition, + pointersInfo = pointersInfo, overSlop = if (isIntercepting) 0f else firstScroll, ), canChangeScene = canChangeScene, @@ -741,7 +699,7 @@ private fun scrollController( return object : ScrollController { override fun onScroll(deltaScroll: Float, source: NestedScrollSource): Float { val pointersInfo = pointersInfoOwner.pointersInfo() - if (pointersInfo.isMouseWheel) { + if (pointersInfo?.isMouseWheel == true) { // Do not support mouse wheel interactions return 0f } 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 8613f6da0f62..a0bee436979c 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 @@ -78,8 +78,8 @@ import kotlinx.coroutines.launch @Stable internal fun Modifier.multiPointerDraggable( orientation: Orientation, - startDragImmediately: (startedPosition: Offset) -> Boolean, - onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, + startDragImmediately: (pointersInfo: PointersInfo) -> Boolean, + onDragStarted: (pointersInfo: PointersInfo, overSlop: Float) -> DragController, onFirstPointerDown: () -> Unit = {}, swipeDetector: SwipeDetector = DefaultSwipeDetector, dispatcher: NestedScrollDispatcher, @@ -97,9 +97,8 @@ internal fun Modifier.multiPointerDraggable( private data class MultiPointerDraggableElement( private val orientation: Orientation, - private val startDragImmediately: (startedPosition: Offset) -> Boolean, - private val onDragStarted: - (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, + private val startDragImmediately: (pointersInfo: PointersInfo) -> Boolean, + private val onDragStarted: (pointersInfo: PointersInfo, overSlop: Float) -> DragController, private val onFirstPointerDown: () -> Unit, private val swipeDetector: SwipeDetector, private val dispatcher: NestedScrollDispatcher, @@ -125,9 +124,8 @@ private data class MultiPointerDraggableElement( internal class MultiPointerDraggableNode( orientation: Orientation, - var startDragImmediately: (startedPosition: Offset) -> Boolean, - var onDragStarted: - (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, + var startDragImmediately: (pointersInfo: PointersInfo) -> Boolean, + var onDragStarted: (pointersInfo: PointersInfo, overSlop: Float) -> DragController, var onFirstPointerDown: () -> Unit, swipeDetector: SwipeDetector = DefaultSwipeDetector, private val dispatcher: NestedScrollDispatcher, @@ -183,17 +181,22 @@ internal class MultiPointerDraggableNode( pointerInput.onPointerEvent(pointerEvent, pass, bounds) } + private var lastPointerEvent: PointerEvent? = null private var startedPosition: Offset? = null private var pointersDown: Int = 0 - private var isMouseWheel: Boolean = false - internal fun pointersInfo(): PointersInfo { - return PointersInfo( + internal fun pointersInfo(): PointersInfo? { + val startedPosition = startedPosition + val lastPointerEvent = lastPointerEvent + if (startedPosition == null || lastPointerEvent == null) { // This may be null, i.e. when the user uses TalkBack + return null + } + + return PointersInfo( startedPosition = startedPosition, - // We could have 0 pointers during fling or for other reasons. - pointersDown = pointersDown.coerceAtLeast(1), - isMouseWheel = isMouseWheel, + pointersDown = pointersDown, + lastPointerEvent = lastPointerEvent, ) } @@ -212,8 +215,8 @@ internal class MultiPointerDraggableNode( if (pointerEvent.type == PointerEventType.Enter) continue val changes = pointerEvent.changes + lastPointerEvent = pointerEvent pointersDown = changes.countDown() - isMouseWheel = pointerEvent.type == PointerEventType.Scroll when { // There are no more pointers down. @@ -285,8 +288,8 @@ internal class MultiPointerDraggableNode( detectDragGestures( orientation = orientation, startDragImmediately = startDragImmediately, - onDragStart = { startedPosition, overSlop, pointersDown -> - onDragStarted(startedPosition, overSlop, pointersDown) + onDragStart = { pointersInfo, overSlop -> + onDragStarted(pointersInfo, overSlop) }, onDrag = { controller, amount -> dispatchScrollEvents( @@ -435,9 +438,8 @@ internal class MultiPointerDraggableNode( */ private suspend fun AwaitPointerEventScope.detectDragGestures( orientation: Orientation, - startDragImmediately: (startedPosition: Offset) -> Boolean, - onDragStart: - (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, + startDragImmediately: (pointersInfo: PointersInfo) -> Boolean, + onDragStart: (pointersInfo: PointersInfo, overSlop: Float) -> DragController, onDrag: (controller: DragController, dragAmount: Float) -> Unit, onDragEnd: (controller: DragController) -> Unit, onDragCancel: (controller: DragController) -> Unit, @@ -462,8 +464,13 @@ internal class MultiPointerDraggableNode( .first() var overSlop = 0f + var lastPointersInfo = + checkNotNull(pointersInfo()) { + "We should have pointers down, last event: $currentEvent" + } + val drag = - if (startDragImmediately(consumablePointer.position)) { + if (startDragImmediately(lastPointersInfo)) { consumablePointer.consume() consumablePointer } else { @@ -488,14 +495,18 @@ internal class MultiPointerDraggableNode( consumablePointer.id, onSlopReached, ) - } + } ?: return + lastPointersInfo = + checkNotNull(pointersInfo()) { + "We should have pointers down, last event: $currentEvent" + } // Make sure that overSlop is not 0f. This can happen when the user drags by exactly // the touch slop. However, the overSlop we pass to onDragStarted() is used to // compute the direction we are dragging in, so overSlop should never be 0f unless // we intercept an ongoing swipe transition (i.e. startDragImmediately() returned // true). - if (drag != null && overSlop == 0f) { + if (overSlop == 0f) { val delta = (drag.position - consumablePointer.position).toFloat() check(delta != 0f) { "delta is equal to 0" } overSlop = delta.sign @@ -503,49 +514,38 @@ internal class MultiPointerDraggableNode( drag } - if (drag != null) { - val controller = - onDragStart( - // The startedPosition is the starting position when a gesture begins (when the - // first pointer touches the screen), not the point where we begin dragging. - // For example, this could be different if one of our children intercepts the - // gesture first and then we do. - requireNotNull(startedPosition), - overSlop, - pointersDown, + val controller = onDragStart(lastPointersInfo, overSlop) + + val successful: Boolean + try { + onDrag(controller, overSlop) + + successful = + drag( + initialPointerId = drag.id, + hasDragged = { it.positionChangeIgnoreConsumed().toFloat() != 0f }, + onDrag = { + onDrag(controller, it.positionChange().toFloat()) + it.consume() + }, + onIgnoredEvent = { + // We are still dragging an object, but this event is not of interest to the + // caller. + // This event will not trigger the onDrag event, but we will consume the + // event to prevent another pointerInput from interrupting the current + // gesture just because the event was ignored. + it.consume() + }, ) + } catch (t: Throwable) { + onDragCancel(controller) + throw t + } - val successful: Boolean - try { - onDrag(controller, overSlop) - - successful = - drag( - initialPointerId = drag.id, - hasDragged = { it.positionChangeIgnoreConsumed().toFloat() != 0f }, - onDrag = { - onDrag(controller, it.positionChange().toFloat()) - it.consume() - }, - onIgnoredEvent = { - // We are still dragging an object, but this event is not of interest to - // the caller. - // This event will not trigger the onDrag event, but we will consume the - // event to prevent another pointerInput from interrupting the current - // gesture just because the event was ignored. - it.consume() - }, - ) - } catch (t: Throwable) { - onDragCancel(controller) - throw t - } - - if (successful) { - onDragEnd(controller) - } else { - onDragCancel(controller) - } + if (successful) { + onDragEnd(controller) + } else { + onDragCancel(controller) } } @@ -655,11 +655,46 @@ internal class MultiPointerDraggableNode( } internal fun interface PointersInfoOwner { - fun pointersInfo(): PointersInfo + /** + * Provides information about the pointers interacting with this composable. + * + * @return A [PointersInfo] object containing details about the pointers, including the starting + * position and the number of pointers down, or `null` if there are no pointers down. + */ + fun pointersInfo(): PointersInfo? } +/** + * Holds information about pointer interactions within a composable. + * + * This class stores details such as the starting position of a gesture, the number of pointers + * down, and whether the last pointer event was a mouse wheel scroll. + * + * @param startedPosition The starting position of the gesture. This is the position where the first + * pointer touched the screen, not necessarily the point where dragging begins. This may be + * different from the initial touch position if a child composable intercepts the gesture before + * this one. + * @param pointersDown The number of pointers currently down. + * @param isMouseWheel Indicates whether the last pointer event was a mouse wheel scroll. + */ internal data class PointersInfo( - val startedPosition: Offset?, + val startedPosition: Offset, val pointersDown: Int, val isMouseWheel: Boolean, -) +) { + init { + check(pointersDown > 0) { "We should have at least 1 pointer down, $pointersDown instead" } + } +} + +private fun PointersInfo( + startedPosition: Offset, + pointersDown: Int, + lastPointerEvent: PointerEvent, +): PointersInfo { + return PointersInfo( + startedPosition = startedPosition, + pointersDown = pointersDown, + isMouseWheel = lastPointerEvent.type == PointerEventType.Scroll, + ) +} 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 fdf01cce396b..12ec5df7d777 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 @@ -19,7 +19,6 @@ package com.android.compose.animation.scene import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode import androidx.compose.ui.input.pointer.PointerEvent @@ -65,6 +64,40 @@ private fun Content.shouldEnableSwipes(orientation: Orientation): Boolean { return userActions.keys.any { it is Swipe.Resolved && it.direction.orientation == orientation } } +/** + * Finds the best matching [UserActionResult] for the given [swipe] within this [Content]. + * Prioritizes actions with matching [Swipe.Resolved.fromSource]. + * + * @param swipe The swipe to match against. + * @return The best matching [UserActionResult], or `null` if no match is found. + */ +internal fun Content.findActionResultBestMatch(swipe: Swipe.Resolved): UserActionResult? { + var bestMatch: UserActionResult? = null + userActions.forEach { (actionSwipe, actionResult) -> + if ( + actionSwipe !is Swipe.Resolved || + // The direction must match. + actionSwipe.direction != swipe.direction || + // The number of pointers down must match. + actionSwipe.pointerCount != swipe.pointerCount || + // The action requires a specific fromSource. + (actionSwipe.fromSource != null && actionSwipe.fromSource != swipe.fromSource) + ) { + // This action is not eligible. + return@forEach + } + + // Prioritize actions with a matching fromSource. + if (actionSwipe.fromSource == swipe.fromSource) { + return actionResult + } + + // Otherwise, keep track of the best eligible action. + bestMatch = actionResult + } + return bestMatch +} + private data class SwipeToSceneElement( val draggableHandler: DraggableHandlerImpl, val swipeDetector: SwipeDetector, @@ -155,10 +188,10 @@ private class SwipeToSceneNode( override fun onCancelPointerInput() = multiPointerDraggableNode.onCancelPointerInput() - private fun startDragImmediately(startedPosition: Offset): Boolean { + private fun startDragImmediately(pointersInfo: PointersInfo): Boolean { // Immediately start the drag if the user can't swipe in the other direction and the gesture // handler can intercept it. - return !canOppositeSwipe() && draggableHandler.shouldImmediatelyIntercept(startedPosition) + return !canOppositeSwipe() && draggableHandler.shouldImmediatelyIntercept(pointersInfo) } private fun canOppositeSwipe(): Boolean { diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt index f24d93f0d79d..23aa7508cebf 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt @@ -23,6 +23,7 @@ import androidx.compose.material3.Text import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.UserInput +import androidx.compose.ui.input.pointer.PointerType import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection @@ -51,6 +52,18 @@ import org.junit.runner.RunWith private const val SCREEN_SIZE = 100f private val LAYOUT_SIZE = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt()) +private fun pointersInfo( + startedPosition: Offset = Offset.Zero, + pointersDown: Int = 1, + isMouseWheel: Boolean = false, +): PointersInfo { + return PointersInfo( + startedPosition = startedPosition, + pointersDown = pointersDown, + isMouseWheel = isMouseWheel, + ) +} + @RunWith(AndroidJUnit4::class) class DraggableHandlerTest { private class TestGestureScope(val testScope: MonotonicClockTestScope) { @@ -126,9 +139,7 @@ class DraggableHandlerTest { val draggableHandler = layoutImpl.draggableHandler(Orientation.Vertical) val horizontalDraggableHandler = layoutImpl.draggableHandler(Orientation.Horizontal) - var pointerInfoOwner: () -> PointersInfo = { - PointersInfo(startedPosition = Offset.Zero, pointersDown = 1, isMouseWheel = false) - } + var pointerInfoOwner: () -> PointersInfo = { pointersInfo() } fun nestedScrollConnection( nestedScrollBehavior: NestedScrollBehavior, @@ -211,42 +222,32 @@ class DraggableHandlerTest { } fun onDragStarted( - startedPosition: Offset = Offset.Zero, + pointersInfo: PointersInfo = pointersInfo(), overSlop: Float, - pointersDown: Int = 1, expectedConsumedOverSlop: Float = overSlop, ): DragController { // overSlop should be 0f only if the drag gesture starts with startDragImmediately if (overSlop == 0f) error("Consider using onDragStartedImmediately()") return onDragStarted( draggableHandler = draggableHandler, - startedPosition = startedPosition, + pointersInfo = pointersInfo, overSlop = overSlop, - pointersDown = pointersDown, expectedConsumedOverSlop = expectedConsumedOverSlop, ) } - fun onDragStartedImmediately( - startedPosition: Offset = Offset.Zero, - pointersDown: Int = 1, - ): DragController { - return onDragStarted(draggableHandler, startedPosition, overSlop = 0f, pointersDown) + fun onDragStartedImmediately(pointersInfo: PointersInfo = pointersInfo()): DragController { + return onDragStarted(draggableHandler, pointersInfo, overSlop = 0f) } fun onDragStarted( draggableHandler: DraggableHandler, - startedPosition: Offset = Offset.Zero, + pointersInfo: PointersInfo = pointersInfo(), overSlop: Float = 0f, - pointersDown: Int = 1, expectedConsumedOverSlop: Float = overSlop, ): DragController { val dragController = - draggableHandler.onDragStarted( - startedPosition = startedPosition, - overSlop = overSlop, - pointersDown = pointersDown, - ) + draggableHandler.onDragStarted(pointersInfo = pointersInfo, overSlop = overSlop) // MultiPointerDraggable will always call onDelta with the initial overSlop right after dragController.onDragDelta(pixels = overSlop, expectedConsumedOverSlop) @@ -528,7 +529,8 @@ class DraggableHandlerTest { mapOf(Swipe.Up to UserActionResult(SceneB, isIrreversible = true), Swipe.Down to SceneC) val dragController = onDragStarted( - startedPosition = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE * 0.5f), + pointersInfo = + pointersInfo(startedPosition = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE * 0.5f)), overSlop = up(fractionOfScreen = 0.2f), ) assertTransition( @@ -554,7 +556,7 @@ class DraggableHandlerTest { // Start dragging from the bottom onDragStarted( - startedPosition = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE), + pointersInfo = pointersInfo(startedPosition = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE)), overSlop = up(fractionOfScreen = 0.1f), ) assertTransition( @@ -1051,8 +1053,8 @@ class DraggableHandlerTest { navigateToSceneC() // Swipe up from the middle to transition to scene B. - val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f) - onDragStarted(startedPosition = middle, overSlop = up(0.1f)) + val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) + onDragStarted(pointersInfo = middle, overSlop = up(0.1f)) assertTransition( currentScene = SceneC, fromScene = SceneC, @@ -1067,7 +1069,7 @@ class DraggableHandlerTest { // should intercept it. Because it is intercepted, the overSlop passed to onDragStarted() // should be 0f. assertThat(draggableHandler.shouldImmediatelyIntercept(middle)).isTrue() - onDragStartedImmediately(startedPosition = middle) + onDragStartedImmediately(pointersInfo = middle) // We should have intercepted the transition, so the transition should be the same object. assertTransition( @@ -1083,9 +1085,9 @@ class DraggableHandlerTest { // 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) + val bottom = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2, SCREEN_SIZE)) assertThat(draggableHandler.shouldImmediatelyIntercept(bottom)).isFalse() - onDragStarted(startedPosition = bottom, overSlop = up(0.1f)) + onDragStarted(pointersInfo = bottom, overSlop = up(0.1f)) assertTransition( currentScene = SceneC, @@ -1102,8 +1104,8 @@ class DraggableHandlerTest { navigateToSceneC() // Swipe up from the middle to transition to scene B. - val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f) - onDragStarted(startedPosition = middle, overSlop = up(0.1f)) + val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) + onDragStarted(pointersInfo = middle, overSlop = up(0.1f)) assertTransition(fromScene = SceneC, toScene = SceneB, isUserInputOngoing = true) // The current transition can be intercepted. @@ -1119,15 +1121,15 @@ class DraggableHandlerTest { @Test fun interruptedTransitionCanNotBeImmediatelyIntercepted() = runGestureTest { - assertThat(draggableHandler.shouldImmediatelyIntercept(startedPosition = null)).isFalse() + assertThat(draggableHandler.shouldImmediatelyIntercept(pointersInfo = null)).isFalse() onDragStarted(overSlop = up(0.1f)) - assertThat(draggableHandler.shouldImmediatelyIntercept(startedPosition = null)).isTrue() + assertThat(draggableHandler.shouldImmediatelyIntercept(pointersInfo = null)).isTrue() layoutState.startTransitionImmediately( animationScope = testScope.backgroundScope, transition(SceneA, SceneB), ) - assertThat(draggableHandler.shouldImmediatelyIntercept(startedPosition = null)).isFalse() + assertThat(draggableHandler.shouldImmediatelyIntercept(pointersInfo = null)).isFalse() } @Test @@ -1159,7 +1161,7 @@ class DraggableHandlerTest { assertTransition(currentScene = SceneB, fromScene = SceneA, toScene = SceneB) // Intercept the transition and swipe down back to scene A. - assertThat(draggableHandler.shouldImmediatelyIntercept(startedPosition = null)).isTrue() + assertThat(draggableHandler.shouldImmediatelyIntercept(pointersInfo = null)).isTrue() val dragController2 = onDragStartedImmediately() // Block the transition when the user release their finger. @@ -1203,9 +1205,7 @@ class DraggableHandlerTest { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways) // Drag from the **top** of the screen - pointerInfoOwner = { - PointersInfo(startedPosition = Offset(0f, 0f), pointersDown = 1, isMouseWheel = false) - } + pointerInfoOwner = { pointersInfo() } assertIdle(currentScene = SceneC) nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f)) @@ -1222,13 +1222,7 @@ class DraggableHandlerTest { advanceUntilIdle() // Drag from the **bottom** of the screen - pointerInfoOwner = { - PointersInfo( - startedPosition = Offset(0f, SCREEN_SIZE), - pointersDown = 1, - isMouseWheel = false, - ) - } + pointerInfoOwner = { pointersInfo(startedPosition = Offset(0f, SCREEN_SIZE)) } assertIdle(currentScene = SceneC) nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f)) @@ -1248,9 +1242,7 @@ class DraggableHandlerTest { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways) // Use mouse wheel - pointerInfoOwner = { - PointersInfo(startedPosition = Offset(0f, 0f), pointersDown = 1, isMouseWheel = true) - } + pointerInfoOwner = { pointersInfo(isMouseWheel = true) } assertIdle(currentScene = SceneC) nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f)) @@ -1260,8 +1252,8 @@ class DraggableHandlerTest { @Test fun transitionIsImmediatelyUpdatedWhenReleasingFinger() = runGestureTest { // Swipe up from the middle to transition to scene B. - val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f) - val dragController = onDragStarted(startedPosition = middle, overSlop = up(0.1f)) + val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) + val dragController = onDragStarted(pointersInfo = middle, overSlop = up(0.1f)) assertTransition(fromScene = SceneA, toScene = SceneB, isUserInputOngoing = true) dragController.onDragStoppedAnimateLater(velocity = 0f) @@ -1274,10 +1266,10 @@ class DraggableHandlerTest { layoutState.transitions = transitions { overscrollDisabled(SceneB, Orientation.Vertical) } // Swipe up to scene B at progress = 200%. - val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f) + val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) val dragController = onDragStarted( - startedPosition = middle, + pointersInfo = middle, overSlop = up(2f), // Overscroll is disabled, it will scroll up to 100% expectedConsumedOverSlop = up(1f), @@ -1305,8 +1297,8 @@ class DraggableHandlerTest { layoutState.transitions = transitions { overscrollDisabled(SceneB, Orientation.Vertical) } // Swipe up to scene B at progress = 200%. - val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f) - val dragController = onDragStarted(startedPosition = middle, overSlop = up(0.99f)) + val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) + val dragController = onDragStarted(pointersInfo = middle, overSlop = up(0.99f)) assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.99f) // Release the finger. @@ -1351,9 +1343,9 @@ class DraggableHandlerTest { overscroll(SceneB, Orientation.Vertical) { fade(TestElements.Foo) } } - val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f) + val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) - val dragController = onDragStarted(startedPosition = middle, overSlop = up(0.5f)) + val dragController = onDragStarted(pointersInfo = middle, overSlop = up(0.5f)) val transition = assertThat(transitionState).isSceneTransition() assertThat(transition).hasFromScene(SceneA) assertThat(transition).hasToScene(SceneB) @@ -1383,9 +1375,9 @@ class DraggableHandlerTest { overscroll(SceneC, Orientation.Vertical) { fade(TestElements.Foo) } } - val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f) + val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) - val dragController = onDragStarted(startedPosition = middle, overSlop = down(0.5f)) + val dragController = onDragStarted(pointersInfo = middle, overSlop = down(0.5f)) val transition = assertThat(transitionState).isSceneTransition() assertThat(transition).hasFromScene(SceneA) assertThat(transition).hasToScene(SceneC) @@ -1414,9 +1406,9 @@ class DraggableHandlerTest { overscroll(SceneB, Orientation.Vertical) { fade(TestElements.Foo) } } - val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f) + val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) - val dragController = onDragStarted(startedPosition = middle, overSlop = up(1.5f)) + val dragController = onDragStarted(pointersInfo = middle, overSlop = up(1.5f)) val transition = assertThat(transitionState).isSceneTransition() assertThat(transition).hasFromScene(SceneA) assertThat(transition).hasToScene(SceneB) @@ -1446,9 +1438,9 @@ class DraggableHandlerTest { overscroll(SceneC, Orientation.Vertical) { fade(TestElements.Foo) } } - val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f) + val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) - val dragController = onDragStarted(startedPosition = middle, overSlop = down(1.5f)) + val dragController = onDragStarted(pointersInfo = middle, overSlop = down(1.5f)) val transition = assertThat(transitionState).isSceneTransition() assertThat(transition).hasFromScene(SceneA) assertThat(transition).hasToScene(SceneC) @@ -1480,8 +1472,8 @@ class DraggableHandlerTest { mutableUserActionsA = mapOf(Swipe.Up to UserActionResult(SceneB)) - val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f) - val dragController = onDragStarted(startedPosition = middle, overSlop = down(1f)) + val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) + val dragController = onDragStarted(pointersInfo = middle, overSlop = down(1f)) val transition = assertThat(transitionState).isSceneTransition() assertThat(transition).hasFromScene(SceneA) assertThat(transition).hasToScene(SceneB) @@ -1513,8 +1505,8 @@ class DraggableHandlerTest { mutableUserActionsA = mapOf(Swipe.Down to UserActionResult(SceneC)) - val middle = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f) - val dragController = onDragStarted(startedPosition = middle, overSlop = up(1f)) + val middle = pointersInfo(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) + val dragController = onDragStarted(pointersInfo = middle, overSlop = up(1f)) val transition = assertThat(transitionState).isSceneTransition() assertThat(transition).hasFromScene(SceneA) assertThat(transition).hasToScene(SceneC) 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 3df608717414..5ec74f8d2260 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 @@ -98,7 +98,7 @@ class MultiPointerDraggableTest { Modifier.multiPointerDraggable( orientation = Orientation.Vertical, startDragImmediately = { false }, - onDragStarted = { _, _, _ -> + onDragStarted = { _, _ -> started = true SimpleDragController( onDrag = { dragged = true }, @@ -167,7 +167,7 @@ class MultiPointerDraggableTest { orientation = Orientation.Vertical, // We want to start a drag gesture immediately startDragImmediately = { true }, - onDragStarted = { _, _, _ -> + onDragStarted = { _, _ -> started = true SimpleDragController( onDrag = { dragged = true }, @@ -239,7 +239,7 @@ class MultiPointerDraggableTest { .multiPointerDraggable( orientation = Orientation.Vertical, startDragImmediately = { false }, - onDragStarted = { _, _, _ -> + onDragStarted = { _, _ -> started = true SimpleDragController( onDrag = { dragged = true }, @@ -358,7 +358,7 @@ class MultiPointerDraggableTest { .multiPointerDraggable( orientation = Orientation.Vertical, startDragImmediately = { false }, - onDragStarted = { _, _, _ -> + onDragStarted = { _, _ -> started = true SimpleDragController( onDrag = { dragged = true }, @@ -463,7 +463,7 @@ class MultiPointerDraggableTest { .multiPointerDraggable( orientation = Orientation.Vertical, startDragImmediately = { false }, - onDragStarted = { _, _, _ -> + onDragStarted = { _, _ -> verticalStarted = true SimpleDragController( onDrag = { verticalDragged = true }, @@ -475,7 +475,7 @@ class MultiPointerDraggableTest { .multiPointerDraggable( orientation = Orientation.Horizontal, startDragImmediately = { false }, - onDragStarted = { _, _, _ -> + onDragStarted = { _, _ -> horizontalStarted = true SimpleDragController( onDrag = { horizontalDragged = true }, @@ -574,7 +574,7 @@ class MultiPointerDraggableTest { return swipeConsume } }, - onDragStarted = { _, _, _ -> + onDragStarted = { _, _ -> started = true SimpleDragController( onDrag = { /* do nothing */ }, @@ -668,7 +668,7 @@ class MultiPointerDraggableTest { .multiPointerDraggable( orientation = Orientation.Vertical, startDragImmediately = { false }, - onDragStarted = { _, _, _ -> + onDragStarted = { _, _ -> SimpleDragController( onDrag = { consumedOnDrag = it }, onStop = { consumedOnDragStop = it }, @@ -739,7 +739,7 @@ class MultiPointerDraggableTest { .multiPointerDraggable( orientation = Orientation.Vertical, startDragImmediately = { false }, - onDragStarted = { _, _, _ -> + onDragStarted = { _, _ -> SimpleDragController( onDrag = { /* do nothing */ }, onStop = { |