Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +62 −42 Original line number Diff line number Diff line Loading @@ -36,11 +36,11 @@ internal typealias SuspendedValue<T> = suspend () -> T internal interface DraggableHandler { /** * Start a drag with the given [pointersInfo] and [overSlop]. * Start a drag with the given [pointersDown] and [overSlop]. * * The returned [DragController] should be used to continue or stop the drag. */ fun onDragStarted(pointersInfo: PointersInfo?, overSlop: Float): DragController fun onDragStarted(pointersDown: PointersInfo.PointersDown?, overSlop: Float): DragController } /** Loading Loading @@ -95,7 +95,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(pointersInfo: PointersInfo?): Boolean { internal fun shouldImmediatelyIntercept(pointersDown: PointersInfo.PointersDown?): Boolean { // We don't intercept the touch if we are not currently driving the transition. val dragController = dragController if (dragController?.isDrivingTransition != true) { Loading @@ -106,7 +106,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(pointersInfo) val swipes = computeSwipes(pointersDown) val fromContent = layoutImpl.content(swipeAnimation.currentContent) val (upOrLeft, downOrRight) = swipes.computeSwipesResults(fromContent) val currentScene = layoutImpl.state.currentScene Loading @@ -123,7 +123,10 @@ internal class DraggableHandlerImpl( )) } override fun onDragStarted(pointersInfo: PointersInfo?, overSlop: Float): DragController { override fun onDragStarted( pointersDown: PointersInfo.PointersDown?, overSlop: Float, ): DragController { if (overSlop == 0f) { val oldDragController = dragController check(oldDragController != null && oldDragController.isDrivingTransition) { Loading @@ -148,7 +151,7 @@ internal class DraggableHandlerImpl( return updateDragController(swipes, swipeAnimation) } val swipes = computeSwipes(pointersInfo) val swipes = computeSwipes(pointersDown) val fromContent = layoutImpl.contentForUserActions() swipes.updateSwipesResults(fromContent) Loading Loading @@ -194,11 +197,11 @@ internal class DraggableHandlerImpl( ) } private fun computeSwipes(pointersInfo: PointersInfo?): Swipes { val fromSource = pointersInfo?.let { resolveSwipeSource(it.startedPosition) } private fun computeSwipes(pointersDown: PointersInfo.PointersDown?): Swipes { val fromSource = pointersDown?.let { resolveSwipeSource(it.startedPosition) } return Swipes( upOrLeft = resolveSwipe(orientation, isUpOrLeft = true, pointersInfo, fromSource), downOrRight = resolveSwipe(orientation, isUpOrLeft = false, pointersInfo, fromSource), upOrLeft = resolveSwipe(orientation, isUpOrLeft = true, pointersDown, fromSource), downOrRight = resolveSwipe(orientation, isUpOrLeft = false, pointersDown, fromSource), ) } } Loading @@ -206,7 +209,7 @@ internal class DraggableHandlerImpl( private fun resolveSwipe( orientation: Orientation, isUpOrLeft: Boolean, pointersInfo: PointersInfo?, pointersDown: PointersInfo.PointersDown?, fromSource: SwipeSource.Resolved?, ): Swipe.Resolved { return Swipe.Resolved( Loading @@ -227,9 +230,9 @@ private fun resolveSwipe( } }, // If the number of pointers is not specified, 1 is assumed. pointerCount = pointersInfo?.pointersDown ?: 1, pointerCount = pointersDown?.count ?: 1, // Resolves the pointer type only if all pointers are of the same type. pointersType = pointersInfo?.pointersDownByType?.keys?.singleOrNull(), pointersType = pointersDown?.countByType?.keys?.singleOrNull(), fromSource = fromSource, ) } Loading Loading @@ -540,13 +543,16 @@ internal class NestedScrollHandlerImpl( val connection: PriorityNestedScrollConnection = nestedScrollConnection() private fun resolveSwipe(isUpOrLeft: Boolean, pointersInfo: PointersInfo?): Swipe.Resolved { private fun resolveSwipe( isUpOrLeft: Boolean, pointersDown: PointersInfo.PointersDown?, ): Swipe.Resolved { return resolveSwipe( orientation = draggableHandler.orientation, isUpOrLeft = isUpOrLeft, pointersInfo = pointersInfo, pointersDown = pointersDown, fromSource = pointersInfo?.let { draggableHandler.resolveSwipeSource(it.startedPosition) }, pointersDown?.let { draggableHandler.resolveSwipeSource(it.startedPosition) }, ) } Loading @@ -555,7 +561,7 @@ internal class NestedScrollHandlerImpl( // moving on to the next scene. var canChangeScene = false var lastPointersInfo: PointersInfo? = null var lastPointersDown: PointersInfo.PointersDown? = null fun hasNextScene(amount: Float): Boolean { val transitionState = layoutState.transitionState Loading @@ -563,8 +569,8 @@ internal class NestedScrollHandlerImpl( val fromScene = layoutImpl.scene(scene) val resolvedSwipe = when { amount < 0f -> resolveSwipe(isUpOrLeft = true, lastPointersInfo) amount > 0f -> resolveSwipe(isUpOrLeft = false, lastPointersInfo) amount < 0f -> resolveSwipe(isUpOrLeft = true, lastPointersDown) amount > 0f -> resolveSwipe(isUpOrLeft = false, lastPointersDown) else -> null } val nextScene = resolvedSwipe?.let { fromScene.findActionResultBestMatch(it) } Loading @@ -581,14 +587,24 @@ internal class NestedScrollHandlerImpl( return PriorityNestedScrollConnection( orientation = orientation, canStartPreScroll = { offsetAvailable, offsetBeforeStart, _ -> val pointersInfo = pointersInfoOwner.pointersInfo() val pointersDown: PointersInfo.PointersDown? = when (val info = pointersInfoOwner.pointersInfo()) { PointersInfo.MouseWheel -> { // Do not support mouse wheel interactions return@PriorityNestedScrollConnection false } is PointersInfo.PointersDown -> info null -> null } canChangeScene = if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f val canInterceptSwipeTransition = canChangeScene && offsetAvailable != 0f && draggableHandler.shouldImmediatelyIntercept(pointersInfo) draggableHandler.shouldImmediatelyIntercept(pointersDown) if (!canInterceptSwipeTransition) return@PriorityNestedScrollConnection false val threshold = layoutImpl.transitionInterceptionThreshold Loading @@ -599,11 +615,7 @@ internal class NestedScrollHandlerImpl( return@PriorityNestedScrollConnection false } if (pointersInfo?.isMouseWheel == true) { // Do not support mouse wheel interactions return@PriorityNestedScrollConnection false } lastPointersInfo = pointersInfo lastPointersDown = pointersDown // 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 Loading @@ -622,12 +634,17 @@ internal class NestedScrollHandlerImpl( val isZeroOffset = if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f val pointersInfo = pointersInfoOwner.pointersInfo() if (pointersInfo?.isMouseWheel == true) { val pointersDown: PointersInfo.PointersDown? = when (val info = pointersInfoOwner.pointersInfo()) { PointersInfo.MouseWheel -> { // Do not support mouse wheel interactions return@PriorityNestedScrollConnection false } lastPointersInfo = pointersInfo is PointersInfo.PointersDown -> info null -> null } lastPointersDown = pointersDown val canStart = when (behavior) { Loading Loading @@ -664,12 +681,17 @@ internal class NestedScrollHandlerImpl( // We could start an overscroll animation canChangeScene = false val pointersInfo = pointersInfoOwner.pointersInfo() if (pointersInfo?.isMouseWheel == true) { val pointersDown: PointersInfo.PointersDown? = when (val info = pointersInfoOwner.pointersInfo()) { PointersInfo.MouseWheel -> { // Do not support mouse wheel interactions return@PriorityNestedScrollConnection false } lastPointersInfo = pointersInfo is PointersInfo.PointersDown -> info null -> null } lastPointersDown = pointersDown val canStart = behavior.canStartOnPostFling && hasNextScene(velocityAvailable) if (canStart) { Loading @@ -679,11 +701,10 @@ internal class NestedScrollHandlerImpl( canStart }, onStart = { firstScroll -> val pointersInfo = lastPointersInfo scrollController( dragController = draggableHandler.onDragStarted( pointersInfo = pointersInfo, pointersDown = lastPointersDown, overSlop = if (isIntercepting) 0f else firstScroll, ), canChangeScene = canChangeScene, Loading @@ -701,8 +722,7 @@ private fun scrollController( ): ScrollController { return object : ScrollController { override fun onScroll(deltaScroll: Float, source: NestedScrollSource): Float { val pointersInfo = pointersInfoOwner.pointersInfo() if (pointersInfo?.isMouseWheel == true) { if (pointersInfoOwner.pointersInfo() == PointersInfo.MouseWheel) { // Do not support mouse wheel interactions return 0f } Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +61 −68 Original line number Diff line number Diff line Loading @@ -80,8 +80,8 @@ import kotlinx.coroutines.launch @Stable internal fun Modifier.multiPointerDraggable( orientation: Orientation, startDragImmediately: (pointersInfo: PointersInfo) -> Boolean, onDragStarted: (pointersInfo: PointersInfo, overSlop: Float) -> DragController, startDragImmediately: (pointersDown: PointersInfo.PointersDown) -> Boolean, onDragStarted: (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController, onFirstPointerDown: () -> Unit = {}, swipeDetector: SwipeDetector = DefaultSwipeDetector, dispatcher: NestedScrollDispatcher, Loading @@ -99,8 +99,9 @@ internal fun Modifier.multiPointerDraggable( private data class MultiPointerDraggableElement( private val orientation: Orientation, private val startDragImmediately: (pointersInfo: PointersInfo) -> Boolean, private val onDragStarted: (pointersInfo: PointersInfo, overSlop: Float) -> DragController, private val startDragImmediately: (pointersDown: PointersInfo.PointersDown) -> Boolean, private val onDragStarted: (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController, private val onFirstPointerDown: () -> Unit, private val swipeDetector: SwipeDetector, private val dispatcher: NestedScrollDispatcher, Loading @@ -126,8 +127,8 @@ private data class MultiPointerDraggableElement( internal class MultiPointerDraggableNode( orientation: Orientation, var startDragImmediately: (pointersInfo: PointersInfo) -> Boolean, var onDragStarted: (pointersInfo: PointersInfo, overSlop: Float) -> DragController, var startDragImmediately: (pointersDown: PointersInfo.PointersDown) -> Boolean, var onDragStarted: (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController, var onFirstPointerDown: () -> Unit, swipeDetector: SwipeDetector = DefaultSwipeDetector, private val dispatcher: NestedScrollDispatcher, Loading Loading @@ -185,20 +186,27 @@ internal class MultiPointerDraggableNode( private var lastPointerEvent: PointerEvent? = null private var startedPosition: Offset? = null private var pointersDown: Int = 0 private var countPointersDown: Int = 0 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 } val lastPointerEvent = lastPointerEvent ?: return null if (lastPointerEvent.type == PointerEventType.Scroll) return PointersInfo.MouseWheel val startedPosition = startedPosition ?: return null return PointersInfo( return PointersInfo.PointersDown( startedPosition = startedPosition, pointersDown = pointersDown, lastPointerEvent = lastPointerEvent, count = countPointersDown, countByType = buildMap { lastPointerEvent.changes.fastForEach { change -> if (!change.pressed) return@fastForEach val newValue = (get(change.type) ?: 0) + 1 put(change.type, newValue) } }, ) } Loading @@ -218,11 +226,11 @@ internal class MultiPointerDraggableNode( val changes = pointerEvent.changes lastPointerEvent = pointerEvent pointersDown = changes.countDown() countPointersDown = changes.countDown() when { // There are no more pointers down. pointersDown == 0 -> { countPointersDown == 0 -> { startedPosition = null // In case of multiple events with 0 pointers down (not pressed) we may have Loading Loading @@ -290,8 +298,8 @@ internal class MultiPointerDraggableNode( detectDragGestures( orientation = orientation, startDragImmediately = startDragImmediately, onDragStart = { pointersInfo, overSlop -> onDragStarted(pointersInfo, overSlop) onDragStart = { pointersDown, overSlop -> onDragStarted(pointersDown, overSlop) }, onDrag = { controller, amount -> dispatchScrollEvents( Loading Loading @@ -440,8 +448,8 @@ internal class MultiPointerDraggableNode( */ private suspend fun AwaitPointerEventScope.detectDragGestures( orientation: Orientation, startDragImmediately: (pointersInfo: PointersInfo) -> Boolean, onDragStart: (pointersInfo: PointersInfo, overSlop: Float) -> DragController, startDragImmediately: (pointersDown: PointersInfo.PointersDown) -> Boolean, onDragStart: (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController, onDrag: (controller: DragController, dragAmount: Float) -> Unit, onDragEnd: (controller: DragController) -> Unit, onDragCancel: (controller: DragController) -> Unit, Loading @@ -466,13 +474,14 @@ internal class MultiPointerDraggableNode( .first() var overSlop = 0f var lastPointersInfo = var lastPointersDown: PointersInfo.PointersDown = checkNotNull(pointersInfo()) { "We should have pointers down, last event: $currentEvent" } as PointersInfo.PointersDown val drag = if (startDragImmediately(lastPointersInfo)) { if (startDragImmediately(lastPointersDown)) { consumablePointer.consume() consumablePointer } else { Loading @@ -499,10 +508,11 @@ internal class MultiPointerDraggableNode( ) } ?: return lastPointersInfo = lastPointersDown = checkNotNull(pointersInfo()) { "We should have pointers down, last event: $currentEvent" } as PointersInfo.PointersDown // 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 Loading @@ -516,7 +526,7 @@ internal class MultiPointerDraggableNode( drag } val controller = onDragStart(lastPointersInfo, overSlop) val controller = onDragStart(lastPointersDown, overSlop) val successful: Boolean try { Loading Loading @@ -666,48 +676,31 @@ internal fun interface PointersInfoOwner { fun pointersInfo(): PointersInfo? } internal sealed interface 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. * @param pointersDownByType Provide a map of pointer types to the count of pointers of that type * @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 count The number of pointers currently down. * @param countByType Provide a map of pointer types to the count of pointers of that type * currently down/pressed. */ internal data class PointersInfo( data class PointersDown( val startedPosition: Offset, val pointersDown: Int, val isMouseWheel: Boolean, val pointersDownByType: Map<PointerType, Int>, ) { val count: Int, val countByType: Map<PointerType, Int>, ) : PointersInfo { init { check(pointersDown > 0) { "We should have at least 1 pointer down, $pointersDown instead" } check(count > 0) { "We should have at least 1 pointer down, $count instead" } } } private fun PointersInfo( startedPosition: Offset, pointersDown: Int, lastPointerEvent: PointerEvent, ): PointersInfo { return PointersInfo( startedPosition = startedPosition, pointersDown = pointersDown, isMouseWheel = lastPointerEvent.type == PointerEventType.Scroll, pointersDownByType = buildMap { lastPointerEvent.changes.fastForEach { change -> if (!change.pressed) return@fastForEach val newValue = (get(change.type) ?: 0) + 1 put(change.type, newValue) } }, ) /** Indicates whether the last pointer event was a mouse wheel scroll. */ data object MouseWheel : PointersInfo } packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt +2 −2 Original line number Diff line number Diff line Loading @@ -200,10 +200,10 @@ private class SwipeToSceneNode( override fun onCancelPointerInput() = multiPointerDraggableNode.onCancelPointerInput() private fun startDragImmediately(pointersInfo: PointersInfo): Boolean { private fun startDragImmediately(pointersDown: PointersInfo.PointersDown): 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(pointersInfo) return !canOppositeSwipe() && draggableHandler.shouldImmediatelyIntercept(pointersDown) } private fun canOppositeSwipe(): Boolean { Loading packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +33 −33 File changed.Preview size limit exceeded, changes collapsed. Show changes packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt +1 −1 Original line number Diff line number Diff line Loading @@ -574,7 +574,7 @@ class SwipeToSceneTest { rule.setContent { touchSlop = LocalViewConfiguration.current.touchSlop SceneTransitionLayout(layoutState, Modifier.size(LayoutWidth, LayoutHeight)) { scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) { scene(SceneA, userActions = mapOf(Swipe.Up to SceneB, Swipe.Down to SceneB)) { Box( Modifier.fillMaxSize() // A scrollable that does not consume the scroll gesture Loading Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +62 −42 Original line number Diff line number Diff line Loading @@ -36,11 +36,11 @@ internal typealias SuspendedValue<T> = suspend () -> T internal interface DraggableHandler { /** * Start a drag with the given [pointersInfo] and [overSlop]. * Start a drag with the given [pointersDown] and [overSlop]. * * The returned [DragController] should be used to continue or stop the drag. */ fun onDragStarted(pointersInfo: PointersInfo?, overSlop: Float): DragController fun onDragStarted(pointersDown: PointersInfo.PointersDown?, overSlop: Float): DragController } /** Loading Loading @@ -95,7 +95,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(pointersInfo: PointersInfo?): Boolean { internal fun shouldImmediatelyIntercept(pointersDown: PointersInfo.PointersDown?): Boolean { // We don't intercept the touch if we are not currently driving the transition. val dragController = dragController if (dragController?.isDrivingTransition != true) { Loading @@ -106,7 +106,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(pointersInfo) val swipes = computeSwipes(pointersDown) val fromContent = layoutImpl.content(swipeAnimation.currentContent) val (upOrLeft, downOrRight) = swipes.computeSwipesResults(fromContent) val currentScene = layoutImpl.state.currentScene Loading @@ -123,7 +123,10 @@ internal class DraggableHandlerImpl( )) } override fun onDragStarted(pointersInfo: PointersInfo?, overSlop: Float): DragController { override fun onDragStarted( pointersDown: PointersInfo.PointersDown?, overSlop: Float, ): DragController { if (overSlop == 0f) { val oldDragController = dragController check(oldDragController != null && oldDragController.isDrivingTransition) { Loading @@ -148,7 +151,7 @@ internal class DraggableHandlerImpl( return updateDragController(swipes, swipeAnimation) } val swipes = computeSwipes(pointersInfo) val swipes = computeSwipes(pointersDown) val fromContent = layoutImpl.contentForUserActions() swipes.updateSwipesResults(fromContent) Loading Loading @@ -194,11 +197,11 @@ internal class DraggableHandlerImpl( ) } private fun computeSwipes(pointersInfo: PointersInfo?): Swipes { val fromSource = pointersInfo?.let { resolveSwipeSource(it.startedPosition) } private fun computeSwipes(pointersDown: PointersInfo.PointersDown?): Swipes { val fromSource = pointersDown?.let { resolveSwipeSource(it.startedPosition) } return Swipes( upOrLeft = resolveSwipe(orientation, isUpOrLeft = true, pointersInfo, fromSource), downOrRight = resolveSwipe(orientation, isUpOrLeft = false, pointersInfo, fromSource), upOrLeft = resolveSwipe(orientation, isUpOrLeft = true, pointersDown, fromSource), downOrRight = resolveSwipe(orientation, isUpOrLeft = false, pointersDown, fromSource), ) } } Loading @@ -206,7 +209,7 @@ internal class DraggableHandlerImpl( private fun resolveSwipe( orientation: Orientation, isUpOrLeft: Boolean, pointersInfo: PointersInfo?, pointersDown: PointersInfo.PointersDown?, fromSource: SwipeSource.Resolved?, ): Swipe.Resolved { return Swipe.Resolved( Loading @@ -227,9 +230,9 @@ private fun resolveSwipe( } }, // If the number of pointers is not specified, 1 is assumed. pointerCount = pointersInfo?.pointersDown ?: 1, pointerCount = pointersDown?.count ?: 1, // Resolves the pointer type only if all pointers are of the same type. pointersType = pointersInfo?.pointersDownByType?.keys?.singleOrNull(), pointersType = pointersDown?.countByType?.keys?.singleOrNull(), fromSource = fromSource, ) } Loading Loading @@ -540,13 +543,16 @@ internal class NestedScrollHandlerImpl( val connection: PriorityNestedScrollConnection = nestedScrollConnection() private fun resolveSwipe(isUpOrLeft: Boolean, pointersInfo: PointersInfo?): Swipe.Resolved { private fun resolveSwipe( isUpOrLeft: Boolean, pointersDown: PointersInfo.PointersDown?, ): Swipe.Resolved { return resolveSwipe( orientation = draggableHandler.orientation, isUpOrLeft = isUpOrLeft, pointersInfo = pointersInfo, pointersDown = pointersDown, fromSource = pointersInfo?.let { draggableHandler.resolveSwipeSource(it.startedPosition) }, pointersDown?.let { draggableHandler.resolveSwipeSource(it.startedPosition) }, ) } Loading @@ -555,7 +561,7 @@ internal class NestedScrollHandlerImpl( // moving on to the next scene. var canChangeScene = false var lastPointersInfo: PointersInfo? = null var lastPointersDown: PointersInfo.PointersDown? = null fun hasNextScene(amount: Float): Boolean { val transitionState = layoutState.transitionState Loading @@ -563,8 +569,8 @@ internal class NestedScrollHandlerImpl( val fromScene = layoutImpl.scene(scene) val resolvedSwipe = when { amount < 0f -> resolveSwipe(isUpOrLeft = true, lastPointersInfo) amount > 0f -> resolveSwipe(isUpOrLeft = false, lastPointersInfo) amount < 0f -> resolveSwipe(isUpOrLeft = true, lastPointersDown) amount > 0f -> resolveSwipe(isUpOrLeft = false, lastPointersDown) else -> null } val nextScene = resolvedSwipe?.let { fromScene.findActionResultBestMatch(it) } Loading @@ -581,14 +587,24 @@ internal class NestedScrollHandlerImpl( return PriorityNestedScrollConnection( orientation = orientation, canStartPreScroll = { offsetAvailable, offsetBeforeStart, _ -> val pointersInfo = pointersInfoOwner.pointersInfo() val pointersDown: PointersInfo.PointersDown? = when (val info = pointersInfoOwner.pointersInfo()) { PointersInfo.MouseWheel -> { // Do not support mouse wheel interactions return@PriorityNestedScrollConnection false } is PointersInfo.PointersDown -> info null -> null } canChangeScene = if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f val canInterceptSwipeTransition = canChangeScene && offsetAvailable != 0f && draggableHandler.shouldImmediatelyIntercept(pointersInfo) draggableHandler.shouldImmediatelyIntercept(pointersDown) if (!canInterceptSwipeTransition) return@PriorityNestedScrollConnection false val threshold = layoutImpl.transitionInterceptionThreshold Loading @@ -599,11 +615,7 @@ internal class NestedScrollHandlerImpl( return@PriorityNestedScrollConnection false } if (pointersInfo?.isMouseWheel == true) { // Do not support mouse wheel interactions return@PriorityNestedScrollConnection false } lastPointersInfo = pointersInfo lastPointersDown = pointersDown // 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 Loading @@ -622,12 +634,17 @@ internal class NestedScrollHandlerImpl( val isZeroOffset = if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f val pointersInfo = pointersInfoOwner.pointersInfo() if (pointersInfo?.isMouseWheel == true) { val pointersDown: PointersInfo.PointersDown? = when (val info = pointersInfoOwner.pointersInfo()) { PointersInfo.MouseWheel -> { // Do not support mouse wheel interactions return@PriorityNestedScrollConnection false } lastPointersInfo = pointersInfo is PointersInfo.PointersDown -> info null -> null } lastPointersDown = pointersDown val canStart = when (behavior) { Loading Loading @@ -664,12 +681,17 @@ internal class NestedScrollHandlerImpl( // We could start an overscroll animation canChangeScene = false val pointersInfo = pointersInfoOwner.pointersInfo() if (pointersInfo?.isMouseWheel == true) { val pointersDown: PointersInfo.PointersDown? = when (val info = pointersInfoOwner.pointersInfo()) { PointersInfo.MouseWheel -> { // Do not support mouse wheel interactions return@PriorityNestedScrollConnection false } lastPointersInfo = pointersInfo is PointersInfo.PointersDown -> info null -> null } lastPointersDown = pointersDown val canStart = behavior.canStartOnPostFling && hasNextScene(velocityAvailable) if (canStart) { Loading @@ -679,11 +701,10 @@ internal class NestedScrollHandlerImpl( canStart }, onStart = { firstScroll -> val pointersInfo = lastPointersInfo scrollController( dragController = draggableHandler.onDragStarted( pointersInfo = pointersInfo, pointersDown = lastPointersDown, overSlop = if (isIntercepting) 0f else firstScroll, ), canChangeScene = canChangeScene, Loading @@ -701,8 +722,7 @@ private fun scrollController( ): ScrollController { return object : ScrollController { override fun onScroll(deltaScroll: Float, source: NestedScrollSource): Float { val pointersInfo = pointersInfoOwner.pointersInfo() if (pointersInfo?.isMouseWheel == true) { if (pointersInfoOwner.pointersInfo() == PointersInfo.MouseWheel) { // Do not support mouse wheel interactions return 0f } Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +61 −68 Original line number Diff line number Diff line Loading @@ -80,8 +80,8 @@ import kotlinx.coroutines.launch @Stable internal fun Modifier.multiPointerDraggable( orientation: Orientation, startDragImmediately: (pointersInfo: PointersInfo) -> Boolean, onDragStarted: (pointersInfo: PointersInfo, overSlop: Float) -> DragController, startDragImmediately: (pointersDown: PointersInfo.PointersDown) -> Boolean, onDragStarted: (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController, onFirstPointerDown: () -> Unit = {}, swipeDetector: SwipeDetector = DefaultSwipeDetector, dispatcher: NestedScrollDispatcher, Loading @@ -99,8 +99,9 @@ internal fun Modifier.multiPointerDraggable( private data class MultiPointerDraggableElement( private val orientation: Orientation, private val startDragImmediately: (pointersInfo: PointersInfo) -> Boolean, private val onDragStarted: (pointersInfo: PointersInfo, overSlop: Float) -> DragController, private val startDragImmediately: (pointersDown: PointersInfo.PointersDown) -> Boolean, private val onDragStarted: (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController, private val onFirstPointerDown: () -> Unit, private val swipeDetector: SwipeDetector, private val dispatcher: NestedScrollDispatcher, Loading @@ -126,8 +127,8 @@ private data class MultiPointerDraggableElement( internal class MultiPointerDraggableNode( orientation: Orientation, var startDragImmediately: (pointersInfo: PointersInfo) -> Boolean, var onDragStarted: (pointersInfo: PointersInfo, overSlop: Float) -> DragController, var startDragImmediately: (pointersDown: PointersInfo.PointersDown) -> Boolean, var onDragStarted: (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController, var onFirstPointerDown: () -> Unit, swipeDetector: SwipeDetector = DefaultSwipeDetector, private val dispatcher: NestedScrollDispatcher, Loading Loading @@ -185,20 +186,27 @@ internal class MultiPointerDraggableNode( private var lastPointerEvent: PointerEvent? = null private var startedPosition: Offset? = null private var pointersDown: Int = 0 private var countPointersDown: Int = 0 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 } val lastPointerEvent = lastPointerEvent ?: return null if (lastPointerEvent.type == PointerEventType.Scroll) return PointersInfo.MouseWheel val startedPosition = startedPosition ?: return null return PointersInfo( return PointersInfo.PointersDown( startedPosition = startedPosition, pointersDown = pointersDown, lastPointerEvent = lastPointerEvent, count = countPointersDown, countByType = buildMap { lastPointerEvent.changes.fastForEach { change -> if (!change.pressed) return@fastForEach val newValue = (get(change.type) ?: 0) + 1 put(change.type, newValue) } }, ) } Loading @@ -218,11 +226,11 @@ internal class MultiPointerDraggableNode( val changes = pointerEvent.changes lastPointerEvent = pointerEvent pointersDown = changes.countDown() countPointersDown = changes.countDown() when { // There are no more pointers down. pointersDown == 0 -> { countPointersDown == 0 -> { startedPosition = null // In case of multiple events with 0 pointers down (not pressed) we may have Loading Loading @@ -290,8 +298,8 @@ internal class MultiPointerDraggableNode( detectDragGestures( orientation = orientation, startDragImmediately = startDragImmediately, onDragStart = { pointersInfo, overSlop -> onDragStarted(pointersInfo, overSlop) onDragStart = { pointersDown, overSlop -> onDragStarted(pointersDown, overSlop) }, onDrag = { controller, amount -> dispatchScrollEvents( Loading Loading @@ -440,8 +448,8 @@ internal class MultiPointerDraggableNode( */ private suspend fun AwaitPointerEventScope.detectDragGestures( orientation: Orientation, startDragImmediately: (pointersInfo: PointersInfo) -> Boolean, onDragStart: (pointersInfo: PointersInfo, overSlop: Float) -> DragController, startDragImmediately: (pointersDown: PointersInfo.PointersDown) -> Boolean, onDragStart: (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController, onDrag: (controller: DragController, dragAmount: Float) -> Unit, onDragEnd: (controller: DragController) -> Unit, onDragCancel: (controller: DragController) -> Unit, Loading @@ -466,13 +474,14 @@ internal class MultiPointerDraggableNode( .first() var overSlop = 0f var lastPointersInfo = var lastPointersDown: PointersInfo.PointersDown = checkNotNull(pointersInfo()) { "We should have pointers down, last event: $currentEvent" } as PointersInfo.PointersDown val drag = if (startDragImmediately(lastPointersInfo)) { if (startDragImmediately(lastPointersDown)) { consumablePointer.consume() consumablePointer } else { Loading @@ -499,10 +508,11 @@ internal class MultiPointerDraggableNode( ) } ?: return lastPointersInfo = lastPointersDown = checkNotNull(pointersInfo()) { "We should have pointers down, last event: $currentEvent" } as PointersInfo.PointersDown // 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 Loading @@ -516,7 +526,7 @@ internal class MultiPointerDraggableNode( drag } val controller = onDragStart(lastPointersInfo, overSlop) val controller = onDragStart(lastPointersDown, overSlop) val successful: Boolean try { Loading Loading @@ -666,48 +676,31 @@ internal fun interface PointersInfoOwner { fun pointersInfo(): PointersInfo? } internal sealed interface 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. * @param pointersDownByType Provide a map of pointer types to the count of pointers of that type * @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 count The number of pointers currently down. * @param countByType Provide a map of pointer types to the count of pointers of that type * currently down/pressed. */ internal data class PointersInfo( data class PointersDown( val startedPosition: Offset, val pointersDown: Int, val isMouseWheel: Boolean, val pointersDownByType: Map<PointerType, Int>, ) { val count: Int, val countByType: Map<PointerType, Int>, ) : PointersInfo { init { check(pointersDown > 0) { "We should have at least 1 pointer down, $pointersDown instead" } check(count > 0) { "We should have at least 1 pointer down, $count instead" } } } private fun PointersInfo( startedPosition: Offset, pointersDown: Int, lastPointerEvent: PointerEvent, ): PointersInfo { return PointersInfo( startedPosition = startedPosition, pointersDown = pointersDown, isMouseWheel = lastPointerEvent.type == PointerEventType.Scroll, pointersDownByType = buildMap { lastPointerEvent.changes.fastForEach { change -> if (!change.pressed) return@fastForEach val newValue = (get(change.type) ?: 0) + 1 put(change.type, newValue) } }, ) /** Indicates whether the last pointer event was a mouse wheel scroll. */ data object MouseWheel : PointersInfo }
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt +2 −2 Original line number Diff line number Diff line Loading @@ -200,10 +200,10 @@ private class SwipeToSceneNode( override fun onCancelPointerInput() = multiPointerDraggableNode.onCancelPointerInput() private fun startDragImmediately(pointersInfo: PointersInfo): Boolean { private fun startDragImmediately(pointersDown: PointersInfo.PointersDown): 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(pointersInfo) return !canOppositeSwipe() && draggableHandler.shouldImmediatelyIntercept(pointersDown) } private fun canOppositeSwipe(): Boolean { Loading
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +33 −33 File changed.Preview size limit exceeded, changes collapsed. Show changes
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt +1 −1 Original line number Diff line number Diff line Loading @@ -574,7 +574,7 @@ class SwipeToSceneTest { rule.setContent { touchSlop = LocalViewConfiguration.current.touchSlop SceneTransitionLayout(layoutState, Modifier.size(LayoutWidth, LayoutHeight)) { scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) { scene(SceneA, userActions = mapOf(Swipe.Up to SceneB, Swipe.Down to SceneB)) { Box( Modifier.fillMaxSize() // A scrollable that does not consume the scroll gesture Loading