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

Commit 4e3e9b28 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Consider the previous unique state of an element during interruptions

This CL ensures that we better support interruptions when an element is
going away but then reappears in a new transition. This can often
happen with dual shade if one shade is being closed while we open the
new one.

Bug: 373799480
Test: ElementTest
Flag: com.android.systemui.scene_container
Change-Id: I5e7cb3fe39cca052e913691aee61d49feee85a6f
parent d51c91e9
Loading
Loading
Loading
Loading
+33 −7
Original line number Diff line number Diff line
@@ -691,8 +691,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
@@ -719,12 +719,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 (
@@ -733,13 +753,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
@@ -2641,4 +2641,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)
    }
}