summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt125
-rw-r--r--packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt33
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 {