Known internally as “Flexiglass”, this framework defines a graph where each node is a “scene” and each edge between the scenes is a transition. The scenes are the main components of System UI, on phones these are: the lockscreen, bouncer, shade, and quick settings panels/views/screens). Each scene is a standalone experience.
The main goal of the framework is to increase code health by applying Separation of concerns over several dimensions:
In addition to the above, some of the secondary goals are: 4. Make customization easier: by separating scenes to standalone pieces, it becomes possible for variant owners and OEMs to exclude or replace certain scenes or to add brand-new scenes. 5. Enable modularization: by separating scenes to standalone pieces, it becomes possible to break down System UI into smaller codebases, each one of which could be built on its own. Note: this isn't part of the scene framework itself but is something that can be done more easily once the scene framework is in place.
@Composable function) that sets up all the scenes, their transitions, etc. To learn more, please see this section.As of the end of 2023, the scene framework is under development; as such, it is disabled by default. For those who are interested in a preview, please follow the instructions below to turn it on.
NOTE: in case these instructions become stale and don't actually enable the framework, please make sure SceneContainerFlag.isEnabled in the SceneContainerFlags.kt file evalutes to true.
Set SCENE_CONTAINER_ENABLED to true in the Flags.kt file
Set the migrate_keyguard_status_bar_view classic flag to true by running: console $ adb shell statusbar cmd migrate_keyguard_status_bar_view true
Set a collection of aconfig flags to true by running the following commands: console $ adb shell device_config put systemui com.android.systemui.scene_container true $ adb shell device_config put systemui com.android.systemui.keyguard_bottom_area_refactor true $ adb shell device_config put systemui com.android.systemui.keyguard_shade_migration_nssl true $ adb shell device_config put systemui com.android.systemui.media_in_scene_container true
Restart System UI by issuing the following command: console $ adb shell am crash com.android.systemui
Verify that the scene framework was turned on. There are two ways to do this:
(a) look for the sash/ribbon UI at the bottom-right corner of the display:
NOTE: this will be removed proper to the actual release of the framework.
(b) Turn on logging and look for the logging statements in logcat:
# Turn on logging from the framework: $ adb shell cmd statusbar echo -b SceneFramework:verbose
$ adb logcat -v time SceneFramework:* *:S ```
To disable the framework, simply turn off the main aconfig flag: console $ adb shell device_config put systemui com.android.systemui.scene_container false
Each scene is defined as an implementation of the ComposableScene interface, which has three parts: 1. The key property returns the SceneKey that uniquely identifies that scene 2. The destinationScenes Flow returns the (potentially ever-changing) set of navigation edges to other scenes, based on user-actions, which is how the navigation graph is defined (see the Scene navigation section for more) 3. The Content function which uses Jetpack Compose to declare of the UI itself. This is the UI “at rest”, e.g. once there is no transition between any two scenes. The Scene Framework has other ways to define how the content of your UI changes with and throughout a transition to learn more please see the Scene transition animations section
For example: ```kotlin @SysUISingleton class YourScene @Inject constructor( // your dependencies here ) : ComposableScene { override val key = SceneKey.YourScene
override val destinationScenes: StateFlow<Map<UserAction, SceneModel>> =
MutableStateFlow<Map<UserAction, SceneModel>>(
mapOf(
// This is where scene navigation is defined, more on that below.
)
).asStateFlow()
@Composable
override fun SceneScope.Content(
modifier: Modifier,
) {
// This is where the UI is defined using Jetpack Compose.
}
} ```
Scenes are injected into the Dagger dependency graph from the SceneModule.
As seen above, each scene is responsible for providing an observable Flow of a Map that connects UserAction (for example: swipe down, swipe up, back button/gesture, etc.) keys to SceneModel destinations. This is how the scene navigation graph is defined.
NOTE: this controls only user-input based navigation. To learn about the other type of scene navigation, please see the Automatic scene transitions section.
Because this is a Flow, scene implemetations should feel free to emit new values over time. For example, the Lockscreen scene ties the “swipe up” user action to go to the Bouncer scene if the device is still locked or to go to the Gone scene if the device is unlocked, allowing the user to dismiss the lockscreen UI when not locked.
The Scene Framework separates transition animations from content UI declaration by placing the definition of the former in a different location. This way, there's no longer a need to contaminate the content UI declaration with animation logic, a practice that becomes unscalable over time.
Under the hood, the Scene Framework uses SceneTransitionLayout, a @Composable function designed with scene graph and transitions in mind. In fact, the Scene Framework is merely a shallow wrapper around SceneTransitionLayout.
The SceneTransitionLayout API requires the transitions to be passed-in separately from the scenes themselves. In System UI, the transitions can be found in SceneContainerTransitions. As you can see, each possible scene-to-scene transition has its own builder, here's one example:
fun TransitionBuilder.lockscreenToShadeTransition() { spec = tween(durationMillis = 500) punchHole(Shade.Elements.QuickSettings, bounds = Shade.Elements.Scrim, Shade.Shapes.Scrim) translate(Shade.Elements.Scrim, Edge.Top, startsOutsideLayoutBounds = false) fractionRange(end = 0.5f) { fade(Shade.Elements.ScrimBackground) translate( QuickSettings.Elements.CollapsedGrid, Edge.Top, startsOutsideLayoutBounds = false, ) } fractionRange(start = 0.5f) { fade(Notifications.Elements.Notifications) } }
Going through the example code: * The spec is the animation that should be invoked, in the example above, we use a tween animation with a duration of 500 milliseconds * Then there‘s a series of function calls: punchHole applies a clip mask to the Scrim element in the destination scene (in this case it’s the Shade scene) which has the position and size determined by the bounds parameter and the shape passed into the shape parameter. This lets the Lockscreen scene render “through” the Shade scene * The translate call shifts the Scrim element to/from the Top edge of the scene container * The first fractionRange wrapper tells the system to apply its contained functions only during the first half of the transition. Inside of it, we see a fade of the ScrimBackground element and a translate o the CollpasedGrid element to/from the Top edge * The second fractionRange only starts at the second half of the transition (e.g. when the previous one ends) and applies a fade on the Notifications element
You can find the actual documentation for this API here.
As demonstrated above, elements within a scene can be addressed from transition defintions. In order to “tag” an element with a specific ElementKey, the element modifier must be used on the composable that declared that element's UI:
Text( text = "Some text", modifier = Modifier.element(MyElements.SomeText), )
In addition to the ability to refer to a tagged element in transition definitions, if the same ElementKey is used for one element in the current scene and another element in the destination scene, the element is considered to be a shared element. As such, the framework automatically translates and scales the bounds of the shared element from its current bounds in the source scene to its final bounds in the destination scene.
To set up a scene framework instance, a scene container must be declared. This is the root of an entire scene graph that puts together the scenes, their transitions, and the configuration. The container is then added to a parent @Composable or View so it can be displayed.
The default scene container in System UI is defined in the SceneContainer.kt file.
The SceneContainer function is passed a few parameters including a view-model and a set of scenes. The exact details of what gets passed in depends on the SceneContainerConfig object which is injected into the Dagger dependency graph here.
The scene framework supports the ability for scenes to change automatically based on device state or events other than direct user input. For example: when the device is locked, there's an automatic scene transition to the Lockscreen scene.
This logic is contained within the SceneContainerStartable class.
Similarly to the above, the SceneContainerStartable also handles side-effects by updating other parts of the System UI codebase whenever internal scene framework state changes. As an example: the visibility of the View that contains our scene container is updated every time there's a transition to or from the Gone scene.
There are a couple of ways to observe the transition state:
SceneScope of the scene container, simply use the animateSharedXAsState API, the full list is here.SceneScope of the scene container, observe SceneInteractor.transitionState.The entire framework is provided into the Dagger dependency graph from the top-level Dagger module at SceneContainerFrameworkModule this puts together the scenes from SceneModule, the configuration from SceneContainerConfigModule, and the startable from SceneContainerStartableModule.