Merge "[flexiglass] Scalable scene transition definitions in Flexiglass." into udc-qpr-dev

This commit is contained in:
Ale Nijamkin
2023-08-14 21:05:02 +00:00
committed by Android (Google) Code Review
19 changed files with 348 additions and 126 deletions

View File

@@ -37,7 +37,14 @@ sealed class Key(val name: String, val identity: Any) {
}
/** Key for a scene. */
class SceneKey(name: String, identity: Any = Object()) : Key(name, identity) {
class SceneKey(
name: String,
identity: Any = Object(),
) : Key(name, identity) {
/** The unique [ElementKey] identifying this scene's root element. */
val rootElementKey = ElementKey(name, identity)
override fun toString(): String {
return "SceneKey(name=$name)"
}

View File

@@ -35,7 +35,7 @@ import com.android.compose.ui.util.fastMap
/** The transitions configuration of a [SceneTransitionLayout]. */
class SceneTransitions(
val transitionSpecs: List<TransitionSpec>,
private val transitionSpecs: List<TransitionSpec>,
) {
private val cache = mutableMapOf<SceneKey, MutableMap<SceneKey, TransitionSpec>>()

View File

@@ -75,7 +75,7 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder {
}
}
private class TransitionBuilderImpl : TransitionBuilder {
internal class TransitionBuilderImpl : TransitionBuilder {
val transformations = mutableListOf<Transformation>()
override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow)

View File

@@ -24,7 +24,7 @@ import android.content.DialogInterface
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -46,6 +46,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.SceneScope
import com.android.systemui.R
import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel
@@ -63,6 +64,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
object Bouncer {
object Elements {
val Background = ElementKey("BouncerBackground")
val Content = ElementKey("BouncerContent")
}
}
/** The bouncer scene displays authentication challenges like PIN, password, or pattern. */
@SysUISingleton
class BouncerScene
@@ -88,7 +96,7 @@ constructor(
}
@Composable
private fun BouncerScene(
private fun SceneScope.BouncerScene(
viewModel: BouncerViewModel,
dialogFactory: BouncerSceneDialogFactory,
modifier: Modifier = Modifier,
@@ -97,84 +105,90 @@ private fun BouncerScene(
val authMethodViewModel: AuthMethodBouncerViewModel? by viewModel.authMethod.collectAsState()
val dialogMessage: String? by viewModel.throttlingDialogMessage.collectAsState()
var dialog: Dialog? by remember { mutableStateOf(null) }
val backgroundColor = MaterialTheme.colorScheme.surface
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(60.dp),
modifier =
modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface)
.padding(start = 32.dp, top = 92.dp, end = 32.dp, bottom = 32.dp)
) {
Crossfade(
targetState = message,
label = "Bouncer message",
animationSpec = if (message.isUpdateAnimated) tween() else snap(),
) { message ->
Text(
text = message.text,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyLarge,
)
Box(modifier) {
Canvas(Modifier.element(Bouncer.Elements.Background).fillMaxSize()) {
drawRect(color = backgroundColor)
}
Box(Modifier.weight(1f)) {
when (val nonNullViewModel = authMethodViewModel) {
is PinBouncerViewModel ->
PinBouncer(
viewModel = nonNullViewModel,
modifier = Modifier.align(Alignment.Center),
)
is PasswordBouncerViewModel ->
PasswordBouncer(
viewModel = nonNullViewModel,
modifier = Modifier.align(Alignment.Center),
)
is PatternBouncerViewModel ->
PatternBouncer(
viewModel = nonNullViewModel,
modifier =
Modifier.aspectRatio(1f, matchHeightConstraintsFirst = false)
.align(Alignment.BottomCenter),
)
else -> Unit
}
}
Button(
onClick = viewModel::onEmergencyServicesButtonClicked,
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
),
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(60.dp),
modifier =
Modifier.element(Bouncer.Elements.Content)
.fillMaxSize()
.padding(start = 32.dp, top = 92.dp, end = 32.dp, bottom = 32.dp)
) {
Text(
text = stringResource(com.android.internal.R.string.lockscreen_emergency_call),
style = MaterialTheme.typography.bodyMedium,
)
}
if (dialogMessage != null) {
if (dialog == null) {
dialog =
dialogFactory().apply {
setMessage(dialogMessage)
setButton(
DialogInterface.BUTTON_NEUTRAL,
context.getString(R.string.ok),
) { _, _ ->
viewModel.onThrottlingDialogDismissed()
}
setCancelable(false)
setCanceledOnTouchOutside(false)
show()
}
Crossfade(
targetState = message,
label = "Bouncer message",
animationSpec = if (message.isUpdateAnimated) tween() else snap(),
) { message ->
Text(
text = message.text,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyLarge,
)
}
Box(Modifier.weight(1f)) {
when (val nonNullViewModel = authMethodViewModel) {
is PinBouncerViewModel ->
PinBouncer(
viewModel = nonNullViewModel,
modifier = Modifier.align(Alignment.Center),
)
is PasswordBouncerViewModel ->
PasswordBouncer(
viewModel = nonNullViewModel,
modifier = Modifier.align(Alignment.Center),
)
is PatternBouncerViewModel ->
PatternBouncer(
viewModel = nonNullViewModel,
modifier =
Modifier.aspectRatio(1f, matchHeightConstraintsFirst = false)
.align(Alignment.BottomCenter),
)
else -> Unit
}
}
Button(
onClick = viewModel::onEmergencyServicesButtonClicked,
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
),
) {
Text(
text = stringResource(com.android.internal.R.string.lockscreen_emergency_call),
style = MaterialTheme.typography.bodyMedium,
)
}
if (dialogMessage != null) {
if (dialog == null) {
dialog =
dialogFactory().apply {
setMessage(dialogMessage)
setButton(
DialogInterface.BUTTON_NEUTRAL,
context.getString(R.string.ok),
) { _, _ ->
viewModel.onThrottlingDialogDismissed()
}
setCancelable(false)
setCanceledOnTouchOutside(false)
show()
}
}
} else {
dialog?.dismiss()
dialog = null
}
} else {
dialog?.dismiss()
dialog = null
}
}
}

View File

@@ -31,9 +31,17 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.SceneScope
object Notifications {
object Elements {
val Notifications = ElementKey("Notifications")
}
}
@Composable
fun Notifications(
fun SceneScope.Notifications(
modifier: Modifier = Modifier,
) {
// TODO(b/272779828): implement.

View File

@@ -31,15 +31,27 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.SceneScope
object QuickSettings {
object Elements {
// TODO RENAME
val Content = ElementKey("QuickSettingsContent")
val CollapsedGrid = ElementKey("QuickSettingsCollapsedGrid")
val FooterActions = ElementKey("QuickSettingsFooterActions")
}
}
@Composable
fun QuickSettings(
fun SceneScope.QuickSettings(
modifier: Modifier = Modifier,
) {
// TODO(b/272780058): implement.
Column(
modifier =
modifier
.element(QuickSettings.Elements.Content)
.fillMaxWidth()
.defaultMinSize(minHeight = 300.dp)
.clip(RoundedCornerShape(32.dp))
@@ -47,15 +59,19 @@ fun QuickSettings(
.padding(16.dp),
) {
Text(
text = "Quick settings",
modifier = Modifier.align(Alignment.CenterHorizontally),
text = "Quick settings grid",
modifier =
Modifier.element(QuickSettings.Elements.CollapsedGrid)
.align(Alignment.CenterHorizontally),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onPrimary,
)
Spacer(modifier = Modifier.weight(1f))
Text(
text = "QS footer actions",
modifier = Modifier.align(Alignment.CenterHorizontally),
modifier =
Modifier.element(QuickSettings.Elements.FooterActions)
.align(Alignment.CenterHorizontally),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onPrimary,
)

View File

@@ -16,19 +16,17 @@
package com.android.systemui.qs.ui.composable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.SceneScope
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.qs.footer.ui.compose.QuickSettings
import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel
import com.android.systemui.scene.shared.model.Direction
import com.android.systemui.scene.shared.model.SceneKey
@@ -69,23 +67,18 @@ constructor(
}
@Composable
private fun QuickSettingsScene(
private fun SceneScope.QuickSettingsScene(
viewModel: QuickSettingsSceneViewModel,
modifier: Modifier = Modifier,
) {
// TODO(b/280887232): implement the real UI.
Box(modifier = modifier) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.align(Alignment.Center)
) {
Text("Quick settings", style = MaterialTheme.typography.headlineMedium)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Button(onClick = { viewModel.onContentClicked() }) { Text("Open some content") }
}
}
Box(
modifier
.fillMaxSize()
.clickable(onClick = { viewModel.onContentClicked() })
.padding(horizontal = 16.dp, vertical = 48.dp)
) {
QuickSettings(modifier = Modifier.fillMaxHeight())
}
}

View File

@@ -31,7 +31,6 @@ import com.android.compose.animation.scene.SceneTransitionLayoutState
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.UserAction as SceneTransitionUserAction
import com.android.compose.animation.scene.observableTransitionState
import com.android.compose.animation.scene.transitions
import com.android.systemui.scene.shared.model.Direction
import com.android.systemui.scene.shared.model.ObservableTransitionState
import com.android.systemui.scene.shared.model.SceneKey
@@ -78,7 +77,7 @@ fun SceneContainer(
SceneTransitionLayout(
currentScene = currentSceneKey.toTransitionSceneKey(),
onChangeScene = viewModel::onSceneChanged,
transitions = transitions {},
transitions = SceneContainerTransitions,
state = state,
modifier = modifier.fillMaxSize(),
) {
@@ -98,7 +97,9 @@ fun SceneContainer(
) {
with(composableScene) {
this@scene.Content(
modifier = Modifier.fillMaxSize(),
modifier =
Modifier.element(sceneKey.toTransitionSceneKey().rootElementKey)
.fillMaxSize(),
)
}
}
@@ -128,14 +129,6 @@ private fun toTransitionModels(
return userAction.toTransitionUserAction() to sceneModel.key.toTransitionSceneKey()
}
// TODO(b/293899074): remove this once we can use the one from SceneTransitionLayout.
private fun SceneKey.toTransitionSceneKey(): SceneTransitionSceneKey {
return SceneTransitionSceneKey(
name = toString(),
identity = this,
)
}
// TODO(b/293899074): remove this once we can use the one from SceneTransitionLayout.
private fun SceneTransitionSceneKey.toModel(): SceneModel {
return SceneModel(key = identity as SceneKey)

View File

@@ -0,0 +1,34 @@
package com.android.systemui.scene.ui.composable
import com.android.compose.animation.scene.transitions
import com.android.systemui.scene.ui.composable.transitions.bouncerToGoneTransition
import com.android.systemui.scene.ui.composable.transitions.goneToQuickSettingsTransition
import com.android.systemui.scene.ui.composable.transitions.goneToShadeTransition
import com.android.systemui.scene.ui.composable.transitions.lockscreenToBouncerTransition
import com.android.systemui.scene.ui.composable.transitions.lockscreenToGoneTransition
import com.android.systemui.scene.ui.composable.transitions.lockscreenToQuickSettingsTransition
import com.android.systemui.scene.ui.composable.transitions.lockscreenToShadeTransition
import com.android.systemui.scene.ui.composable.transitions.shadeToQuickSettingsTransition
/**
* Comprehensive definition of all transitions between scenes in [SceneContainer].
*
* Transitions are automatically reversible, so define only one transition per scene pair. By
* convention, use the more common transition direction when defining the pair order, e.g.
* Lockscreen to Bouncer rather than Bouncer to Lockscreen.
*
* The actual transition DSL must be placed in a separate file under the package
* [com.android.systemui.scene.ui.composable.transitions].
*
* Please keep the list sorted alphabetically.
*/
val SceneContainerTransitions = transitions {
from(Bouncer, to = Gone) { bouncerToGoneTransition() }
from(Gone, to = Shade) { goneToShadeTransition() }
from(Gone, to = QuickSettings) { goneToQuickSettingsTransition() }
from(Lockscreen, to = Bouncer) { lockscreenToBouncerTransition() }
from(Lockscreen, to = Shade) { lockscreenToShadeTransition() }
from(Lockscreen, to = QuickSettings) { lockscreenToQuickSettingsTransition() }
from(Lockscreen, to = Gone) { lockscreenToGoneTransition() }
from(Shade, to = QuickSettings) { shadeToQuickSettingsTransition() }
}

View File

@@ -0,0 +1,15 @@
package com.android.systemui.scene.ui.composable
import com.android.compose.animation.scene.SceneKey as SceneTransitionSceneKey
import com.android.systemui.scene.shared.model.SceneKey
val Lockscreen = SceneKey.Lockscreen.toTransitionSceneKey()
val Bouncer = SceneKey.Bouncer.toTransitionSceneKey()
val Shade = SceneKey.Shade.toTransitionSceneKey()
val QuickSettings = SceneKey.QuickSettings.toTransitionSceneKey()
val Gone = SceneKey.Gone.toTransitionSceneKey()
// TODO(b/293899074): Remove this file once we can use the scene keys from SceneTransitionLayout.
fun SceneKey.toTransitionSceneKey(): SceneTransitionSceneKey {
return SceneTransitionSceneKey(name = toString(), identity = this)
}

View File

@@ -0,0 +1,11 @@
package com.android.systemui.scene.ui.composable.transitions
import androidx.compose.animation.core.tween
import com.android.compose.animation.scene.TransitionBuilder
import com.android.systemui.scene.ui.composable.Bouncer
fun TransitionBuilder.bouncerToGoneTransition() {
spec = tween(durationMillis = 500)
fade(Bouncer.rootElementKey)
}

View File

@@ -0,0 +1,11 @@
package com.android.systemui.scene.ui.composable.transitions
import androidx.compose.animation.core.tween
import com.android.compose.animation.scene.TransitionBuilder
import com.android.systemui.scene.ui.composable.QuickSettings
fun TransitionBuilder.goneToQuickSettingsTransition() {
spec = tween(durationMillis = 500)
fade(QuickSettings.rootElementKey)
}

View File

@@ -0,0 +1,11 @@
package com.android.systemui.scene.ui.composable.transitions
import androidx.compose.animation.core.tween
import com.android.compose.animation.scene.TransitionBuilder
import com.android.systemui.scene.ui.composable.Shade
fun TransitionBuilder.goneToShadeTransition() {
spec = tween(durationMillis = 500)
fade(Shade.rootElementKey)
}

View File

@@ -0,0 +1,14 @@
package com.android.systemui.scene.ui.composable.transitions
import androidx.compose.animation.core.tween
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.TransitionBuilder
import com.android.systemui.bouncer.ui.composable.Bouncer
fun TransitionBuilder.lockscreenToBouncerTransition() {
spec = tween(durationMillis = 500)
translate(Bouncer.Elements.Content, y = 300.dp)
fractionRange(end = 0.5f) { fade(Bouncer.Elements.Background) }
fractionRange(start = 0.5f) { fade(Bouncer.Elements.Content) }
}

View File

@@ -0,0 +1,11 @@
package com.android.systemui.scene.ui.composable.transitions
import androidx.compose.animation.core.tween
import com.android.compose.animation.scene.TransitionBuilder
import com.android.systemui.scene.ui.composable.Lockscreen
fun TransitionBuilder.lockscreenToGoneTransition() {
spec = tween(durationMillis = 500)
fade(Lockscreen.rootElementKey)
}

View File

@@ -0,0 +1,11 @@
package com.android.systemui.scene.ui.composable.transitions
import androidx.compose.animation.core.tween
import com.android.compose.animation.scene.TransitionBuilder
import com.android.systemui.scene.ui.composable.QuickSettings
fun TransitionBuilder.lockscreenToQuickSettingsTransition() {
spec = tween(durationMillis = 500)
fade(QuickSettings.rootElementKey)
}

View File

@@ -0,0 +1,24 @@
package com.android.systemui.scene.ui.composable.transitions
import androidx.compose.animation.core.tween
import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.TransitionBuilder
import com.android.systemui.notifications.ui.composable.Notifications
import com.android.systemui.qs.footer.ui.compose.QuickSettings
import com.android.systemui.shade.ui.composable.Shade
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) }
}

View File

@@ -0,0 +1,15 @@
package com.android.systemui.scene.ui.composable.transitions
import androidx.compose.animation.core.tween
import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.TransitionBuilder
import com.android.systemui.notifications.ui.composable.Notifications
import com.android.systemui.qs.footer.ui.compose.QuickSettings
fun TransitionBuilder.shadeToQuickSettingsTransition() {
spec = tween(durationMillis = 500)
translate(Notifications.Elements.Notifications, Edge.Bottom)
fade(Notifications.Elements.Notifications)
timestampRange(endMillis = 83) { fade(QuickSettings.Elements.FooterActions) }
}

View File

@@ -16,16 +16,22 @@
package com.android.systemui.shade.ui.composable
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.SceneScope
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -44,6 +50,26 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
object Shade {
object Elements {
val QuickSettings = ElementKey("ShadeQuickSettings")
val Scrim = ElementKey("ShadeScrim")
val ScrimBackground = ElementKey("ShadeScrimBackground")
}
object Dimensions {
val ScrimCornerSize = 32.dp
}
object Shapes {
val Scrim =
RoundedCornerShape(
topStart = Dimensions.ScrimCornerSize,
topEnd = Dimensions.ScrimCornerSize,
)
}
}
/** The shade scene shows scrolling list of notifications and some of the quick setting tiles. */
@SysUISingleton
class ShadeScene
@@ -79,20 +105,28 @@ constructor(
}
@Composable
private fun ShadeScene(
private fun SceneScope.ShadeScene(
viewModel: ShadeSceneViewModel,
modifier: Modifier = Modifier,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier =
modifier
.fillMaxSize()
.clickable(onClick = { viewModel.onContentClicked() })
.padding(horizontal = 16.dp, vertical = 48.dp)
) {
QuickSettings(modifier = Modifier.height(160.dp))
Notifications(modifier = Modifier.weight(1f))
Box(modifier.element(Shade.Elements.Scrim)) {
Spacer(
modifier =
Modifier.element(Shade.Elements.ScrimBackground)
.fillMaxSize()
.background(MaterialTheme.colorScheme.scrim, shape = Shade.Shapes.Scrim)
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier =
Modifier.fillMaxSize()
.clickable(onClick = { viewModel.onContentClicked() })
.padding(horizontal = 16.dp, vertical = 48.dp)
) {
QuickSettings(modifier = Modifier.height(160.dp))
Notifications(modifier = Modifier.weight(1f))
}
}
}