diff options
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, ) ) |