Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +25 −3 Original line number Diff line number Diff line Loading @@ -631,7 +631,13 @@ internal class NestedScrollHandlerImpl( return@PriorityNestedScrollConnection false } _lastPointersInfo = pointersInfoOwner.pointersInfo() val pointersInfo = pointersInfoOwner.pointersInfo() if (pointersInfo.isMouseWheel) { // Do not support mouse wheel interactions return@PriorityNestedScrollConnection false } _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 Loading @@ -650,7 +656,12 @@ internal class NestedScrollHandlerImpl( val isZeroOffset = if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f _lastPointersInfo = pointersInfoOwner.pointersInfo() val pointersInfo = pointersInfoOwner.pointersInfo() if (pointersInfo.isMouseWheel) { // Do not support mouse wheel interactions return@PriorityNestedScrollConnection false } _lastPointersInfo = pointersInfo val canStart = when (behavior) { Loading Loading @@ -685,7 +696,12 @@ internal class NestedScrollHandlerImpl( // We could start an overscroll animation canChangeScene = false _lastPointersInfo = pointersInfoOwner.pointersInfo() val pointersInfo = pointersInfoOwner.pointersInfo() if (pointersInfo.isMouseWheel) { // Do not support mouse wheel interactions return@PriorityNestedScrollConnection false } _lastPointersInfo = pointersInfo val canStart = behavior.canStartOnPostFling && hasNextScene(velocityAvailable) if (canStart) { Loading @@ -707,6 +723,12 @@ internal class NestedScrollHandlerImpl( onScroll = { offsetAvailable, _ -> val controller = dragController ?: error("Should be called after onStart") val pointersInfo = pointersInfoOwner.pointersInfo() if (pointersInfo.isMouseWheel) { // Do not support mouse wheel interactions return@PriorityNestedScrollConnection 0f } // TODO(b/297842071) We should handle the overscroll or slow drag if the gesture is // initiated in a nested child. controller.onDrag(delta = offsetAvailable) Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +18 −3 Original line number Diff line number Diff line Loading @@ -29,6 +29,7 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.PointerId import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerInputScope Loading Loading @@ -184,6 +185,7 @@ internal class MultiPointerDraggableNode( private var startedPosition: Offset? = null private var pointersDown: Int = 0 private var isMouseWheel: Boolean = false internal fun pointersInfo(): PointersInfo { return PointersInfo( Loading @@ -191,6 +193,7 @@ internal class MultiPointerDraggableNode( startedPosition = startedPosition, // We could have 0 pointers during fling or for other reasons. pointersDown = pointersDown.coerceAtLeast(1), isMouseWheel = isMouseWheel, ) } Loading @@ -202,8 +205,15 @@ internal class MultiPointerDraggableNode( // [requireAncestorPointersInfoOwner], to our descendants. while (currentContext.isActive) { // During the Initial pass, we receive the event after our ancestors. val changes = awaitPointerEvent(PointerEventPass.Initial).changes val pointerEvent = awaitPointerEvent(PointerEventPass.Initial) // Ignore cursor has entered the input region. // This will only be sent after the cursor is hovering when in the input region. if (pointerEvent.type == PointerEventType.Enter) continue val changes = pointerEvent.changes pointersDown = changes.countDown() isMouseWheel = pointerEvent.type == PointerEventType.Scroll when { // There are no more pointers down. Loading @@ -223,7 +233,8 @@ internal class MultiPointerDraggableNode( // The first pointer down, startedPosition was not set. startedPosition == null -> { val firstPointerDown = changes.single() // Mouse wheel could start with multiple pointer down val firstPointerDown = changes.first() velocityPointerId = firstPointerDown.id velocityTracker.resetTracking() velocityTracker.addPointerInputChange(firstPointerDown) Loading Loading @@ -647,4 +658,8 @@ internal fun interface PointersInfoOwner { fun pointersInfo(): PointersInfo } internal data class PointersInfo(val startedPosition: Offset?, val pointersDown: Int) internal data class PointersInfo( val startedPosition: Offset?, val pointersDown: Int, val isMouseWheel: Boolean, ) packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +25 −3 Original line number Diff line number Diff line Loading @@ -127,7 +127,7 @@ class DraggableHandlerTest { val horizontalDraggableHandler = layoutImpl.draggableHandler(Orientation.Horizontal) var pointerInfoOwner: () -> PointersInfo = { PointersInfo(startedPosition = Offset.Zero, pointersDown = 1) PointersInfo(startedPosition = Offset.Zero, pointersDown = 1, isMouseWheel = false) } fun nestedScrollConnection( Loading Loading @@ -1187,7 +1187,9 @@ class DraggableHandlerTest { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways) // Drag from the **top** of the screen pointerInfoOwner = { PointersInfo(startedPosition = Offset(0f, 0f), pointersDown = 1) } pointerInfoOwner = { PointersInfo(startedPosition = Offset(0f, 0f), pointersDown = 1, isMouseWheel = false) } assertIdle(currentScene = SceneC) nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f)) Loading @@ -1205,7 +1207,11 @@ class DraggableHandlerTest { // Drag from the **bottom** of the screen pointerInfoOwner = { PointersInfo(startedPosition = Offset(0f, SCREEN_SIZE), pointersDown = 1) PointersInfo( startedPosition = Offset(0f, SCREEN_SIZE), pointersDown = 1, isMouseWheel = false, ) } assertIdle(currentScene = SceneC) Loading @@ -1219,6 +1225,22 @@ class DraggableHandlerTest { ) } @Test fun ignoreMouseWheel() = runGestureTest { // Start at scene C. navigateToSceneC() val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways) // Use mouse wheel pointerInfoOwner = { PointersInfo(startedPosition = Offset(0f, 0f), pointersDown = 1, isMouseWheel = true) } assertIdle(currentScene = SceneC) nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f)) assertIdle(currentScene = SceneC) } @Test fun transitionIsImmediatelyUpdatedWhenReleasingFinger() = runGestureTest { // Swipe up from the middle to transition to scene B. Loading packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt +87 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,8 @@ package com.android.compose.animation.scene import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.rememberScrollableState import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize Loading @@ -39,12 +41,14 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.ScrollWheel import androidx.compose.ui.test.assertPositionInRootIsEqualTo import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performMouseInput import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeRight import androidx.compose.ui.test.swipeUp Loading Loading @@ -497,6 +501,89 @@ class SwipeToSceneTest { assertThat(layoutState.currentTransition).isNotNull() } @Test fun mouseWheel_pointerInputApi_ignoredByStl() { val layoutState = layoutState() var touchSlop = 0f rule.setContent { touchSlop = LocalViewConfiguration.current.touchSlop TestContent(layoutState) } rule.onRoot().performMouseInput { enter(middle) scroll(touchSlop, ScrollWheel.Vertical) } // Mouse wheel scroll are ignored assertThat(layoutState.currentTransition).isNull() } @Test fun mouseWheel_scrollableCannotScroll_ignoredByStl() { val layoutState = layoutState() var touchSlop = 0f rule.setContent { touchSlop = LocalViewConfiguration.current.touchSlop SceneTransitionLayout(layoutState, Modifier.size(LayoutWidth, LayoutHeight)) { scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) { Box( Modifier.fillMaxSize() // A scrollable that does not consume the scroll gesture .scrollable(rememberScrollableState { 0f }, Orientation.Vertical) ) } scene(SceneB) { Box(Modifier.fillMaxSize()) } } } rule.onRoot().performMouseInput { enter(middle) scroll(touchSlop, ScrollWheel.Vertical) } // Mouse wheel scroll are ignored assertThat(layoutState.currentTransition).isNull() } @Test fun mouseWheel_scrollableConsume_ignoredByStl() { val layoutState = layoutState() var touchSlop = 0f var lastScroll = 0f rule.setContent { touchSlop = LocalViewConfiguration.current.touchSlop SceneTransitionLayout(layoutState, Modifier.size(LayoutWidth, LayoutHeight)) { scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) { Box( Modifier.fillMaxSize() // A scrollable that consumes the scroll gesture .scrollable( rememberScrollableState { lastScroll = it it }, Orientation.Vertical, ) ) } scene(SceneB) { Box(Modifier.fillMaxSize()) } } } rule.onRoot().performMouseInput { enter(middle) scroll(touchSlop * 10, ScrollWheel.Vertical) } // Mouse wheel scroll are ignored assertThat(layoutState.currentTransition).isNull() // Mouse wheel scroll can still be consumed by the scrollable assertThat(lastScroll).isNotEqualTo(0f) assertThat(touchSlop).isNotEqualTo(0f) } @Test fun transitionKey() { val transitionkey = TransitionKey(debugName = "foo") Loading Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +25 −3 Original line number Diff line number Diff line Loading @@ -631,7 +631,13 @@ internal class NestedScrollHandlerImpl( return@PriorityNestedScrollConnection false } _lastPointersInfo = pointersInfoOwner.pointersInfo() val pointersInfo = pointersInfoOwner.pointersInfo() if (pointersInfo.isMouseWheel) { // Do not support mouse wheel interactions return@PriorityNestedScrollConnection false } _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 Loading @@ -650,7 +656,12 @@ internal class NestedScrollHandlerImpl( val isZeroOffset = if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f _lastPointersInfo = pointersInfoOwner.pointersInfo() val pointersInfo = pointersInfoOwner.pointersInfo() if (pointersInfo.isMouseWheel) { // Do not support mouse wheel interactions return@PriorityNestedScrollConnection false } _lastPointersInfo = pointersInfo val canStart = when (behavior) { Loading Loading @@ -685,7 +696,12 @@ internal class NestedScrollHandlerImpl( // We could start an overscroll animation canChangeScene = false _lastPointersInfo = pointersInfoOwner.pointersInfo() val pointersInfo = pointersInfoOwner.pointersInfo() if (pointersInfo.isMouseWheel) { // Do not support mouse wheel interactions return@PriorityNestedScrollConnection false } _lastPointersInfo = pointersInfo val canStart = behavior.canStartOnPostFling && hasNextScene(velocityAvailable) if (canStart) { Loading @@ -707,6 +723,12 @@ internal class NestedScrollHandlerImpl( onScroll = { offsetAvailable, _ -> val controller = dragController ?: error("Should be called after onStart") val pointersInfo = pointersInfoOwner.pointersInfo() if (pointersInfo.isMouseWheel) { // Do not support mouse wheel interactions return@PriorityNestedScrollConnection 0f } // TODO(b/297842071) We should handle the overscroll or slow drag if the gesture is // initiated in a nested child. controller.onDrag(delta = offsetAvailable) Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +18 −3 Original line number Diff line number Diff line Loading @@ -29,6 +29,7 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.PointerId import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerInputScope Loading Loading @@ -184,6 +185,7 @@ internal class MultiPointerDraggableNode( private var startedPosition: Offset? = null private var pointersDown: Int = 0 private var isMouseWheel: Boolean = false internal fun pointersInfo(): PointersInfo { return PointersInfo( Loading @@ -191,6 +193,7 @@ internal class MultiPointerDraggableNode( startedPosition = startedPosition, // We could have 0 pointers during fling or for other reasons. pointersDown = pointersDown.coerceAtLeast(1), isMouseWheel = isMouseWheel, ) } Loading @@ -202,8 +205,15 @@ internal class MultiPointerDraggableNode( // [requireAncestorPointersInfoOwner], to our descendants. while (currentContext.isActive) { // During the Initial pass, we receive the event after our ancestors. val changes = awaitPointerEvent(PointerEventPass.Initial).changes val pointerEvent = awaitPointerEvent(PointerEventPass.Initial) // Ignore cursor has entered the input region. // This will only be sent after the cursor is hovering when in the input region. if (pointerEvent.type == PointerEventType.Enter) continue val changes = pointerEvent.changes pointersDown = changes.countDown() isMouseWheel = pointerEvent.type == PointerEventType.Scroll when { // There are no more pointers down. Loading @@ -223,7 +233,8 @@ internal class MultiPointerDraggableNode( // The first pointer down, startedPosition was not set. startedPosition == null -> { val firstPointerDown = changes.single() // Mouse wheel could start with multiple pointer down val firstPointerDown = changes.first() velocityPointerId = firstPointerDown.id velocityTracker.resetTracking() velocityTracker.addPointerInputChange(firstPointerDown) Loading Loading @@ -647,4 +658,8 @@ internal fun interface PointersInfoOwner { fun pointersInfo(): PointersInfo } internal data class PointersInfo(val startedPosition: Offset?, val pointersDown: Int) internal data class PointersInfo( val startedPosition: Offset?, val pointersDown: Int, val isMouseWheel: Boolean, )
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +25 −3 Original line number Diff line number Diff line Loading @@ -127,7 +127,7 @@ class DraggableHandlerTest { val horizontalDraggableHandler = layoutImpl.draggableHandler(Orientation.Horizontal) var pointerInfoOwner: () -> PointersInfo = { PointersInfo(startedPosition = Offset.Zero, pointersDown = 1) PointersInfo(startedPosition = Offset.Zero, pointersDown = 1, isMouseWheel = false) } fun nestedScrollConnection( Loading Loading @@ -1187,7 +1187,9 @@ class DraggableHandlerTest { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways) // Drag from the **top** of the screen pointerInfoOwner = { PointersInfo(startedPosition = Offset(0f, 0f), pointersDown = 1) } pointerInfoOwner = { PointersInfo(startedPosition = Offset(0f, 0f), pointersDown = 1, isMouseWheel = false) } assertIdle(currentScene = SceneC) nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f)) Loading @@ -1205,7 +1207,11 @@ class DraggableHandlerTest { // Drag from the **bottom** of the screen pointerInfoOwner = { PointersInfo(startedPosition = Offset(0f, SCREEN_SIZE), pointersDown = 1) PointersInfo( startedPosition = Offset(0f, SCREEN_SIZE), pointersDown = 1, isMouseWheel = false, ) } assertIdle(currentScene = SceneC) Loading @@ -1219,6 +1225,22 @@ class DraggableHandlerTest { ) } @Test fun ignoreMouseWheel() = runGestureTest { // Start at scene C. navigateToSceneC() val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways) // Use mouse wheel pointerInfoOwner = { PointersInfo(startedPosition = Offset(0f, 0f), pointersDown = 1, isMouseWheel = true) } assertIdle(currentScene = SceneC) nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f)) assertIdle(currentScene = SceneC) } @Test fun transitionIsImmediatelyUpdatedWhenReleasingFinger() = runGestureTest { // Swipe up from the middle to transition to scene B. Loading
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt +87 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,8 @@ package com.android.compose.animation.scene import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.rememberScrollableState import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize Loading @@ -39,12 +41,14 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.ScrollWheel import androidx.compose.ui.test.assertPositionInRootIsEqualTo import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performMouseInput import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeRight import androidx.compose.ui.test.swipeUp Loading Loading @@ -497,6 +501,89 @@ class SwipeToSceneTest { assertThat(layoutState.currentTransition).isNotNull() } @Test fun mouseWheel_pointerInputApi_ignoredByStl() { val layoutState = layoutState() var touchSlop = 0f rule.setContent { touchSlop = LocalViewConfiguration.current.touchSlop TestContent(layoutState) } rule.onRoot().performMouseInput { enter(middle) scroll(touchSlop, ScrollWheel.Vertical) } // Mouse wheel scroll are ignored assertThat(layoutState.currentTransition).isNull() } @Test fun mouseWheel_scrollableCannotScroll_ignoredByStl() { val layoutState = layoutState() var touchSlop = 0f rule.setContent { touchSlop = LocalViewConfiguration.current.touchSlop SceneTransitionLayout(layoutState, Modifier.size(LayoutWidth, LayoutHeight)) { scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) { Box( Modifier.fillMaxSize() // A scrollable that does not consume the scroll gesture .scrollable(rememberScrollableState { 0f }, Orientation.Vertical) ) } scene(SceneB) { Box(Modifier.fillMaxSize()) } } } rule.onRoot().performMouseInput { enter(middle) scroll(touchSlop, ScrollWheel.Vertical) } // Mouse wheel scroll are ignored assertThat(layoutState.currentTransition).isNull() } @Test fun mouseWheel_scrollableConsume_ignoredByStl() { val layoutState = layoutState() var touchSlop = 0f var lastScroll = 0f rule.setContent { touchSlop = LocalViewConfiguration.current.touchSlop SceneTransitionLayout(layoutState, Modifier.size(LayoutWidth, LayoutHeight)) { scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) { Box( Modifier.fillMaxSize() // A scrollable that consumes the scroll gesture .scrollable( rememberScrollableState { lastScroll = it it }, Orientation.Vertical, ) ) } scene(SceneB) { Box(Modifier.fillMaxSize()) } } } rule.onRoot().performMouseInput { enter(middle) scroll(touchSlop * 10, ScrollWheel.Vertical) } // Mouse wheel scroll are ignored assertThat(layoutState.currentTransition).isNull() // Mouse wheel scroll can still be consumed by the scrollable assertThat(lastScroll).isNotEqualTo(0f) assertThat(touchSlop).isNotEqualTo(0f) } @Test fun transitionKey() { val transitionkey = TransitionKey(debugName = "foo") Loading