summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/compose/core/src/com/android/systemui/compose/SystemUiButtons.kt110
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/TextExt.kt31
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt423
-rw-r--r--packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ButtonsScreen.kt77
-rw-r--r--packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt2
5 files changed, 643 insertions, 0 deletions
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/SystemUiButtons.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/SystemUiButtons.kt
new file mode 100644
index 000000000000..496f4b3460f7
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/SystemUiButtons.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.compose
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ButtonColors
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.android.systemui.compose.theme.LocalAndroidColorScheme
+
+@Composable
+fun SysUiButton(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ content: @Composable RowScope.() -> Unit,
+) {
+ androidx.compose.material3.Button(
+ modifier = modifier.padding(vertical = 6.dp).height(36.dp),
+ colors = filledButtonColors(),
+ contentPadding = ButtonPaddings,
+ onClick = onClick,
+ enabled = enabled,
+ ) {
+ content()
+ }
+}
+
+@Composable
+fun SysUiOutlinedButton(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ content: @Composable RowScope.() -> Unit,
+) {
+ androidx.compose.material3.OutlinedButton(
+ modifier = modifier.padding(vertical = 6.dp).height(36.dp),
+ enabled = enabled,
+ colors = outlineButtonColors(),
+ border = outlineButtonBorder(),
+ contentPadding = ButtonPaddings,
+ onClick = onClick,
+ ) {
+ content()
+ }
+}
+
+@Composable
+fun SysUiTextButton(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ content: @Composable RowScope.() -> Unit,
+) {
+ androidx.compose.material3.TextButton(
+ onClick = onClick,
+ modifier = modifier,
+ enabled = enabled,
+ content = content,
+ )
+}
+
+private val ButtonPaddings = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
+
+@Composable
+private fun filledButtonColors(): ButtonColors {
+ val colors = LocalAndroidColorScheme.current
+ return ButtonDefaults.buttonColors(
+ containerColor = colors.colorAccentPrimary,
+ contentColor = colors.textColorOnAccent,
+ )
+}
+
+@Composable
+private fun outlineButtonColors(): ButtonColors {
+ val colors = LocalAndroidColorScheme.current
+ return ButtonDefaults.outlinedButtonColors(
+ contentColor = colors.textColorPrimary,
+ )
+}
+
+@Composable
+private fun outlineButtonBorder(): BorderStroke {
+ val colors = LocalAndroidColorScheme.current
+ return BorderStroke(
+ width = 1.dp,
+ color = colors.colorAccentPrimaryVariant,
+ )
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/TextExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/TextExt.kt
new file mode 100644
index 000000000000..e1f73e304b9e
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/TextExt.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.common.ui.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import com.android.systemui.common.shared.model.Text
+
+/** Returns the loaded [String] or `null` if there isn't one. */
+@Composable
+fun Text.load(): String? {
+ return when (this) {
+ is Text.Loaded -> text
+ is Text.Resource -> stringResource(res)
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt
new file mode 100644
index 000000000000..3175dcfa092b
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/user/ui/compose/UserSwitcherScreen.kt
@@ -0,0 +1,423 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.user.ui.compose
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.drawable.Drawable
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+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.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.painter.ColorPainter
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.android.systemui.common.ui.compose.load
+import com.android.systemui.compose.SysUiOutlinedButton
+import com.android.systemui.compose.SysUiTextButton
+import com.android.systemui.compose.features.R
+import com.android.systemui.compose.theme.LocalAndroidColorScheme
+import com.android.systemui.user.ui.viewmodel.UserActionViewModel
+import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
+import com.android.systemui.user.ui.viewmodel.UserViewModel
+import java.lang.Integer.min
+import kotlin.math.ceil
+
+@Composable
+fun UserSwitcherScreen(
+ viewModel: UserSwitcherViewModel,
+ onFinished: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val isFinishRequested: Boolean by viewModel.isFinishRequested.collectAsState(false)
+ val users: List<UserViewModel> by viewModel.users.collectAsState(emptyList())
+ val maxUserColumns: Int by viewModel.maximumUserColumns.collectAsState(1)
+ val menuActions: List<UserActionViewModel> by viewModel.menu.collectAsState(emptyList())
+ val isOpenMenuButtonVisible: Boolean by viewModel.isOpenMenuButtonVisible.collectAsState(false)
+ val isMenuVisible: Boolean by viewModel.isMenuVisible.collectAsState(false)
+
+ UserSwitcherScreenStateless(
+ isFinishRequested = isFinishRequested,
+ users = users,
+ maxUserColumns = maxUserColumns,
+ menuActions = menuActions,
+ isOpenMenuButtonVisible = isOpenMenuButtonVisible,
+ isMenuVisible = isMenuVisible,
+ onMenuClosed = viewModel::onMenuClosed,
+ onOpenMenuButtonClicked = viewModel::onOpenMenuButtonClicked,
+ onCancelButtonClicked = viewModel::onCancelButtonClicked,
+ onFinished = {
+ onFinished()
+ viewModel.onFinished()
+ },
+ modifier = modifier,
+ )
+}
+
+@Composable
+private fun UserSwitcherScreenStateless(
+ isFinishRequested: Boolean,
+ users: List<UserViewModel>,
+ maxUserColumns: Int,
+ menuActions: List<UserActionViewModel>,
+ isOpenMenuButtonVisible: Boolean,
+ isMenuVisible: Boolean,
+ onMenuClosed: () -> Unit,
+ onOpenMenuButtonClicked: () -> Unit,
+ onCancelButtonClicked: () -> Unit,
+ onFinished: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ LaunchedEffect(isFinishRequested) {
+ if (isFinishRequested) {
+ onFinished()
+ }
+ }
+
+ Box(
+ modifier =
+ modifier
+ .fillMaxSize()
+ .padding(
+ horizontal = 60.dp,
+ vertical = 40.dp,
+ ),
+ ) {
+ UserGrid(
+ users = users,
+ maxUserColumns = maxUserColumns,
+ modifier = Modifier.align(Alignment.Center),
+ )
+
+ Buttons(
+ menuActions = menuActions,
+ isOpenMenuButtonVisible = isOpenMenuButtonVisible,
+ isMenuVisible = isMenuVisible,
+ onMenuClosed = onMenuClosed,
+ onOpenMenuButtonClicked = onOpenMenuButtonClicked,
+ onCancelButtonClicked = onCancelButtonClicked,
+ modifier = Modifier.align(Alignment.BottomEnd),
+ )
+ }
+}
+
+@Composable
+private fun UserGrid(
+ users: List<UserViewModel>,
+ maxUserColumns: Int,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(44.dp),
+ modifier = modifier,
+ ) {
+ val rowCount = ceil(users.size / maxUserColumns.toFloat()).toInt()
+ (0 until rowCount).forEach { rowIndex ->
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(64.dp),
+ modifier = modifier,
+ ) {
+ val fromIndex = rowIndex * maxUserColumns
+ val toIndex = min(users.size, (rowIndex + 1) * maxUserColumns)
+ users.subList(fromIndex, toIndex).forEach { user ->
+ UserItem(
+ viewModel = user,
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun UserItem(
+ viewModel: UserViewModel,
+) {
+ val onClicked = viewModel.onClicked
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier =
+ if (onClicked != null) {
+ Modifier.clickable { onClicked() }
+ } else {
+ Modifier
+ }
+ .alpha(viewModel.alpha),
+ ) {
+ Box {
+ UserItemBackground(modifier = Modifier.align(Alignment.Center).size(222.dp))
+
+ UserItemIcon(
+ image = viewModel.image,
+ isSelectionMarkerVisible = viewModel.isSelectionMarkerVisible,
+ modifier = Modifier.align(Alignment.Center).size(222.dp)
+ )
+ }
+
+ // User name
+ val text = viewModel.name.load()
+ if (text != null) {
+ // We use the box to center-align the text vertically as that is not possible with Text
+ // alone.
+ Box(
+ modifier = Modifier.size(width = 222.dp, height = 48.dp),
+ ) {
+ Text(
+ text = text,
+ style = MaterialTheme.typography.titleLarge,
+ color = colorResource(com.android.internal.R.color.system_neutral1_50),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.align(Alignment.Center),
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun UserItemBackground(
+ modifier: Modifier = Modifier,
+) {
+ Image(
+ painter = ColorPainter(LocalAndroidColorScheme.current.colorBackground),
+ contentDescription = null,
+ modifier = modifier.clip(CircleShape),
+ )
+}
+
+@Composable
+private fun UserItemIcon(
+ image: Drawable,
+ isSelectionMarkerVisible: Boolean,
+ modifier: Modifier = Modifier,
+) {
+ Image(
+ bitmap = image.toBitmap().asImageBitmap(),
+ contentDescription = null,
+ modifier =
+ if (isSelectionMarkerVisible) {
+ // Draws a ring
+ modifier.border(
+ width = 8.dp,
+ color = LocalAndroidColorScheme.current.colorAccentPrimary,
+ shape = CircleShape,
+ )
+ } else {
+ modifier
+ }
+ .padding(16.dp)
+ .clip(CircleShape)
+ )
+}
+
+@Composable
+private fun Buttons(
+ menuActions: List<UserActionViewModel>,
+ isOpenMenuButtonVisible: Boolean,
+ isMenuVisible: Boolean,
+ onMenuClosed: () -> Unit,
+ onOpenMenuButtonClicked: () -> Unit,
+ onCancelButtonClicked: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ modifier = modifier,
+ ) {
+ // Cancel button.
+ SysUiTextButton(
+ onClick = onCancelButtonClicked,
+ ) {
+ Text(stringResource(R.string.cancel))
+ }
+
+ // "Open menu" button.
+ if (isOpenMenuButtonVisible) {
+ Spacer(modifier = Modifier.width(8.dp))
+ // To properly use a DropdownMenu in Compose, we need to wrap the button that opens it
+ // and the menu itself in a Box.
+ Box {
+ SysUiOutlinedButton(
+ onClick = onOpenMenuButtonClicked,
+ ) {
+ Text(stringResource(R.string.add))
+ }
+ Menu(
+ viewModel = menuActions,
+ isMenuVisible = isMenuVisible,
+ onMenuClosed = onMenuClosed,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun Menu(
+ viewModel: List<UserActionViewModel>,
+ isMenuVisible: Boolean,
+ onMenuClosed: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val maxItemWidth = LocalConfiguration.current.screenWidthDp.dp / 4
+ DropdownMenu(
+ expanded = isMenuVisible,
+ onDismissRequest = onMenuClosed,
+ modifier =
+ modifier.background(
+ color = MaterialTheme.colorScheme.inverseOnSurface,
+ ),
+ ) {
+ viewModel.forEachIndexed { index, action ->
+ MenuItem(
+ viewModel = action,
+ onClicked = { action.onClicked() },
+ topPadding =
+ if (index == 0) {
+ 16.dp
+ } else {
+ 0.dp
+ },
+ bottomPadding =
+ if (index == viewModel.size - 1) {
+ 16.dp
+ } else {
+ 0.dp
+ },
+ modifier = Modifier.sizeIn(maxWidth = maxItemWidth),
+ )
+ }
+ }
+}
+
+@Composable
+private fun MenuItem(
+ viewModel: UserActionViewModel,
+ onClicked: () -> Unit,
+ topPadding: Dp,
+ bottomPadding: Dp,
+ modifier: Modifier = Modifier,
+) {
+ val context = LocalContext.current
+ val density = LocalDensity.current
+
+ val icon =
+ remember(viewModel.iconResourceId) {
+ val drawable =
+ checkNotNull(AppCompatResources.getDrawable(context, viewModel.iconResourceId))
+ drawable
+ .toBitmap(
+ size = with(density) { 20.dp.toPx() }.toInt(),
+ tintColor = Color.White,
+ )
+ .asImageBitmap()
+ }
+
+ DropdownMenuItem(
+ text = {
+ Text(
+ text = stringResource(viewModel.textResourceId),
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ },
+ onClick = onClicked,
+ leadingIcon = {
+ Spacer(modifier = Modifier.width(10.dp))
+ Image(
+ bitmap = icon,
+ contentDescription = null,
+ )
+ },
+ modifier =
+ modifier
+ .heightIn(
+ min = 56.dp,
+ )
+ .padding(
+ start = 18.dp,
+ end = 65.dp,
+ top = topPadding,
+ bottom = bottomPadding,
+ ),
+ )
+}
+
+/**
+ * Converts the [Drawable] to a [Bitmap].
+ *
+ * Note that this is a relatively memory-heavy operation as it allocates a whole bitmap and draws
+ * the `Drawable` onto it. Use sparingly and with care.
+ */
+private fun Drawable.toBitmap(
+ size: Int? = null,
+ tintColor: Color? = null,
+): Bitmap {
+ val bitmap =
+ if (intrinsicWidth <= 0 || intrinsicHeight <= 0) {
+ Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
+ } else {
+ Bitmap.createBitmap(
+ size ?: intrinsicWidth,
+ size ?: intrinsicHeight,
+ Bitmap.Config.ARGB_8888
+ )
+ }
+ val canvas = Canvas(bitmap)
+ setBounds(0, 0, canvas.width, canvas.height)
+ if (tintColor != null) {
+ setTint(tintColor.toArgb())
+ }
+ draw(canvas)
+ return bitmap
+}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ButtonsScreen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ButtonsScreen.kt
new file mode 100644
index 000000000000..881a1def113a
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ButtonsScreen.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+@file:OptIn(ExperimentalMaterial3Api::class)
+
+package com.android.systemui.compose.gallery
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.android.systemui.compose.SysUiButton
+import com.android.systemui.compose.SysUiOutlinedButton
+import com.android.systemui.compose.SysUiTextButton
+
+@Composable
+fun ButtonsScreen(
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier,
+ ) {
+ SysUiButton(
+ onClick = {},
+ ) {
+ Text("SysUiButton")
+ }
+
+ SysUiButton(
+ onClick = {},
+ enabled = false,
+ ) {
+ Text("SysUiButton - disabled")
+ }
+
+ SysUiOutlinedButton(
+ onClick = {},
+ ) {
+ Text("SysUiOutlinedButton")
+ }
+
+ SysUiOutlinedButton(
+ onClick = {},
+ enabled = false,
+ ) {
+ Text("SysUiOutlinedButton - disabled")
+ }
+
+ SysUiTextButton(
+ onClick = {},
+ ) {
+ Text("SysUiTextButton")
+ }
+
+ SysUiTextButton(
+ onClick = {},
+ enabled = false,
+ ) {
+ Text("SysUiTextButton - disabled")
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt
index bb98fb350a2e..2e6456bcc4e1 100644
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt
@@ -31,6 +31,7 @@ object GalleryAppScreens {
val Typography = ChildScreen("typography") { TypographyScreen() }
val MaterialColors = ChildScreen("material_colors") { MaterialColorsScreen() }
val AndroidColors = ChildScreen("android_colors") { AndroidColorsScreen() }
+ val Buttons = ChildScreen("buttons") { ButtonsScreen() }
val ExampleFeature = ChildScreen("example_feature") { ExampleFeatureScreen() }
val PeopleEmpty =
@@ -63,6 +64,7 @@ object GalleryAppScreens {
"Material colors" to MaterialColors,
"Android colors" to AndroidColors,
"Example feature" to ExampleFeature,
+ "Buttons" to Buttons,
"People" to People,
)
)