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

Commit a3907fda authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Consider the previous unique state of an element during interruptions" into main

parents 0b3c835d 4e3e9b28
Loading
Loading
Loading
Loading
+33 −7
Original line number Diff line number Diff line
@@ -693,8 +693,8 @@ private fun prepareInterruption(
    val fromState = updateStateInContent(transition.fromContent)
    val toState = updateStateInContent(transition.toContent)

    reconcileStates(element, previousTransition)
    reconcileStates(element, transition)
    val previousUniqueState = reconcileStates(element, previousTransition, previousState = null)
    reconcileStates(element, transition, previousState = previousUniqueState)

    // Remove the interruption values to all contents but the content(s) where the element will be
    // placed, to make sure that interruption deltas are computed only right after this interruption
@@ -721,12 +721,32 @@ private fun prepareInterruption(
/**
 * Reconcile the state of [element] in the formContent and toContent of [transition] so that the
 * values before interruption have their expected values, taking shared transitions into account.
 *
 * @return the unique state this element had during [transition], `null` if it had multiple
 *   different states (i.e. the shared animation was disabled).
 */
private fun reconcileStates(element: Element, transition: TransitionState.Transition) {
    val fromContentState = element.stateByContent[transition.fromContent] ?: return
    val toContentState = element.stateByContent[transition.toContent] ?: return
private fun reconcileStates(
    element: Element,
    transition: TransitionState.Transition,
    previousState: Element.State?,
): Element.State? {
    fun reconcileWithPreviousState(state: Element.State) {
        if (previousState != null && state.offsetBeforeInterruption == Offset.Unspecified) {
            state.updateValuesBeforeInterruption(previousState)
        }
    }

    val fromContentState = element.stateByContent[transition.fromContent]
    val toContentState = element.stateByContent[transition.toContent]

    if (fromContentState == null || toContentState == null) {
        return (fromContentState ?: toContentState)
            ?.also { reconcileWithPreviousState(it) }
            ?.takeIf { it.offsetBeforeInterruption != Offset.Unspecified }
    }

    if (!isSharedElementEnabled(element.key, transition)) {
        return
        return null
    }

    if (
@@ -735,13 +755,19 @@ private fun reconcileStates(element: Element, transition: TransitionState.Transi
    ) {
        // Element is shared and placed in fromContent only.
        toContentState.updateValuesBeforeInterruption(fromContentState)
    } else if (
        return fromContentState
    }

    if (
        toContentState.offsetBeforeInterruption != Offset.Unspecified &&
            fromContentState.offsetBeforeInterruption == Offset.Unspecified
    ) {
        // Element is shared and placed in toContent only.
        fromContentState.updateValuesBeforeInterruption(toContentState)
        return toContentState
    }

    return null
}

private fun Element.State.selfUpdateValuesBeforeInterruption() {
+54 −0
Original line number Diff line number Diff line
@@ -2638,4 +2638,58 @@ class ElementTest {
            assertWithMessage("Frame $i didn't replace Foo").that(numberOfPlacements).isEqualTo(0)
        }
    }

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

        val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
        val scope =
            rule.setContentAndCreateMainScope {
                SceneTransitionLayout(state) {
                    scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
                    scene(SceneB) { Box(Modifier.fillMaxSize()) }
                    scene(SceneC) {
                        Box(Modifier.fillMaxSize()) { Foo(Modifier.offset(x = 100.dp, y = 100.dp)) }
                    }
                }
            }

        // During A => B, Foo disappears and stays in its original position.
        scope.launch { state.startTransition(transition(SceneA, SceneB)) }
        rule
            .onNode(isElement(TestElements.Foo))
            .assertSizeIsEqualTo(50.dp)
            .assertPositionInRootIsEqualTo(0.dp, 0.dp)

        // Interrupt A => B by B => C.
        var interruptionProgress by mutableFloatStateOf(1f)
        scope.launch {
            state.startTransition(
                transition(SceneB, SceneC, interruptionProgress = { interruptionProgress })
            )
        }

        // During B => C, Foo appears again. It is still at (0, 0) when the interruption progress is
        // 100%, and converges to its position (100, 100) in C.
        rule
            .onNode(isElement(TestElements.Foo))
            .assertSizeIsEqualTo(50.dp)
            .assertPositionInRootIsEqualTo(0.dp, 0.dp)

        interruptionProgress = 0.5f
        rule
            .onNode(isElement(TestElements.Foo))
            .assertSizeIsEqualTo(50.dp)
            .assertPositionInRootIsEqualTo(50.dp, 50.dp)

        interruptionProgress = 0f
        rule
            .onNode(isElement(TestElements.Foo))
            .assertSizeIsEqualTo(50.dp)
            .assertPositionInRootIsEqualTo(100.dp, 100.dp)
    }
}