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

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

Merge changes from topic "stl-full-distance-swipe" into main

* changes:
  Do not consider elements with alpha=0f when interrupting
  Add UserActionResult.requiresFullDistanceSwipe
parents fc362ef1 042ca77a
Loading
Loading
Loading
Loading
+12 −3
Original line number Diff line number Diff line
@@ -395,10 +395,11 @@ private class DragControllerImpl(
            if (
                distance != DistanceUnspecified &&
                    shouldCommitSwipe(
                        offset,
                        distance,
                        velocity,
                        offset = offset,
                        distance = distance,
                        velocity = velocity,
                        wasCommitted = swipeTransition._currentScene == toScene,
                        requiresFullDistanceSwipe = swipeTransition.requiresFullDistanceSwipe,
                    )
            ) {
                targetScene = toScene
@@ -472,7 +473,12 @@ private class DragControllerImpl(
        distance: Float,
        velocity: Float,
        wasCommitted: Boolean,
        requiresFullDistanceSwipe: Boolean,
    ): Boolean {
        if (requiresFullDistanceSwipe && !wasCommitted) {
            return offset / distance >= 1f
        }

        fun isCloserToTarget(): Boolean {
            return (offset - distance).absoluteValue < offset.absoluteValue
        }
@@ -530,6 +536,7 @@ private fun SwipeTransition(
        userActionDistanceScope = layoutImpl.userActionDistanceScope,
        orientation = orientation,
        isUpOrLeft = isUpOrLeft,
        requiresFullDistanceSwipe = result.requiresFullDistanceSwipe,
    )
}

@@ -545,6 +552,7 @@ private fun SwipeTransition(old: SwipeTransition): SwipeTransition {
            orientation = old.orientation,
            isUpOrLeft = old.isUpOrLeft,
            lastDistance = old.lastDistance,
            requiresFullDistanceSwipe = old.requiresFullDistanceSwipe,
        )
        .apply {
            _currentScene = old._currentScene
@@ -562,6 +570,7 @@ private class SwipeTransition(
    val userActionDistanceScope: UserActionDistanceScope,
    override val orientation: Orientation,
    override val isUpOrLeft: Boolean,
    val requiresFullDistanceSwipe: Boolean,
    var lastDistance: Float = DistanceUnspecified,
) :
    TransitionState.Transition(_fromScene.key, _toScene.key),
+13 −4
Original line number Diff line number Diff line
@@ -306,7 +306,6 @@ internal class ElementNode(
        return layout(placeable.width, placeable.height) { place(transition, placeable) }
    }

    @OptIn(ExperimentalComposeUiApi::class)
    private fun Placeable.PlacementScope.place(
        transition: TransitionState.Transition?,
        placeable: Placeable,
@@ -561,10 +560,20 @@ private fun reconcileStates(
}

private fun Element.SceneState.selfUpdateValuesBeforeInterruption() {
    offsetBeforeInterruption = lastOffset
    sizeBeforeInterruption = lastSize

    if (lastAlpha > 0f) {
        offsetBeforeInterruption = lastOffset
        scaleBeforeInterruption = lastScale
        alphaBeforeInterruption = lastAlpha
    } else {
        // Consider the element as not placed in this scene if it was fully transparent.
        // TODO(b/290930950): Look into using derived state inside place() instead to not even place
        // the element at all when alpha == 0f.
        offsetBeforeInterruption = Offset.Unspecified
        scaleBeforeInterruption = Scale.Unspecified
        alphaBeforeInterruption = Element.AlphaUnspecified
    }
}

private fun Element.SceneState.updateValuesBeforeInterruption(lastState: Element.SceneState) {
+7 −0
Original line number Diff line number Diff line
@@ -459,6 +459,13 @@ data class UserActionResult(

    /** The key of the transition that should be used. */
    val transitionKey: TransitionKey? = null,

    /**
     * If `true`, the swipe will be committed and we will settle to [toScene] if only if the user
     * swiped at least the swipe distance, i.e. the transition progress was already equal to or
     * bigger than 100% when the user released their finger. `
     */
    val requiresFullDistanceSwipe: Boolean = false,
)

fun interface UserActionDistance {
+18 −0
Original line number Diff line number Diff line
@@ -1215,4 +1215,22 @@ class DraggableHandlerTest {
        onDragStartedImmediately()
        assertTransition(fromScene = SceneA, toScene = SceneB, progress = 50f / 75f)
    }

    @Test
    fun requireFullDistanceSwipe() = runGestureTest {
        mutableUserActionsA[Swipe.Up] = UserActionResult(SceneB, requiresFullDistanceSwipe = true)

        val controller = onDragStarted(overSlop = up(fractionOfScreen = 0.9f))
        assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.9f)

        controller.onDragStopped(velocity = 0f)
        advanceUntilIdle()
        assertIdle(SceneA)

        val otherController = onDragStarted(overSlop = up(fractionOfScreen = 1f))
        assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f)
        otherController.onDragStopped(velocity = 0f)
        advanceUntilIdle()
        assertIdle(SceneB)
    }
}
+83 −5
Original line number Diff line number Diff line
@@ -851,7 +851,8 @@ class ElementTest {
            rule.runOnUiThread {
                MutableSceneTransitionLayoutState(
                    initialScene = SceneA,
                    transitions = transitions {
                    transitions =
                        transitions {
                            from(SceneA, to = SceneB) {
                                translate(TestElements.Foo, y = translateY)
                            }
@@ -2010,4 +2011,81 @@ class ElementTest {
            )
            .isEqualTo(Element.SizeUnspecified)
    }

    @Test
    fun transparentElementIsNotImpactingInterruption() = runTest {
        val state =
            rule.runOnIdle {
                MutableSceneTransitionLayoutStateImpl(
                    SceneA,
                    transitions {
                        from(SceneA, to = SceneB) {
                            // In A => B, Foo is not shared and first fades out from A then fades in
                            // B.
                            sharedElement(TestElements.Foo, enabled = false)
                            fractionRange(end = 0.5f) { fade(TestElements.Foo.inScene(SceneA)) }
                            fractionRange(start = 0.5f) { fade(TestElements.Foo.inScene(SceneB)) }
                        }

                        from(SceneB, to = SceneA) {
                            // In B => A, Foo is shared.
                            sharedElement(TestElements.Foo, enabled = true)
                        }
                    }
                )
            }

        @Composable
        fun SceneScope.Foo(modifier: Modifier = Modifier) {
            Box(modifier.element(TestElements.Foo).size(10.dp))
        }

        rule.setContent {
            SceneTransitionLayout(state) {
                scene(SceneB) { Foo(Modifier.offset(40.dp, 60.dp)) }

                // Define A after B so that Foo is placed in A during A <=> B.
                scene(SceneA) { Foo() }
            }
        }

        // Start A => B at 70%.
        rule.runOnUiThread {
            state.startTransition(
                transition(
                    from = SceneA,
                    to = SceneB,
                    progress = { 0.7f },
                    onFinish = neverFinish(),
                )
            )
        }

        rule.onNode(isElement(TestElements.Foo, SceneA)).assertPositionInRootIsEqualTo(0.dp, 0.dp)
        rule.onNode(isElement(TestElements.Foo, SceneB)).assertPositionInRootIsEqualTo(40.dp, 60.dp)

        // Start B => A at 50% with interruptionProgress = 100%. Foo is placed in A and should still
        // be at (40dp, 60dp) given that it was fully transparent in A before the interruption.
        var interruptionProgress by mutableStateOf(1f)
        rule.runOnUiThread {
            state.startTransition(
                transition(
                    from = SceneB,
                    to = SceneA,
                    progress = { 0.5f },
                    interruptionProgress = { interruptionProgress },
                    onFinish = neverFinish(),
                )
            )
        }

        rule.onNode(isElement(TestElements.Foo, SceneA)).assertPositionInRootIsEqualTo(40.dp, 60.dp)
        rule.onNode(isElement(TestElements.Foo, SceneB)).assertIsNotDisplayed()

        // Set the interruption progress to 0%. Foo should be at (20dp, 30dp) given that B => is at
        // 50%.
        interruptionProgress = 0f
        rule.onNode(isElement(TestElements.Foo, SceneA)).assertPositionInRootIsEqualTo(20.dp, 30.dp)
        rule.onNode(isElement(TestElements.Foo, SceneB)).assertIsNotDisplayed()
    }
}