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

Commit 61cc9d30 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere Committed by Android (Google) Code Review
Browse files

Merge "Reapply "Don't install a pointer input when there are no user actions"" into main

parents 6f9d5cec 04a358e0
Loading
Loading
Loading
Loading
+3 −35
Original line number Diff line number Diff line
@@ -42,10 +42,8 @@ import androidx.compose.ui.input.pointer.util.addPointerInputChange
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
import androidx.compose.ui.node.DelegatingNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.ObserverModifierNode
import androidx.compose.ui.node.PointerInputModifierNode
import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.node.observeReads
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.Velocity
@@ -79,7 +77,6 @@ import kotlinx.coroutines.launch
@Stable
internal fun Modifier.multiPointerDraggable(
    orientation: Orientation,
    enabled: () -> Boolean,
    startDragImmediately: (startedPosition: Offset) -> Boolean,
    onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController,
    onFirstPointerDown: () -> Unit = {},
@@ -89,7 +86,6 @@ internal fun Modifier.multiPointerDraggable(
    this.then(
        MultiPointerDraggableElement(
            orientation,
            enabled,
            startDragImmediately,
            onDragStarted,
            onFirstPointerDown,
@@ -100,7 +96,6 @@ internal fun Modifier.multiPointerDraggable(

private data class MultiPointerDraggableElement(
    private val orientation: Orientation,
    private val enabled: () -> Boolean,
    private val startDragImmediately: (startedPosition: Offset) -> Boolean,
    private val onDragStarted:
        (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController,
@@ -111,7 +106,6 @@ private data class MultiPointerDraggableElement(
    override fun create(): MultiPointerDraggableNode =
        MultiPointerDraggableNode(
            orientation = orientation,
            enabled = enabled,
            startDragImmediately = startDragImmediately,
            onDragStarted = onDragStarted,
            onFirstPointerDown = onFirstPointerDown,
@@ -121,7 +115,6 @@ private data class MultiPointerDraggableElement(

    override fun update(node: MultiPointerDraggableNode) {
        node.orientation = orientation
        node.enabled = enabled
        node.startDragImmediately = startDragImmediately
        node.onDragStarted = onDragStarted
        node.onFirstPointerDown = onFirstPointerDown
@@ -131,27 +124,23 @@ private data class MultiPointerDraggableElement(

internal class MultiPointerDraggableNode(
    orientation: Orientation,
    enabled: () -> Boolean,
    var startDragImmediately: (startedPosition: Offset) -> Boolean,
    var onDragStarted:
        (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController,
    var onFirstPointerDown: () -> Unit,
    var swipeDetector: SwipeDetector = DefaultSwipeDetector,
    swipeDetector: SwipeDetector = DefaultSwipeDetector,
    private val dispatcher: NestedScrollDispatcher,
) :
    DelegatingNode(),
    PointerInputModifierNode,
    CompositionLocalConsumerModifierNode,
    ObserverModifierNode,
    SpaceVectorConverter {
    private val pointerTracker = delegate(SuspendingPointerInputModifierNode { pointerTracker() })
    private val pointerInput = delegate(SuspendingPointerInputModifierNode { pointerInput() })
    private val velocityTracker = VelocityTracker()
    private var previousEnabled: Boolean = false

    var enabled: () -> Boolean = enabled
    var swipeDetector: SwipeDetector = swipeDetector
        set(value) {
            // Reset the pointer input whenever enabled changed.
            if (value != field) {
                field = value
                pointerInput.resetPointerInputHandler()
@@ -178,21 +167,6 @@ internal class MultiPointerDraggableNode(
            }
        }

    override fun onAttach() {
        previousEnabled = enabled()
        onObservedReadsChanged()
    }

    override fun onObservedReadsChanged() {
        observeReads {
            val newEnabled = enabled()
            if (newEnabled != previousEnabled) {
                pointerInput.resetPointerInputHandler()
            }
            previousEnabled = newEnabled
        }
    }

    override fun onCancelPointerInput() {
        pointerTracker.onCancelPointerInput()
        pointerInput.onCancelPointerInput()
@@ -254,10 +228,8 @@ internal class MultiPointerDraggableNode(
                        velocityTracker.resetTracking()
                        velocityTracker.addPointerInputChange(firstPointerDown)
                        startedPosition = firstPointerDown.position
                        if (enabled()) {
                        onFirstPointerDown()
                    }
                    }

                    // Changes with at least one pointer
                    else -> {
@@ -295,10 +267,6 @@ internal class MultiPointerDraggableNode(
    }

    private suspend fun PointerInputScope.pointerInput() {
        if (!enabled()) {
            return
        }

        val currentContext = currentCoroutineContext()
        awaitPointerEventScope {
            while (currentContext.isActive) {
+50 −35
Original line number Diff line number Diff line
@@ -41,30 +41,69 @@ internal fun Modifier.swipeToScene(
    draggableHandler: DraggableHandlerImpl,
    swipeDetector: SwipeDetector,
): Modifier {
    return this.then(SwipeToSceneElement(draggableHandler, swipeDetector))
    return if (draggableHandler.enabled()) {
        this.then(SwipeToSceneElement(draggableHandler, swipeDetector))
    } else {
        this
    }
}

private fun DraggableHandlerImpl.enabled(): Boolean {
    return isDrivingTransition || contentForSwipes().shouldEnableSwipes(orientation)
}

private fun DraggableHandlerImpl.contentForSwipes(): Content {
    return layoutImpl.contentForUserActions()
}

/** Whether swipe should be enabled in the given [orientation]. */
private fun Content.shouldEnableSwipes(orientation: Orientation): Boolean {
    if (userActions.isEmpty()) {
        return false
    }

    return userActions.keys.any { it is Swipe.Resolved && it.direction.orientation == orientation }
}

private data class SwipeToSceneElement(
    val draggableHandler: DraggableHandlerImpl,
    val swipeDetector: SwipeDetector,
) : ModifierNodeElement<SwipeToSceneNode>() {
    override fun create(): SwipeToSceneNode = SwipeToSceneNode(draggableHandler, swipeDetector)
) : ModifierNodeElement<SwipeToSceneRootNode>() {
    override fun create(): SwipeToSceneRootNode =
        SwipeToSceneRootNode(draggableHandler, swipeDetector)

    override fun update(node: SwipeToSceneNode) {
        node.draggableHandler = draggableHandler
    override fun update(node: SwipeToSceneRootNode) {
        node.update(draggableHandler, swipeDetector)
    }
}

private class SwipeToSceneNode(
private class SwipeToSceneRootNode(
    draggableHandler: DraggableHandlerImpl,
    swipeDetector: SwipeDetector,
) : DelegatingNode() {
    private var delegate = delegate(SwipeToSceneNode(draggableHandler, swipeDetector))

    fun update(draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector) {
        if (draggableHandler == delegate.draggableHandler) {
            // Simple update, just update the swipe detector directly and keep the node.
            delegate.swipeDetector = swipeDetector
        } else {
            // The draggableHandler changed, force recreate the underlying SwipeToSceneNode.
            undelegate(delegate)
            delegate = delegate(SwipeToSceneNode(draggableHandler, swipeDetector))
        }
    }
}

private class SwipeToSceneNode(
    val draggableHandler: DraggableHandlerImpl,
    swipeDetector: SwipeDetector,
) : DelegatingNode(), PointerInputModifierNode {
    private val dispatcher = NestedScrollDispatcher()
    private val multiPointerDraggableNode =
        delegate(
            MultiPointerDraggableNode(
                orientation = draggableHandler.orientation,
                enabled = ::enabled,
                startDragImmediately = ::startDragImmediately,
                onDragStarted = draggableHandler::onDragStarted,
                onFirstPointerDown = ::onFirstPointerDown,
@@ -73,18 +112,10 @@ private class SwipeToSceneNode(
            )
        )

    private var _draggableHandler = draggableHandler
    var draggableHandler: DraggableHandlerImpl
        get() = _draggableHandler
    var swipeDetector: SwipeDetector
        get() = multiPointerDraggableNode.swipeDetector
        set(value) {
            if (_draggableHandler != value) {
                _draggableHandler = value

                // Make sure to update the delegate orientation. Note that this will automatically
                // reset the underlying pointer input handler, so previous gestures will be
                // cancelled.
                multiPointerDraggableNode.orientation = value.orientation
            }
            multiPointerDraggableNode.swipeDetector = value
        }

    private val nestedScrollHandlerImpl =
@@ -124,22 +155,6 @@ private class SwipeToSceneNode(

    override fun onCancelPointerInput() = multiPointerDraggableNode.onCancelPointerInput()

    private fun enabled(): Boolean {
        return draggableHandler.isDrivingTransition ||
            contentForSwipes().shouldEnableSwipes(multiPointerDraggableNode.orientation)
    }

    private fun contentForSwipes(): Content {
        return draggableHandler.layoutImpl.contentForUserActions()
    }

    /** Whether swipe should be enabled in the given [orientation]. */
    private fun Content.shouldEnableSwipes(orientation: Orientation): Boolean {
        return userActions.keys.any {
            it is Swipe.Resolved && it.direction.orientation == orientation
        }
    }

    private fun startDragImmediately(startedPosition: Offset): Boolean {
        // Immediately start the drag if the user can't swipe in the other direction and the gesture
        // handler can intercept it.
@@ -152,7 +167,7 @@ private class SwipeToSceneNode(
                Orientation.Vertical -> Orientation.Horizontal
                Orientation.Horizontal -> Orientation.Vertical
            }
        return contentForSwipes().shouldEnableSwipes(oppositeOrientation)
        return draggableHandler.contentForSwipes().shouldEnableSwipes(oppositeOrientation)
    }
}

+15 −21
Original line number Diff line number Diff line
@@ -45,6 +45,7 @@ import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Velocity
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.compose.modifiers.thenIf
import com.android.compose.nestedscroll.SuspendedValue
import com.google.common.truth.Truth.assertThat
import kotlin.properties.Delegates
@@ -94,9 +95,9 @@ class MultiPointerDraggableTest {
            Box(
                Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() })
                    .nestedScrollDispatcher()
                    .multiPointerDraggable(
                    .thenIf(enabled) {
                        Modifier.multiPointerDraggable(
                            orientation = Orientation.Vertical,
                        enabled = { enabled },
                            startDragImmediately = { false },
                            onDragStarted = { _, _, _ ->
                                started = true
@@ -107,6 +108,7 @@ class MultiPointerDraggableTest {
                            },
                            dispatcher = defaultDispatcher,
                        )
                    }
            )
        }

@@ -164,7 +166,6 @@ class MultiPointerDraggableTest {
                    .nestedScrollDispatcher()
                    .multiPointerDraggable(
                        orientation = Orientation.Vertical,
                        enabled = { true },
                        // We want to start a drag gesture immediately
                        startDragImmediately = { true },
                        onDragStarted = { _, _, _ ->
@@ -238,7 +239,6 @@ class MultiPointerDraggableTest {
                    .nestedScrollDispatcher()
                    .multiPointerDraggable(
                        orientation = Orientation.Vertical,
                        enabled = { true },
                        startDragImmediately = { false },
                        onDragStarted = { _, _, _ ->
                            started = true
@@ -358,7 +358,6 @@ class MultiPointerDraggableTest {
                    .nestedScrollDispatcher()
                    .multiPointerDraggable(
                        orientation = Orientation.Vertical,
                        enabled = { true },
                        startDragImmediately = { false },
                        onDragStarted = { _, _, _ ->
                            started = true
@@ -464,7 +463,6 @@ class MultiPointerDraggableTest {
                    .nestedScrollDispatcher()
                    .multiPointerDraggable(
                        orientation = Orientation.Vertical,
                        enabled = { true },
                        startDragImmediately = { false },
                        onDragStarted = { _, _, _ ->
                            verticalStarted = true
@@ -477,7 +475,6 @@ class MultiPointerDraggableTest {
                    )
                    .multiPointerDraggable(
                        orientation = Orientation.Horizontal,
                        enabled = { true },
                        startDragImmediately = { false },
                        onDragStarted = { _, _, _ ->
                            horizontalStarted = true
@@ -570,7 +567,6 @@ class MultiPointerDraggableTest {
                    .nestedScrollDispatcher()
                    .multiPointerDraggable(
                        orientation = Orientation.Vertical,
                        enabled = { true },
                        startDragImmediately = { false },
                        swipeDetector =
                            object : SwipeDetector {
@@ -672,7 +668,6 @@ class MultiPointerDraggableTest {
                    .nestedScrollDispatcher()
                    .multiPointerDraggable(
                        orientation = Orientation.Vertical,
                        enabled = { true },
                        startDragImmediately = { false },
                        onDragStarted = { _, _, _ ->
                            SimpleDragController(
@@ -744,7 +739,6 @@ class MultiPointerDraggableTest {
                    .nestedScrollDispatcher()
                    .multiPointerDraggable(
                        orientation = Orientation.Vertical,
                        enabled = { true },
                        startDragImmediately = { false },
                        onDragStarted = { _, _, _ ->
                            SimpleDragController(
+67 −0
Original line number Diff line number Diff line
@@ -22,11 +22,15 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
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
@@ -36,9 +40,14 @@ import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.testTag
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.performTouchInput
import androidx.compose.ui.test.swipeRight
import androidx.compose.ui.test.swipeUp
import androidx.compose.ui.test.swipeWithVelocity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
@@ -844,4 +853,62 @@ class SwipeToSceneTest {
        assertThat(transition.progress).isEqualTo(1f)
        assertThat(availableOnPostScroll).isEqualTo(ovescrollPx)
    }

    @Test
    fun sceneWithoutSwipesDoesNotConsumeGestures() {
        val buttonTag = "button"

        rule.setContent {
            Box {
                var count by remember { mutableStateOf(0) }
                Button(onClick = { count++ }, Modifier.testTag(buttonTag).align(Alignment.Center)) {
                    Text("Count: $count")
                }

                SceneTransitionLayout(remember { MutableSceneTransitionLayoutState(SceneA) }) {
                    scene(SceneA) { Box(Modifier.fillMaxSize()) }
                }
            }
        }

        rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 0")

        // Click on the root at its center, where the button is located. Clicks should go through
        // the STL and reach the button given that there is no swipes for the current scene.
        repeat(3) { rule.onRoot().performClick() }
        rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 3")
    }

    @Test
    fun swipeToSceneSupportsUpdates() {
        val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }

        rule.setContent {
            SceneTransitionLayout(state) {
                // SceneA only has vertical actions, so only one vertical Modifier.swipeToScene()
                // is composed.
                scene(SceneA, mapOf(Swipe.Up to SceneB)) { Box(Modifier.fillMaxSize()) }

                // SceneB only has horizontal actions, so only one vertical Modifier.swipeToScene()
                // is composed, which will be force update it with a new draggableHandler.
                scene(SceneB, mapOf(Swipe.Right to SceneC)) { Box(Modifier.fillMaxSize()) }
                scene(SceneC) { Box(Modifier.fillMaxSize()) }
            }
        }

        assertThat(state.transitionState).isIdle()
        assertThat(state.transitionState).hasCurrentScene(SceneA)

        // Swipe up to scene B.
        rule.onRoot().performTouchInput { swipeUp() }
        rule.waitForIdle()
        assertThat(state.transitionState).isIdle()
        assertThat(state.transitionState).hasCurrentScene(SceneB)

        // Swipe right to scene C.
        rule.onRoot().performTouchInput { swipeRight() }
        rule.waitForIdle()
        assertThat(state.transitionState).isIdle()
        assertThat(state.transitionState).hasCurrentScene(SceneC)
    }
}