diff options
| -rw-r--r-- | packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt | 125 | ||||
| -rw-r--r-- | packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt | 33 |
2 files changed, 106 insertions, 52 deletions
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt index c7d6e8aed3b4..96401ce6e1c7 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt @@ -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, @@ -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 @@ -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( @@ -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() } /* diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt index f9cf495d9d9f..5de0f1221f0f 100644 --- a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt +++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt @@ -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 @@ -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 @@ -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) @@ -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 { |