[flexiglass] Revert^2 of Bouncer scene large screen support
This reverts commit 938618d45b1e28ceba6a041062405fea013263ae.
Test: see original CL
Change-Id: I656898c7703d1c29b56c97d8895d2185fba4540a
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index d0f2ce8..a61e959 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -14,40 +14,49 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalMaterial3Api::class)
-
package com.android.systemui.bouncer.ui.composable
import android.app.AlertDialog
import android.app.Dialog
import android.content.DialogInterface
import androidx.compose.animation.Crossfade
+import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
+import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.SceneScope
+import com.android.compose.windowsizeclass.LocalWindowSizeClass
import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
@@ -103,96 +112,231 @@
dialogFactory: BouncerSceneDialogFactory,
modifier: Modifier = Modifier,
) {
- val message: BouncerViewModel.MessageViewModel by viewModel.message.collectAsState()
- val authMethodViewModel: AuthMethodBouncerViewModel? by
- viewModel.authMethodViewModel.collectAsState()
- val dialogMessage: String? by viewModel.throttlingDialogMessage.collectAsState()
- var dialog: Dialog? by remember { mutableStateOf(null) }
val backgroundColor = MaterialTheme.colorScheme.surface
+ val windowSizeClass = LocalWindowSizeClass.current
Box(modifier) {
Canvas(Modifier.element(Bouncer.Elements.Background).fillMaxSize()) {
drawRect(color = backgroundColor)
}
- 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)
- ) {
- 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,
+ val childModifier = Modifier.element(Bouncer.Elements.Content).fillMaxSize()
+
+ when (windowSizeClass.widthSizeClass) {
+ WindowWidthSizeClass.Expanded ->
+ SideBySide(
+ viewModel = viewModel,
+ dialogFactory = dialogFactory,
+ modifier = childModifier,
)
- }
-
- 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,
+ WindowWidthSizeClass.Medium ->
+ Stacked(
+ viewModel = viewModel,
+ dialogFactory = dialogFactory,
+ modifier = childModifier,
)
- }
+ else ->
+ Bouncer(
+ viewModel = viewModel,
+ dialogFactory = dialogFactory,
+ modifier = childModifier,
+ )
+ }
+ }
+}
- 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
+/**
+ * Renders the contents of the actual bouncer UI, the area that takes user input to do an
+ * authentication attempt, including all messaging UI (directives, reasoning, errors, etc.).
+ */
+@Composable
+private fun Bouncer(
+ viewModel: BouncerViewModel,
+ dialogFactory: BouncerSceneDialogFactory,
+ modifier: Modifier = Modifier,
+) {
+ val message: BouncerViewModel.MessageViewModel by viewModel.message.collectAsState()
+ val authMethodViewModel: AuthMethodBouncerViewModel? by
+ viewModel.authMethodViewModel.collectAsState()
+ val dialogMessage: String? by viewModel.throttlingDialogMessage.collectAsState()
+ var dialog: Dialog? by remember { mutableStateOf(null) }
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(60.dp),
+ modifier = modifier.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.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
+ }
+ }
+}
+
+/** Renders the UI of the user switcher that's displayed on large screens next to the bouncer UI. */
+@Composable
+private fun UserSwitcher(
+ modifier: Modifier = Modifier,
+) {
+ Box(modifier) {
+ Text(
+ text = "TODO: the user switcher goes here",
+ modifier = Modifier.align(Alignment.Center)
+ )
+ }
+}
+
+/**
+ * Arranges the bouncer contents and user switcher contents side-by-side, supporting a double tap
+ * anywhere on the background to flip their positions.
+ */
+@Composable
+private fun SideBySide(
+ viewModel: BouncerViewModel,
+ dialogFactory: BouncerSceneDialogFactory,
+ modifier: Modifier = Modifier,
+) {
+ val layoutDirection = LocalLayoutDirection.current
+ val isLeftToRight = layoutDirection == LayoutDirection.Ltr
+ val (isUserSwitcherFirst, setUserSwitcherFirst) =
+ rememberSaveable(isLeftToRight) { mutableStateOf(isLeftToRight) }
+
+ Row(
+ modifier =
+ modifier.pointerInput(Unit) {
+ detectTapGestures(
+ onDoubleTap = { offset ->
+ // Depending on where the user double tapped, switch the elements such that
+ // the bouncer contents element is closer to the side that was double
+ // tapped.
+ setUserSwitcherFirst(offset.x > size.width / 2)
+ }
+ )
+ },
+ ) {
+ val animatedOffset by
+ animateFloatAsState(
+ targetValue =
+ if (isUserSwitcherFirst) {
+ // When the user switcher is first, both elements have their natural
+ // placement so they are not offset in any way.
+ 0f
+ } else if (isLeftToRight) {
+ // Since the user switcher is not first, the elements have to be swapped
+ // horizontally. In the case of LTR locales, this means pushing the user
+ // switcher to the right, hence the positive number.
+ 1f
+ } else {
+ // Since the user switcher is not first, the elements have to be swapped
+ // horizontally. In the case of RTL locales, this means pushing the user
+ // switcher to the left, hence the negative number.
+ -1f
+ },
+ label = "offset",
+ )
+
+ UserSwitcher(
+ modifier =
+ Modifier.fillMaxHeight().weight(1f).graphicsLayer {
+ translationX = size.width * animatedOffset
+ },
+ )
+ Bouncer(
+ viewModel = viewModel,
+ dialogFactory = dialogFactory,
+ modifier =
+ Modifier.fillMaxHeight().weight(1f).graphicsLayer {
+ // A negative sign is used to make sure this is offset in the direction that's
+ // opposite of the direction that the user switcher is pushed in.
+ translationX = -size.width * animatedOffset
+ },
+ )
+ }
+}
+
+/** Arranges the bouncer contents and user switcher contents one on top of the other. */
+@Composable
+private fun Stacked(
+ viewModel: BouncerViewModel,
+ dialogFactory: BouncerSceneDialogFactory,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier,
+ ) {
+ UserSwitcher(
+ modifier = Modifier.fillMaxWidth().weight(1f),
+ )
+ Bouncer(
+ viewModel = viewModel,
+ dialogFactory = dialogFactory,
+ modifier = Modifier.fillMaxWidth().weight(1f),
+ )
}
}