blob: 3175dcfa092b65295df2bbfa68016789289dd276 [file] [log] [blame]
/*
* 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
}