Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit be9d1529 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Don't install a pointer input when a NestedDraggable is disabled

This CL is inspired from ag/29699552: when a Modifier.nestedDraggable is
disabled, we make sure that we don't even install a
PointerInputModifierNode so that gestures will be dispatched to siblings
with lower z-Indices.

Bug: 378470603
Test: atest NestedDraggableTest
Flag: EXEMPT NestedDraggable is not used yet
Change-Id: Id596d79f733cd7d1c633a6f456809e7a30b74945
parent 24511345
Loading
Loading
Loading
Loading
+73 −52
Original line number Diff line number Diff line
@@ -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()
    }

    /*
+33 −0
Original line number Diff line number Diff line
@@ -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 {