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

Commit d9bcdd72 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere Committed by Fabián Kozynski
Browse files

Add scene.alwaysCompose flag to STL scenes

This CL adds an alwaysCompose flag that can be added to any STL scene so
that this scene is always composed. Instead of being removed from
composition when they leave the current STL state/transition, these
scenes are made invisible by not placing them. That way they are not
drawn and don't participate in gesture handling, but they are still
composed and measured. This can help reduce the time it takes to
transition to a scene, and will be used for the Compose QS in
particular. See b/389985793#comment109 for details.

Important: This is not meant to be used outside of Compose QS, and
should not be used by any Flexiglass scene.

Bug: 389985793
Bug: 399825091
Test: atest SceneTransitionLayoutTest
Flag: com.android.systemui.qs_ui_refactor_compose_fragment
Change-Id: I17462c64fc2ae5e70963ec4b1129ddb03930d287
parent d280057b
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -102,6 +102,7 @@ interface SceneTransitionLayoutScope<out CS : ContentScope> {
        key: SceneKey,
        userActions: Map<UserAction, UserActionResult> = emptyMap(),
        effectFactory: OverscrollFactory? = null,
        alwaysCompose: Boolean = false,
        content: @Composable CS.() -> Unit,
    )

+35 −12
Original line number Diff line number Diff line
@@ -207,6 +207,9 @@ internal class SceneTransitionLayoutImpl(
    private val nestedScrollDispatcher = NestedScrollDispatcher()
    private val nestedScrollConnection = object : NestedScrollConnection {}

    // TODO(b/399825091): Remove this.
    private var scenesToAlwaysCompose: MutableList<Scene>? = null

    init {
        updateContents(builder, layoutDirection, defaultEffectFactory)

@@ -312,6 +315,7 @@ internal class SceneTransitionLayoutImpl(
                    key: SceneKey,
                    userActions: Map<UserAction, UserActionResult>,
                    effectFactory: OverscrollFactory?,
                    alwaysCompose: Boolean,
                    content: @Composable InternalContentScope.() -> Unit,
                ) {
                    require(!overlaysDefined) { "all scenes must be defined before overlays" }
@@ -324,6 +328,10 @@ internal class SceneTransitionLayoutImpl(
                        Content.calculateGlobalZIndex(parentZIndex, ++zIndex, ancestors.size)
                    val factory = effectFactory ?: defaultEffectFactory
                    if (scene != null) {
                        check(alwaysCompose == scene.alwaysCompose) {
                            "scene.alwaysCompose can not change"
                        }

                        // Update an existing scene.
                        scene.content = content
                        scene.userActions = resolvedUserActions
@@ -332,7 +340,7 @@ internal class SceneTransitionLayoutImpl(
                        scene.maybeUpdateEffects(factory)
                    } else {
                        // New scene.
                        scenes[key] =
                        val scene =
                            Scene(
                                key,
                                this@SceneTransitionLayoutImpl,
@@ -341,7 +349,16 @@ internal class SceneTransitionLayoutImpl(
                                zIndex.toFloat(),
                                globalZIndex,
                                factory,
                                alwaysCompose,
                            )

                        scenes[key] = scene

                        if (alwaysCompose) {
                            (scenesToAlwaysCompose
                                    ?: mutableListOf<Scene>().also { scenesToAlwaysCompose = it })
                                .add(scene)
                        }
                    }
                }

@@ -470,22 +487,24 @@ internal class SceneTransitionLayoutImpl(

    @Composable
    private fun Scenes() {
        scenesToCompose().fastForEach { scene -> key(scene.key) { scene.Content() } }
        scenesToCompose().fastForEach { (scene, isInvisible) ->
            key(scene.key) { scene.Content(isInvisible = isInvisible) }
        }
    }

    private fun scenesToCompose(): List<Scene> {
    private fun scenesToCompose(): List<SceneToCompose> {
        val transitions = state.currentTransitions
        return if (transitions.isEmpty()) {
            listOf(scene(state.transitionState.currentScene))
        } else {
            buildList {
        return buildList {
            val visited = mutableSetOf<SceneKey>()
                fun maybeAdd(sceneKey: SceneKey) {
            fun maybeAdd(sceneKey: SceneKey, isInvisible: Boolean = false) {
                if (visited.add(sceneKey)) {
                        add(scene(sceneKey))
                    add(SceneToCompose(scene(sceneKey), isInvisible))
                }
            }

            if (transitions.isEmpty()) {
                maybeAdd(state.transitionState.currentScene)
            } else {
                // Compose the new scene we are going to first.
                transitions.fastForEachReversed { transition ->
                    when (transition) {
@@ -504,9 +523,13 @@ internal class SceneTransitionLayoutImpl(
                // Make sure that the current scene is always composed.
                maybeAdd(transitions.last().currentScene)
            }

            scenesToAlwaysCompose?.fastForEach { maybeAdd(it.key, isInvisible = true) }
        }
    }

    private data class SceneToCompose(val scene: Scene, val isInvisible: Boolean)

    @Composable
    private fun BoxScope.Overlays() {
        val overlaysOrderedByZIndex = overlaysToComposeOrderedByZIndex()
+8 −1
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.approachLayout
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.zIndex
@@ -154,11 +155,12 @@ internal sealed class Content(

    @SuppressLint("NotConstructor")
    @Composable
    fun Content(modifier: Modifier = Modifier) {
    fun Content(modifier: Modifier = Modifier, isInvisible: Boolean = false) {
        // If this content has a custom factory, provide it to the content so that the factory is
        // automatically used when calling rememberOverscrollEffect().
        Box(
            modifier
                .thenIf(isInvisible) { InvisibleModifier }
                .zIndex(zIndex)
                .approachLayout(
                    isMeasurementApproachInProgress = { layoutImpl.state.isTransitioning() }
@@ -305,3 +307,8 @@ internal class ContentScopeImpl(
        )
    }
}

private val InvisibleModifier =
    Modifier.layout { measurable, constraints ->
        measurable.measure(constraints).run { layout(width, height) {} }
    }
+1 −0
Original line number Diff line number Diff line
@@ -35,6 +35,7 @@ internal class Scene(
    zIndex: Float,
    globalZIndex: Long,
    effectFactory: OverscrollFactory,
    val alwaysCompose: Boolean,
) : Content(key, layoutImpl, content, actions, zIndex, globalZIndex, effectFactory) {
    override fun toString(): String {
        return "Scene(key=$key)"
+34 −0
Original line number Diff line number Diff line
@@ -45,6 +45,7 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.assertHeightIsEqualTo
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.assertPositionInRootIsEqualTo
import androidx.compose.ui.test.assertWidthIsEqualTo
import androidx.compose.ui.test.junit4.createComposeRule
@@ -64,10 +65,13 @@ import com.android.compose.animation.scene.TestScenes.SceneB
import com.android.compose.animation.scene.TestScenes.SceneC
import com.android.compose.animation.scene.subjects.assertThat
import com.android.compose.test.assertSizeIsEqualTo
import com.android.compose.test.setContentAndCreateMainScope
import com.android.compose.test.subjects.DpOffsetSubject
import com.android.compose.test.subjects.assertThat
import com.android.compose.test.transition
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.junit.Assert.assertThrows
import org.junit.Rule
import org.junit.Test
@@ -582,4 +586,34 @@ class SceneTransitionLayoutTest {
        assertThat((state2 as MutableSceneTransitionLayoutStateImpl).motionScheme)
            .isEqualTo(motionScheme2)
    }

    @Test
    fun alwaysCompose() {
        val state = rule.runOnUiThread { MutableSceneTransitionLayoutStateForTests(SceneA) }
        val scope =
            rule.setContentAndCreateMainScope {
                SceneTransitionLayoutForTesting(state) {
                    scene(SceneA) { Box(Modifier.element(TestElements.Foo).size(20.dp)) }
                    scene(SceneB, alwaysCompose = true) {
                        Box(Modifier.element(TestElements.Bar).size(40.dp))
                    }
                }
            }

        // Idle(A): Foo is displayed and Bar exists given that SceneB is always composed but it is
        // not displayed.
        rule.onNode(isElement(TestElements.Foo)).assertIsDisplayed().assertSizeIsEqualTo(20.dp)
        rule.onNode(isElement(TestElements.Bar)).assertExists().assertIsNotDisplayed()

        // Transition(A => B): Foo and Bar are both displayed
        val aToB = transition(SceneA, SceneB)
        scope.launch { state.startTransition(aToB) }
        rule.onNode(isElement(TestElements.Foo)).assertIsDisplayed().assertSizeIsEqualTo(20.dp)
        rule.onNode(isElement(TestElements.Bar)).assertIsDisplayed().assertSizeIsEqualTo(40.dp)

        // Idle(B): Foo does not exist and Bar is displayed.
        aToB.finish()
        rule.onNode(isElement(TestElements.Foo)).assertDoesNotExist()
        rule.onNode(isElement(TestElements.Bar)).assertIsDisplayed().assertSizeIsEqualTo(40.dp)
    }
}