Loading packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +3 −6 Original line number Diff line number Diff line Loading @@ -89,10 +89,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastMaxOf import androidx.compose.ui.util.fastMinOf import androidx.compose.ui.util.lerp import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.currentStateAsState import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey Loading Loading @@ -323,8 +320,7 @@ fun ContentScope.NotificationScrollingStack( supportNestedScrolling: Boolean, onEmptySpaceClick: (() -> Unit)? = null, ) { val lifecycleState by LocalLifecycleOwner.current.lifecycle.currentStateAsState() if (!lifecycleState.isAtLeast(Lifecycle.State.STARTED)) { if (!isAlwaysComposedContentVisible()) { // Some scenes or overlays that use this Composable may be using alwaysCompose=true which // will cause them to compose everything but not be visible. Because this Composable has // many side effects that push UI state upstream to its view-model, interactors, and Loading Loading @@ -676,7 +672,8 @@ fun ContentScope.NotificationScrollingStack( } .verticalScroll(scrollState, overscrollEffect = overScrollEffect) .fillMaxWidth() // Added extra bottom padding for keeping footerView inside parent Viewbounds during overscroll, refer to b/437347340#comment3 // Added extra bottom padding for keeping footerView inside parent // Viewbounds during overscroll, refer to b/437347340#comment3 .padding(bottom = 4.dp) .onGloballyPositioned { coordinates -> stackBoundsOnScreen.value = coordinates.boundsInWindow() Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +19 −0 Original line number Diff line number Diff line Loading @@ -379,6 +379,25 @@ interface ContentScope : BaseContentScope { modifier: Modifier, builder: SceneTransitionLayoutScope<ContentScope>.() -> Unit, ) /** * Whether this content can be considered "visible", i.e. it is either: * - the [current scene][SceneTransitionLayoutState.currentScene] * - one of the [current overlays][SceneTransitionLayoutState.currentOverlays] * - in a transition to become the current scene or one of the current overlays * * Note that this does not actually do any visibility check, a content will be considered * visible even if its alpha is 0, or if it is translated outside the device bounds, or if it is * fully obscured by another content, etc. * * This function takes the ancestor contents from ancestor STLs into account, so that this * returns false if this content OR any ancestor content is not "visible". * * This is meant to be used only by contents that leverage the `alwaysCompose` flag to remain * composed even when not "visible". When `alwaysCompose` is false, you should rely on * composition only as a signal for "visibility". */ fun isAlwaysComposedContentVisible(): Boolean } internal interface InternalContentScope : ContentScope { Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt +19 −0 Original line number Diff line number Diff line Loading @@ -45,6 +45,7 @@ import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.IntSize import androidx.compose.ui.util.fastAny import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner Loading Loading @@ -393,6 +394,24 @@ internal class ContentScopeImpl( implicitTestTags = layoutImpl.implicitTestTags, ) } override fun isAlwaysComposedContentVisible(): Boolean { return isAlwaysComposedContentVisible(layoutImpl.state, contentKey) && layoutImpl.ancestors.all { ancestor -> isAlwaysComposedContentVisible(ancestor.layoutImpl.state, ancestor.inContent) } } private fun isAlwaysComposedContentVisible( layoutState: SceneTransitionLayoutState, content: ContentKey, ): Boolean { return layoutState.currentScene == content || layoutState.currentOverlays.contains(content) || layoutState.currentTransitions.fastAny { it.fromContent == content || it.toContent == content } } } /** A [LifecycleOwner] that follows its [parentLifecycle] but is capped at [maxLifecycleState]. */ Loading packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ContentTest.kt +125 −0 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember Loading @@ -42,7 +43,10 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.compose.animation.scene.TestScenes.SceneA import com.android.compose.animation.scene.TestScenes.SceneB import com.android.compose.animation.scene.TestScenes.SceneC import com.android.compose.test.setContentAndCreateMainScope import com.android.compose.test.transition import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.launch import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith Loading Loading @@ -202,4 +206,125 @@ class ContentTest { assertThat(lifecycleC).isNull() assertThat(lastLifecycleC?.currentState).isEqualTo(Lifecycle.State.DESTROYED) } @Test fun isAlwaysComposedContentVisible() { @Composable fun ContentScope.Visibility(f: (Boolean) -> Unit) { val isVisible = isAlwaysComposedContentVisible() SideEffect { f(isVisible) } } var outerSceneAVisible by mutableStateOf(false) var outerSceneBVisible by mutableStateOf(false) var outerOverlayAVisible by mutableStateOf(false) var innerSceneAVisible by mutableStateOf(false) var innerSceneBVisible by mutableStateOf(false) var innerOverlayAVisible by mutableStateOf(false) val outerSceneA = SceneKey("OuterSceneA") val outerSceneB = SceneKey("OuterSceneB") val outerOverlayA = OverlayKey("OuterOverlayA") val innerSceneA = SceneKey("InnerSceneA") val innerSceneB = SceneKey("InnerSceneB") val innerOverlayA = OverlayKey("InnerOverlayA") val outerState = rule.runOnUiThread { MutableSceneTransitionLayoutStateForTests(outerSceneA) } val innerState = rule.runOnUiThread { MutableSceneTransitionLayoutStateForTests(innerSceneA) } val scope = rule.setContentAndCreateMainScope { SceneTransitionLayout(outerState) { scene(outerSceneA, alwaysCompose = true) { Visibility { outerSceneAVisible = it } NestedSceneTransitionLayout(innerState, Modifier) { scene(innerSceneA, alwaysCompose = true) { Visibility { innerSceneAVisible = it } } scene(innerSceneB, alwaysCompose = true) { Visibility { innerSceneBVisible = it } } overlay(innerOverlayA, alwaysCompose = true) { Visibility { innerOverlayAVisible = it } } } } scene(outerSceneB, alwaysCompose = true) { Visibility { outerSceneBVisible = it } } overlay(outerOverlayA, alwaysCompose = true) { Visibility { outerOverlayAVisible = it } } } } // Initial state. rule.waitForIdle() assertThat(outerSceneAVisible).isTrue() assertThat(innerSceneAVisible).isTrue() assertThat(outerSceneBVisible).isFalse() assertThat(outerOverlayAVisible).isFalse() assertThat(innerSceneBVisible).isFalse() assertThat(innerOverlayAVisible).isFalse() // Transition in inner layout: InnerSceneA -> InnerSceneB. val innerAToB = transition(innerSceneA, innerSceneB) scope.launch { innerState.startTransition(innerAToB) } rule.waitForIdle() assertThat(outerSceneAVisible).isTrue() assertThat(innerSceneAVisible).isTrue() assertThat(innerSceneBVisible).isTrue() // Finish transition. innerAToB.finish() rule.waitForIdle() assertThat(innerSceneAVisible).isFalse() assertThat(innerSceneBVisible).isTrue() // Transition to show inner overlay. val showInnerOverlay = transition(innerState.currentScene, innerOverlayA) scope.launch { innerState.startTransition(showInnerOverlay) } rule.waitForIdle() assertThat(innerSceneBVisible).isTrue() assertThat(innerOverlayAVisible).isTrue() // Finish transition. showInnerOverlay.finish() rule.waitForIdle() assertThat(innerSceneBVisible).isTrue() assertThat(innerOverlayAVisible).isTrue() // Transition in outer layout: OuterSceneA -> OuterSceneB. val outerAToB = transition(outerSceneA, outerSceneB) scope.launch { outerState.startTransition(outerAToB) } rule.waitForIdle() assertThat(outerSceneAVisible).isTrue() assertThat(outerSceneBVisible).isTrue() assertThat(innerSceneBVisible).isTrue() assertThat(innerOverlayAVisible).isTrue() // Finish transition. outerAToB.finish() rule.waitForIdle() assertThat(outerSceneAVisible).isFalse() assertThat(outerSceneBVisible).isTrue() assertThat(innerSceneBVisible).isFalse() assertThat(innerOverlayAVisible).isFalse() // Transition to show outer overlay. val showOuterOverlay = transition(outerState.currentScene, outerOverlayA) scope.launch { outerState.startTransition(showOuterOverlay) } rule.waitForIdle() assertThat(outerSceneBVisible).isTrue() assertThat(outerOverlayAVisible).isTrue() // Finish transition. showOuterOverlay.finish() rule.waitForIdle() assertThat(outerSceneBVisible).isTrue() assertThat(outerOverlayAVisible).isTrue() } } Loading
packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +3 −6 Original line number Diff line number Diff line Loading @@ -89,10 +89,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastMaxOf import androidx.compose.ui.util.fastMinOf import androidx.compose.ui.util.lerp import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.currentStateAsState import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey Loading Loading @@ -323,8 +320,7 @@ fun ContentScope.NotificationScrollingStack( supportNestedScrolling: Boolean, onEmptySpaceClick: (() -> Unit)? = null, ) { val lifecycleState by LocalLifecycleOwner.current.lifecycle.currentStateAsState() if (!lifecycleState.isAtLeast(Lifecycle.State.STARTED)) { if (!isAlwaysComposedContentVisible()) { // Some scenes or overlays that use this Composable may be using alwaysCompose=true which // will cause them to compose everything but not be visible. Because this Composable has // many side effects that push UI state upstream to its view-model, interactors, and Loading Loading @@ -676,7 +672,8 @@ fun ContentScope.NotificationScrollingStack( } .verticalScroll(scrollState, overscrollEffect = overScrollEffect) .fillMaxWidth() // Added extra bottom padding for keeping footerView inside parent Viewbounds during overscroll, refer to b/437347340#comment3 // Added extra bottom padding for keeping footerView inside parent // Viewbounds during overscroll, refer to b/437347340#comment3 .padding(bottom = 4.dp) .onGloballyPositioned { coordinates -> stackBoundsOnScreen.value = coordinates.boundsInWindow() Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +19 −0 Original line number Diff line number Diff line Loading @@ -379,6 +379,25 @@ interface ContentScope : BaseContentScope { modifier: Modifier, builder: SceneTransitionLayoutScope<ContentScope>.() -> Unit, ) /** * Whether this content can be considered "visible", i.e. it is either: * - the [current scene][SceneTransitionLayoutState.currentScene] * - one of the [current overlays][SceneTransitionLayoutState.currentOverlays] * - in a transition to become the current scene or one of the current overlays * * Note that this does not actually do any visibility check, a content will be considered * visible even if its alpha is 0, or if it is translated outside the device bounds, or if it is * fully obscured by another content, etc. * * This function takes the ancestor contents from ancestor STLs into account, so that this * returns false if this content OR any ancestor content is not "visible". * * This is meant to be used only by contents that leverage the `alwaysCompose` flag to remain * composed even when not "visible". When `alwaysCompose` is false, you should rely on * composition only as a signal for "visibility". */ fun isAlwaysComposedContentVisible(): Boolean } internal interface InternalContentScope : ContentScope { Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt +19 −0 Original line number Diff line number Diff line Loading @@ -45,6 +45,7 @@ import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.IntSize import androidx.compose.ui.util.fastAny import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner Loading Loading @@ -393,6 +394,24 @@ internal class ContentScopeImpl( implicitTestTags = layoutImpl.implicitTestTags, ) } override fun isAlwaysComposedContentVisible(): Boolean { return isAlwaysComposedContentVisible(layoutImpl.state, contentKey) && layoutImpl.ancestors.all { ancestor -> isAlwaysComposedContentVisible(ancestor.layoutImpl.state, ancestor.inContent) } } private fun isAlwaysComposedContentVisible( layoutState: SceneTransitionLayoutState, content: ContentKey, ): Boolean { return layoutState.currentScene == content || layoutState.currentOverlays.contains(content) || layoutState.currentTransitions.fastAny { it.fromContent == content || it.toContent == content } } } /** A [LifecycleOwner] that follows its [parentLifecycle] but is capped at [maxLifecycleState]. */ Loading
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ContentTest.kt +125 −0 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember Loading @@ -42,7 +43,10 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.compose.animation.scene.TestScenes.SceneA import com.android.compose.animation.scene.TestScenes.SceneB import com.android.compose.animation.scene.TestScenes.SceneC import com.android.compose.test.setContentAndCreateMainScope import com.android.compose.test.transition import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.launch import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith Loading Loading @@ -202,4 +206,125 @@ class ContentTest { assertThat(lifecycleC).isNull() assertThat(lastLifecycleC?.currentState).isEqualTo(Lifecycle.State.DESTROYED) } @Test fun isAlwaysComposedContentVisible() { @Composable fun ContentScope.Visibility(f: (Boolean) -> Unit) { val isVisible = isAlwaysComposedContentVisible() SideEffect { f(isVisible) } } var outerSceneAVisible by mutableStateOf(false) var outerSceneBVisible by mutableStateOf(false) var outerOverlayAVisible by mutableStateOf(false) var innerSceneAVisible by mutableStateOf(false) var innerSceneBVisible by mutableStateOf(false) var innerOverlayAVisible by mutableStateOf(false) val outerSceneA = SceneKey("OuterSceneA") val outerSceneB = SceneKey("OuterSceneB") val outerOverlayA = OverlayKey("OuterOverlayA") val innerSceneA = SceneKey("InnerSceneA") val innerSceneB = SceneKey("InnerSceneB") val innerOverlayA = OverlayKey("InnerOverlayA") val outerState = rule.runOnUiThread { MutableSceneTransitionLayoutStateForTests(outerSceneA) } val innerState = rule.runOnUiThread { MutableSceneTransitionLayoutStateForTests(innerSceneA) } val scope = rule.setContentAndCreateMainScope { SceneTransitionLayout(outerState) { scene(outerSceneA, alwaysCompose = true) { Visibility { outerSceneAVisible = it } NestedSceneTransitionLayout(innerState, Modifier) { scene(innerSceneA, alwaysCompose = true) { Visibility { innerSceneAVisible = it } } scene(innerSceneB, alwaysCompose = true) { Visibility { innerSceneBVisible = it } } overlay(innerOverlayA, alwaysCompose = true) { Visibility { innerOverlayAVisible = it } } } } scene(outerSceneB, alwaysCompose = true) { Visibility { outerSceneBVisible = it } } overlay(outerOverlayA, alwaysCompose = true) { Visibility { outerOverlayAVisible = it } } } } // Initial state. rule.waitForIdle() assertThat(outerSceneAVisible).isTrue() assertThat(innerSceneAVisible).isTrue() assertThat(outerSceneBVisible).isFalse() assertThat(outerOverlayAVisible).isFalse() assertThat(innerSceneBVisible).isFalse() assertThat(innerOverlayAVisible).isFalse() // Transition in inner layout: InnerSceneA -> InnerSceneB. val innerAToB = transition(innerSceneA, innerSceneB) scope.launch { innerState.startTransition(innerAToB) } rule.waitForIdle() assertThat(outerSceneAVisible).isTrue() assertThat(innerSceneAVisible).isTrue() assertThat(innerSceneBVisible).isTrue() // Finish transition. innerAToB.finish() rule.waitForIdle() assertThat(innerSceneAVisible).isFalse() assertThat(innerSceneBVisible).isTrue() // Transition to show inner overlay. val showInnerOverlay = transition(innerState.currentScene, innerOverlayA) scope.launch { innerState.startTransition(showInnerOverlay) } rule.waitForIdle() assertThat(innerSceneBVisible).isTrue() assertThat(innerOverlayAVisible).isTrue() // Finish transition. showInnerOverlay.finish() rule.waitForIdle() assertThat(innerSceneBVisible).isTrue() assertThat(innerOverlayAVisible).isTrue() // Transition in outer layout: OuterSceneA -> OuterSceneB. val outerAToB = transition(outerSceneA, outerSceneB) scope.launch { outerState.startTransition(outerAToB) } rule.waitForIdle() assertThat(outerSceneAVisible).isTrue() assertThat(outerSceneBVisible).isTrue() assertThat(innerSceneBVisible).isTrue() assertThat(innerOverlayAVisible).isTrue() // Finish transition. outerAToB.finish() rule.waitForIdle() assertThat(outerSceneAVisible).isFalse() assertThat(outerSceneBVisible).isTrue() assertThat(innerSceneBVisible).isFalse() assertThat(innerOverlayAVisible).isFalse() // Transition to show outer overlay. val showOuterOverlay = transition(outerState.currentScene, outerOverlayA) scope.launch { outerState.startTransition(showOuterOverlay) } rule.waitForIdle() assertThat(outerSceneBVisible).isTrue() assertThat(outerOverlayAVisible).isTrue() // Finish transition. showOuterOverlay.finish() rule.waitForIdle() assertThat(outerSceneBVisible).isTrue() assertThat(outerOverlayAVisible).isTrue() } }