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

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

Merge changes Idfc827a8,Ia53940b3 into main

* changes:
  Move Content.findActionResultBestMatch()
  Always compose both swipeToScene() in STLImpl
parents 73c6a87f 24511345
Loading
Loading
Loading
Loading
+52 −0
Original line number Diff line number Diff line
@@ -428,6 +428,58 @@ internal class Swipes(val upOrLeft: Swipe.Resolved, val downOrRight: Swipe.Resol
        return upOrLeftResult to downOrRightResult
    }

    /**
     * Finds the best matching [UserActionResult] for the given [swipe] within this [Content].
     * Prioritizes actions with matching [Swipe.Resolved.fromSource].
     *
     * @param swipe The swipe to match against.
     * @return The best matching [UserActionResult], or `null` if no match is found.
     */
    private fun Content.findActionResultBestMatch(swipe: Swipe.Resolved): UserActionResult? {
        if (!areSwipesAllowed()) {
            return null
        }

        var bestPoints = Int.MIN_VALUE
        var bestMatch: UserActionResult? = null
        userActions.forEach { (actionSwipe, actionResult) ->
            if (
                actionSwipe !is Swipe.Resolved ||
                    // The direction must match.
                    actionSwipe.direction != swipe.direction ||
                    // The number of pointers down must match.
                    actionSwipe.pointerCount != swipe.pointerCount ||
                    // The action requires a specific fromSource.
                    (actionSwipe.fromSource != null &&
                        actionSwipe.fromSource != swipe.fromSource) ||
                    // The action requires a specific pointerType.
                    (actionSwipe.pointersType != null &&
                        actionSwipe.pointersType != swipe.pointersType)
            ) {
                // This action is not eligible.
                return@forEach
            }

            val sameFromSource = actionSwipe.fromSource == swipe.fromSource
            val samePointerType = actionSwipe.pointersType == swipe.pointersType
            // Prioritize actions with a perfect match.
            if (sameFromSource && samePointerType) {
                return actionResult
            }

            var points = 0
            if (sameFromSource) points++
            if (samePointerType) points++

            // Otherwise, keep track of the best eligible action.
            if (points > bestPoints) {
                bestPoints = points
                bestMatch = actionResult
            }
        }
        return bestMatch
    }

    /**
     * Update the swipes results.
     *
+37 −63
Original line number Diff line number Diff line
@@ -37,11 +37,7 @@ internal fun Modifier.swipeToScene(
    draggableHandler: DraggableHandlerImpl,
    swipeDetector: SwipeDetector,
): Modifier {
    return if (draggableHandler.enabled()) {
        this.then(SwipeToSceneElement(draggableHandler, swipeDetector))
    } else {
        this
    }
    return then(SwipeToSceneElement(draggableHandler, swipeDetector, draggableHandler.enabled()))
}

private fun DraggableHandlerImpl.enabled(): Boolean {
@@ -61,83 +57,61 @@ internal fun Content.shouldEnableSwipes(orientation: Orientation): Boolean {
    return userActions.keys.any { it is Swipe.Resolved && it.direction.orientation == orientation }
}

/**
 * Finds the best matching [UserActionResult] for the given [swipe] within this [Content].
 * Prioritizes actions with matching [Swipe.Resolved.fromSource].
 *
 * @param swipe The swipe to match against.
 * @return The best matching [UserActionResult], or `null` if no match is found.
 */
internal fun Content.findActionResultBestMatch(swipe: Swipe.Resolved): UserActionResult? {
    if (!areSwipesAllowed()) {
        return null
    }

    var bestPoints = Int.MIN_VALUE
    var bestMatch: UserActionResult? = null
    userActions.forEach { (actionSwipe, actionResult) ->
        if (
            actionSwipe !is Swipe.Resolved ||
                // The direction must match.
                actionSwipe.direction != swipe.direction ||
                // The number of pointers down must match.
                actionSwipe.pointerCount != swipe.pointerCount ||
                // The action requires a specific fromSource.
                (actionSwipe.fromSource != null && actionSwipe.fromSource != swipe.fromSource) ||
                // The action requires a specific pointerType.
                (actionSwipe.pointersType != null && actionSwipe.pointersType != swipe.pointersType)
        ) {
            // This action is not eligible.
            return@forEach
        }

        val sameFromSource = actionSwipe.fromSource == swipe.fromSource
        val samePointerType = actionSwipe.pointersType == swipe.pointersType
        // Prioritize actions with a perfect match.
        if (sameFromSource && samePointerType) {
            return actionResult
        }

        var points = 0
        if (sameFromSource) points++
        if (samePointerType) points++

        // Otherwise, keep track of the best eligible action.
        if (points > bestPoints) {
            bestPoints = points
            bestMatch = actionResult
        }
    }
    return bestMatch
}

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

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

private class SwipeToSceneRootNode(
    draggableHandler: DraggableHandlerImpl,
    swipeDetector: SwipeDetector,
    enabled: Boolean,
) : DelegatingNode() {
    private var delegateNode = delegate(SwipeToSceneNode(draggableHandler, swipeDetector))
    private var delegateNode = if (enabled) create(draggableHandler, swipeDetector) else null

    fun update(
        draggableHandler: DraggableHandlerImpl,
        swipeDetector: SwipeDetector,
        enabled: Boolean,
    ) {
        // Disabled.
        if (!enabled) {
            delegateNode?.let { undelegate(it) }
            delegateNode = null
            return
        }

    fun update(draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector) {
        if (draggableHandler == delegateNode.draggableHandler) {
        // Disabled => Enabled.
        val nullableDelegate = delegateNode
        if (nullableDelegate == null) {
            delegateNode = create(draggableHandler, swipeDetector)
            return
        }

        // Enabled => Enabled (update).
        if (draggableHandler == nullableDelegate.draggableHandler) {
            // Simple update, just update the swipe detector directly and keep the node.
            delegateNode.swipeDetector = swipeDetector
            nullableDelegate.swipeDetector = swipeDetector
        } else {
            // The draggableHandler changed, force recreate the underlying SwipeToSceneNode.
            undelegate(delegateNode)
            delegateNode = delegate(SwipeToSceneNode(draggableHandler, swipeDetector))
            undelegate(nullableDelegate)
            delegateNode = create(draggableHandler, swipeDetector)
        }
    }

    private fun create(
        draggableHandler: DraggableHandlerImpl,
        swipeDetector: SwipeDetector,
    ): SwipeToSceneNode {
        return delegate(SwipeToSceneNode(draggableHandler, swipeDetector))
    }
}

+41 −0
Original line number Diff line number Diff line
@@ -936,4 +936,45 @@ class SwipeToSceneTest {
        assertThat(state.transitionState).isIdle()
        assertThat(state.transitionState).hasCurrentScene(SceneC)
    }

    @Test
    fun swipeToSceneNodeIsKeptWhenDisabled() {
        var hasHorizontalActions by mutableStateOf(false)
        val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
        var touchSlop = 0f
        rule.setContent {
            touchSlop = LocalViewConfiguration.current.touchSlop
            SceneTransitionLayout(state) {
                scene(
                    SceneA,
                    userActions =
                        buildList {
                                add(Swipe.Down to SceneB)

                                if (hasHorizontalActions) {
                                    add(Swipe.Left to SceneC)
                                }
                            }
                            .toMap(),
                ) {
                    Box(Modifier.fillMaxSize())
                }
                scene(SceneB) { Box(Modifier.fillMaxSize()) }
            }
        }

        // Swipe down to start a transition to B.
        rule.onRoot().performTouchInput {
            down(middle)
            moveBy(Offset(0f, touchSlop))
        }

        assertThat(state.transitionState).isSceneTransition()

        // Add new horizontal user actions. This should not stop the current transition, even if a
        // new horizontal Modifier.swipeToScene() handler is introduced where the vertical one was.
        hasHorizontalActions = true
        rule.waitForIdle()
        assertThat(state.transitionState).isSceneTransition()
    }
}