Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e7f3fd57 authored by Ale Nijamkin's avatar Ale Nijamkin Committed by Automerger Merge Worker
Browse files

Merge changes I90d871bb,If284f43c into tm-qpr-dev am: 8877729c am: 8a6624ce

parents a3875731 8a6624ce
Loading
Loading
Loading
Loading
+110 −0
Original line number Diff line number Diff line
/*
 * 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,
    )
}
+31 −0
Original line number Diff line number Diff line
/*
 * 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)
    }
}
+423 −0
Original line number Diff line number Diff line
/*
 * 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
}
+77 −0
Original line number Diff line number Diff line
/*
 * 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")
        }
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -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,
            )
        )