Loading packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt +73 −52 Original line number Diff line number Diff line Loading @@ -147,21 +147,66 @@ private data class NestedDraggableElement( private val orientation: Orientation, private val overscrollEffect: OverscrollEffect?, private val enabled: Boolean, ) : ModifierNodeElement<NestedDraggableNode>() { override fun create(): NestedDraggableNode { return NestedDraggableNode(draggable, orientation, overscrollEffect, enabled) ) : ModifierNodeElement<NestedDraggableRootNode>() { override fun create(): NestedDraggableRootNode { return NestedDraggableRootNode(draggable, orientation, overscrollEffect, enabled) } override fun update(node: NestedDraggableNode) { override fun update(node: NestedDraggableRootNode) { node.update(draggable, orientation, overscrollEffect, enabled) } } /** * A root node on top of [NestedDraggableNode] so that no [PointerInputModifierNode] is installed * when this draggable is disabled. */ private class NestedDraggableRootNode( draggable: NestedDraggable, orientation: Orientation, overscrollEffect: OverscrollEffect?, enabled: Boolean, ) : DelegatingNode() { private var delegateNode = if (enabled) create(draggable, orientation, overscrollEffect) else null fun update( draggable: NestedDraggable, orientation: Orientation, overscrollEffect: OverscrollEffect?, enabled: Boolean, ) { // Disabled. if (!enabled) { delegateNode?.let { undelegate(it) } delegateNode = null return } // Disabled => Enabled. val nullableDelegate = delegateNode if (nullableDelegate == null) { delegateNode = create(draggable, orientation, overscrollEffect) return } // Enabled => Enabled (update). nullableDelegate.update(draggable, orientation, overscrollEffect) } private fun create( draggable: NestedDraggable, orientation: Orientation, overscrollEffect: OverscrollEffect?, ): NestedDraggableNode { return delegate(NestedDraggableNode(draggable, orientation, overscrollEffect)) } } private class NestedDraggableNode( private var draggable: NestedDraggable, override var orientation: Orientation, private var overscrollEffect: OverscrollEffect?, private var enabled: Boolean, ) : DelegatingNode(), PointerInputModifierNode, Loading @@ -169,23 +214,11 @@ private class NestedDraggableNode( CompositionLocalConsumerModifierNode, OrientationAware { private val nestedScrollDispatcher = NestedScrollDispatcher() private var trackWheelScroll: SuspendingPointerInputModifierNode? = null set(value) { field?.let { undelegate(it) } field = value?.also { delegate(it) } } private var trackDownPositionDelegate: SuspendingPointerInputModifierNode? = null set(value) { field?.let { undelegate(it) } field = value?.also { delegate(it) } } private var detectDragsDelegate: SuspendingPointerInputModifierNode? = null set(value) { field?.let { undelegate(it) } field = value?.also { delegate(it) } } private val trackWheelScroll = delegate(SuspendingPointerInputModifierNode { trackWheelScroll() }) private val trackDownPositionDelegate = delegate(SuspendingPointerInputModifierNode { trackDownPosition() }) private val detectDragsDelegate = delegate(SuspendingPointerInputModifierNode { detectDrags() }) /** The controller created by the nested scroll logic (and *not* the drag logic). */ private var nestedScrollController: NestedScrollController? = null Loading Loading @@ -214,26 +247,25 @@ private class NestedDraggableNode( draggable: NestedDraggable, orientation: Orientation, overscrollEffect: OverscrollEffect?, enabled: Boolean, ) { if ( draggable == this.draggable && orientation == this.orientation && overscrollEffect == this.overscrollEffect ) { return } this.draggable = draggable this.orientation = orientation this.overscrollEffect = overscrollEffect this.enabled = enabled trackDownPositionDelegate?.resetPointerInputHandler() detectDragsDelegate?.resetPointerInputHandler() trackWheelScroll.resetPointerInputHandler() trackDownPositionDelegate.resetPointerInputHandler() detectDragsDelegate.resetPointerInputHandler() nestedScrollController?.ensureOnDragStoppedIsCalled() nestedScrollController = null if (!enabled && trackWheelScroll != null) { check(trackDownPositionDelegate != null) check(detectDragsDelegate != null) trackWheelScroll = null trackDownPositionDelegate = null detectDragsDelegate = null } } override fun onPointerEvent( Loading @@ -241,26 +273,15 @@ private class NestedDraggableNode( pass: PointerEventPass, bounds: IntSize, ) { if (!enabled) return if (trackWheelScroll == null) { check(trackDownPositionDelegate == null) check(detectDragsDelegate == null) trackWheelScroll = SuspendingPointerInputModifierNode { trackWheelScroll() } trackDownPositionDelegate = SuspendingPointerInputModifierNode { trackDownPosition() } detectDragsDelegate = SuspendingPointerInputModifierNode { detectDrags() } } checkNotNull(trackWheelScroll).onPointerEvent(pointerEvent, pass, bounds) checkNotNull(trackDownPositionDelegate).onPointerEvent(pointerEvent, pass, bounds) checkNotNull(detectDragsDelegate).onPointerEvent(pointerEvent, pass, bounds) trackWheelScroll.onPointerEvent(pointerEvent, pass, bounds) trackDownPositionDelegate.onPointerEvent(pointerEvent, pass, bounds) detectDragsDelegate.onPointerEvent(pointerEvent, pass, bounds) } override fun onCancelPointerInput() { trackWheelScroll?.onCancelPointerInput() trackDownPositionDelegate?.onCancelPointerInput() detectDragsDelegate?.onCancelPointerInput() trackWheelScroll.onCancelPointerInput() trackDownPositionDelegate.onCancelPointerInput() detectDragsDelegate.onCancelPointerInput() } /* Loading packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt +33 −0 Original line number Diff line number Diff line Loading @@ -25,11 +25,14 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection Loading @@ -37,10 +40,14 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerType import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.ScrollWheel import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.junit4.ComposeContentTestRule 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.swipeDown Loading Loading @@ -693,6 +700,7 @@ class NestedDraggableTest(override val orientation: Orientation) : OrientationAw } @Test @Ignore("b/388507816: re-enable this when the crash in HitPath is fixed") fun pointersDown_clearedWhenDisabled() { val draggable = TestDraggable() var enabled by mutableStateOf(true) Loading Loading @@ -740,6 +748,31 @@ class NestedDraggableTest(override val orientation: Orientation) : OrientationAw assertThat(draggable.onDragStartedCalled).isFalse() } @Test fun doesNotConsumeGesturesWhenDisabled() { val buttonTag = "button" rule.setContent { Box { var count by remember { mutableStateOf(0) } Button(onClick = { count++ }, Modifier.testTag(buttonTag).align(Alignment.Center)) { Text("Count: $count") } Box( Modifier.fillMaxSize() .nestedDraggable(remember { TestDraggable() }, orientation, enabled = false) ) } } rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 0") // Click on the root at its center, where the button is located. Clicks should go through // the draggable and reach the button given that it is disabled. repeat(3) { rule.onRoot().performClick() } rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 3") } private fun ComposeContentTestRule.setContentWithTouchSlop( content: @Composable () -> Unit ): Float { Loading Loading
packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt +73 −52 Original line number Diff line number Diff line Loading @@ -147,21 +147,66 @@ private data class NestedDraggableElement( private val orientation: Orientation, private val overscrollEffect: OverscrollEffect?, private val enabled: Boolean, ) : ModifierNodeElement<NestedDraggableNode>() { override fun create(): NestedDraggableNode { return NestedDraggableNode(draggable, orientation, overscrollEffect, enabled) ) : ModifierNodeElement<NestedDraggableRootNode>() { override fun create(): NestedDraggableRootNode { return NestedDraggableRootNode(draggable, orientation, overscrollEffect, enabled) } override fun update(node: NestedDraggableNode) { override fun update(node: NestedDraggableRootNode) { node.update(draggable, orientation, overscrollEffect, enabled) } } /** * A root node on top of [NestedDraggableNode] so that no [PointerInputModifierNode] is installed * when this draggable is disabled. */ private class NestedDraggableRootNode( draggable: NestedDraggable, orientation: Orientation, overscrollEffect: OverscrollEffect?, enabled: Boolean, ) : DelegatingNode() { private var delegateNode = if (enabled) create(draggable, orientation, overscrollEffect) else null fun update( draggable: NestedDraggable, orientation: Orientation, overscrollEffect: OverscrollEffect?, enabled: Boolean, ) { // Disabled. if (!enabled) { delegateNode?.let { undelegate(it) } delegateNode = null return } // Disabled => Enabled. val nullableDelegate = delegateNode if (nullableDelegate == null) { delegateNode = create(draggable, orientation, overscrollEffect) return } // Enabled => Enabled (update). nullableDelegate.update(draggable, orientation, overscrollEffect) } private fun create( draggable: NestedDraggable, orientation: Orientation, overscrollEffect: OverscrollEffect?, ): NestedDraggableNode { return delegate(NestedDraggableNode(draggable, orientation, overscrollEffect)) } } private class NestedDraggableNode( private var draggable: NestedDraggable, override var orientation: Orientation, private var overscrollEffect: OverscrollEffect?, private var enabled: Boolean, ) : DelegatingNode(), PointerInputModifierNode, Loading @@ -169,23 +214,11 @@ private class NestedDraggableNode( CompositionLocalConsumerModifierNode, OrientationAware { private val nestedScrollDispatcher = NestedScrollDispatcher() private var trackWheelScroll: SuspendingPointerInputModifierNode? = null set(value) { field?.let { undelegate(it) } field = value?.also { delegate(it) } } private var trackDownPositionDelegate: SuspendingPointerInputModifierNode? = null set(value) { field?.let { undelegate(it) } field = value?.also { delegate(it) } } private var detectDragsDelegate: SuspendingPointerInputModifierNode? = null set(value) { field?.let { undelegate(it) } field = value?.also { delegate(it) } } private val trackWheelScroll = delegate(SuspendingPointerInputModifierNode { trackWheelScroll() }) private val trackDownPositionDelegate = delegate(SuspendingPointerInputModifierNode { trackDownPosition() }) private val detectDragsDelegate = delegate(SuspendingPointerInputModifierNode { detectDrags() }) /** The controller created by the nested scroll logic (and *not* the drag logic). */ private var nestedScrollController: NestedScrollController? = null Loading Loading @@ -214,26 +247,25 @@ private class NestedDraggableNode( draggable: NestedDraggable, orientation: Orientation, overscrollEffect: OverscrollEffect?, enabled: Boolean, ) { if ( draggable == this.draggable && orientation == this.orientation && overscrollEffect == this.overscrollEffect ) { return } this.draggable = draggable this.orientation = orientation this.overscrollEffect = overscrollEffect this.enabled = enabled trackDownPositionDelegate?.resetPointerInputHandler() detectDragsDelegate?.resetPointerInputHandler() trackWheelScroll.resetPointerInputHandler() trackDownPositionDelegate.resetPointerInputHandler() detectDragsDelegate.resetPointerInputHandler() nestedScrollController?.ensureOnDragStoppedIsCalled() nestedScrollController = null if (!enabled && trackWheelScroll != null) { check(trackDownPositionDelegate != null) check(detectDragsDelegate != null) trackWheelScroll = null trackDownPositionDelegate = null detectDragsDelegate = null } } override fun onPointerEvent( Loading @@ -241,26 +273,15 @@ private class NestedDraggableNode( pass: PointerEventPass, bounds: IntSize, ) { if (!enabled) return if (trackWheelScroll == null) { check(trackDownPositionDelegate == null) check(detectDragsDelegate == null) trackWheelScroll = SuspendingPointerInputModifierNode { trackWheelScroll() } trackDownPositionDelegate = SuspendingPointerInputModifierNode { trackDownPosition() } detectDragsDelegate = SuspendingPointerInputModifierNode { detectDrags() } } checkNotNull(trackWheelScroll).onPointerEvent(pointerEvent, pass, bounds) checkNotNull(trackDownPositionDelegate).onPointerEvent(pointerEvent, pass, bounds) checkNotNull(detectDragsDelegate).onPointerEvent(pointerEvent, pass, bounds) trackWheelScroll.onPointerEvent(pointerEvent, pass, bounds) trackDownPositionDelegate.onPointerEvent(pointerEvent, pass, bounds) detectDragsDelegate.onPointerEvent(pointerEvent, pass, bounds) } override fun onCancelPointerInput() { trackWheelScroll?.onCancelPointerInput() trackDownPositionDelegate?.onCancelPointerInput() detectDragsDelegate?.onCancelPointerInput() trackWheelScroll.onCancelPointerInput() trackDownPositionDelegate.onCancelPointerInput() detectDragsDelegate.onCancelPointerInput() } /* Loading
packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt +33 −0 Original line number Diff line number Diff line Loading @@ -25,11 +25,14 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection Loading @@ -37,10 +40,14 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerType import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.ScrollWheel import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.junit4.ComposeContentTestRule 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.swipeDown Loading Loading @@ -693,6 +700,7 @@ class NestedDraggableTest(override val orientation: Orientation) : OrientationAw } @Test @Ignore("b/388507816: re-enable this when the crash in HitPath is fixed") fun pointersDown_clearedWhenDisabled() { val draggable = TestDraggable() var enabled by mutableStateOf(true) Loading Loading @@ -740,6 +748,31 @@ class NestedDraggableTest(override val orientation: Orientation) : OrientationAw assertThat(draggable.onDragStartedCalled).isFalse() } @Test fun doesNotConsumeGesturesWhenDisabled() { val buttonTag = "button" rule.setContent { Box { var count by remember { mutableStateOf(0) } Button(onClick = { count++ }, Modifier.testTag(buttonTag).align(Alignment.Center)) { Text("Count: $count") } Box( Modifier.fillMaxSize() .nestedDraggable(remember { TestDraggable() }, orientation, enabled = false) ) } } rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 0") // Click on the root at its center, where the button is located. Clicks should go through // the draggable and reach the button given that it is disabled. repeat(3) { rule.onRoot().performClick() } rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 3") } private fun ComposeContentTestRule.setContentWithTouchSlop( content: @Composable () -> Unit ): Float { Loading