diff options
6 files changed, 89 insertions, 13 deletions
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index cb1f1af8b5a6..a90081738062 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -2093,3 +2093,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "always_compose_qs_ui_fragment" + namespace: "systemui" + description: "Have QQS and QS scenes in the Compose fragment always composed, not just when it should be visible." + bug: "389985793" + metadata { + purpose: PURPOSE_BUGFIX + } +}
\ No newline at end of file diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index 404f1b217026..22688d310b44 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -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, ) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index e3c4eb0f8bea..4da83c3a6fc9 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -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 { - val visited = mutableSetOf<SceneKey>() - fun maybeAdd(sceneKey: SceneKey) { - if (visited.add(sceneKey)) { - add(scene(sceneKey)) - } + return buildList { + val visited = mutableSetOf<SceneKey>() + fun maybeAdd(sceneKey: SceneKey, isInvisible: Boolean = false) { + if (visited.add(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() diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt index 149a9e7c4705..72ee75ad2d47 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt @@ -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) {} } + } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Scene.kt index 7f57798fb1b3..38acd4be80ae 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Scene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Scene.kt @@ -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)" diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt index fa7661b6d102..6538d4340cf3 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt @@ -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) + } } |