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

Commit eb135e29 authored by Olivier St-Onge's avatar Olivier St-Onge Committed by Android (Google) Code Review
Browse files

Merge "Implement UI for dual target tiles" into main

parents aacd102e 63cfa8c2
Loading
Loading
Loading
Loading
+34 −6
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@

package com.android.systemui.qs.panels.ui.compose

import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -40,6 +39,13 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.addOutline
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
@@ -268,10 +274,9 @@ class PartitionedGridLayout @Inject constructor(private val viewModel: Partition
    private fun CurrentTilesContainer(content: @Composable () -> Unit) {
        Box(
            Modifier.fillMaxWidth()
                .border(
                    width = 1.dp,
                    color = MaterialTheme.colorScheme.onBackground,
                    shape = RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius))
                .dashedBorder(
                    color = MaterialTheme.colorScheme.onBackground.copy(alpha = .5f),
                    shape = Dimensions.ContainerShape,
                )
                .padding(dimensionResource(R.dimen.qs_tile_margin_vertical))
        ) {
@@ -286,7 +291,7 @@ class PartitionedGridLayout @Inject constructor(private val viewModel: Partition
                .background(
                    color = MaterialTheme.colorScheme.background,
                    alpha = { 1f },
                    shape = RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius))
                    shape = Dimensions.ContainerShape,
                )
                .padding(dimensionResource(R.dimen.qs_tile_margin_vertical))
        ) {
@@ -305,4 +310,27 @@ class PartitionedGridLayout @Inject constructor(private val viewModel: Partition
            item(span = { GridItemSpan(maxCurrentLineSpan) }) { Spacer(Modifier) }
        }
    }

    private fun Modifier.dashedBorder(
        color: Color,
        shape: Shape,
    ): Modifier {
        return this.drawWithContent {
            val outline = shape.createOutline(size, layoutDirection, this)
            val path = Path()
            path.addOutline(outline)
            val stroke =
                Stroke(
                    width = 1.dp.toPx(),
                    pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f))
                )
            this.drawContent()
            drawPath(path = path, style = stroke, color = color)
        }
    }

    private object Dimensions {
        // Corner radius is half the height of a tile + padding
        val ContainerShape = RoundedCornerShape(48.dp)
    }
}
+204 −152
Original line number Diff line number Diff line
@@ -14,6 +14,8 @@
 * limitations under the License.
 */

@file:OptIn(ExperimentalFoundationApi::class)

package com.android.systemui.qs.panels.ui.compose

import android.graphics.drawable.Animatable
@@ -27,17 +29,17 @@ import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
import androidx.compose.animation.graphics.vector.AnimatedImageVector
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
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.fillMaxWidth
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -46,11 +48,6 @@ import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Remove
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -68,7 +65,6 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
@@ -78,9 +74,11 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.Expandable
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.compose.Icon
import com.android.systemui.common.ui.compose.load
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.qs.panels.ui.viewmodel.AvailableEditActions
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileUiState
@@ -90,13 +88,14 @@ import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.tileimpl.QSTileImpl
import com.android.systemui.res.R
import java.util.function.Supplier
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.mapLatest

object TileType

@OptIn(ExperimentalCoroutinesApi::class, ExperimentalFoundationApi::class)
@OptIn(ExperimentalCoroutinesApi::class)
@Composable
fun Tile(
    tile: TileViewModel,
@@ -110,44 +109,141 @@ fun Tile(
            .collectAsStateWithLifecycle(tile.currentState.toUiState())
    val colors = TileDefaults.getColorForState(state.state)

    val context = LocalContext.current
    TileContainer(
        colors = colors,
        showLabels = showLabels,
        label = state.label.toString(),
        iconOnly = iconOnly,
        onClick = tile::onClick,
        onLongClick = tile::onLongClick,
        modifier = modifier,
    ) {
        val icon = getTileIcon(icon = state.icon)
        if (iconOnly) {
            TileIcon(icon = icon, color = colors.icon, modifier = Modifier.align(Alignment.Center))
        } else {
            LargeTileContent(
                label = state.label.toString(),
                secondaryLabel = state.secondaryLabel.toString(),
                icon = icon,
                colors = colors,
                onClick = tile::onSecondaryClick,
                onLongClick = tile::onLongClick,
            )
        }
    }
}

@Composable
private fun TileContainer(
    colors: TileColors,
    showLabels: Boolean,
    label: String,
    iconOnly: Boolean,
    clickEnabled: Boolean = true,
    onClick: (Expandable) -> Unit = {},
    onLongClick: (Expandable) -> Unit = {},
    modifier: Modifier = Modifier,
    content: @Composable BoxScope.() -> Unit,
) {
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement =
            spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin), Alignment.Top),
        modifier = modifier,
    ) {
        val backgroundColor =
            if (iconOnly) {
                colors.iconBackground
            } else {
                colors.background
            }
        Expandable(
        color = colors.background,
        shape = RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius)),
            color = backgroundColor,
            shape = TileDefaults.TileShape,
            modifier =
                Modifier.height(dimensionResource(id = R.dimen.qs_tile_height))
                    .clip(TileDefaults.TileShape)
        ) {
        Row(
            Box(
                modifier =
                modifier
                    Modifier.fillMaxSize()
                        .combinedClickable(
                        onClick = { tile.onClick(it) },
                        onLongClick = { tile.onLongClick(it) }
                            enabled = clickEnabled,
                            onClick = { onClick(it) },
                            onLongClick = { onLongClick(it) }
                        )
                    .tileModifier(colors),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = tileHorizontalArrangement(iconOnly),
                        .tilePadding(),
            ) {
            val icon =
                remember(state.icon) {
                    state.icon.get().let {
                        if (it is QSTileImpl.ResourceIcon) {
                            Icon.Resource(it.resId, null)
                        } else {
                            Icon.Loaded(it.getDrawable(context), null)
                content()
            }
        }

        if (showLabels && iconOnly) {
            Text(
                label,
                maxLines = 2,
                color = colors.label,
                overflow = TextOverflow.Ellipsis,
                textAlign = TextAlign.Center,
            )
        }
            TileContent(
                label = state.label.toString(),
                secondaryLabel = state.secondaryLabel.toString(),
    }
}

@Composable
private fun LargeTileContent(
    label: String,
    secondaryLabel: String?,
    icon: Icon,
    colors: TileColors,
    clickEnabled: Boolean = true,
    onClick: (Expandable) -> Unit = {},
    onLongClick: (Expandable) -> Unit = {},
) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = tileHorizontalArrangement()
    ) {
        Expandable(
            color = colors.iconBackground,
            shape = TileDefaults.TileShape,
            modifier = Modifier.fillMaxHeight().aspectRatio(1f)
        ) {
            Box(
                modifier =
                    Modifier.fillMaxSize()
                        .clip(TileDefaults.TileShape)
                        .combinedClickable(
                            enabled = clickEnabled,
                            onClick = { onClick(it) },
                            onLongClick = { onLongClick(it) }
                        )
            ) {
                TileIcon(
                    icon = icon,
                colors = colors,
                iconOnly = iconOnly,
                showLabels = showLabels,
                    color = colors.icon,
                    modifier = Modifier.align(Alignment.Center)
                )
            }
        }

        Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) {
            Text(
                label,
                color = colors.label,
                modifier = Modifier.basicMarquee(),
            )
            if (!TextUtils.isEmpty(secondaryLabel)) {
                Text(
                    secondaryLabel ?: "",
                    color = colors.secondaryLabel,
                    modifier = Modifier.basicMarquee(),
                )
            }
        }
    }
}

@Composable
fun TileLazyGrid(
@@ -247,41 +343,19 @@ fun LazyGridScope.editTiles(
                ""
            }

        Box(
            modifier =
                Modifier.clickable(enabled = canClick) { onClick.invoke(viewModel.tileSpec) }
                    .animateItem()
                    .semantics {
                        onClick(onClickActionName) { false }
                        this.stateDescription = stateDescription
                    }
        ) {
        val iconOnly = isIconOnly(viewModel.tileSpec)
        val tileHeight = tileHeight(iconOnly && showLabels)
        EditTile(
            tileViewModel = viewModel,
            iconOnly = iconOnly,
            showLabels = showLabels,
                modifier = Modifier.height(tileHeight)
            )
            if (canClick) {
                Badge(clickAction, Modifier.align(Alignment.TopEnd))
            }
        }
    }
            clickEnabled = canClick,
            onClick = { onClick.invoke(viewModel.tileSpec) },
            modifier =
                Modifier.height(tileHeight).animateItem().semantics {
                    onClick(onClickActionName) { false }
                    this.stateDescription = stateDescription
                }

@Composable
fun Badge(action: ClickAction, modifier: Modifier = Modifier) {
    Box(modifier = modifier.size(16.dp).background(Color.Cyan, shape = CircleShape)) {
        Icon(
            imageVector =
                when (action) {
                    ClickAction.ADD -> Icons.Filled.Add
                    ClickAction.REMOVE -> Icons.Filled.Remove
                },
            "",
            tint = Color.Black,
        )
    }
}
@@ -291,41 +365,69 @@ fun EditTile(
    tileViewModel: EditTileViewModel,
    iconOnly: Boolean,
    showLabels: Boolean,
    clickEnabled: Boolean,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    val label = tileViewModel.label.load() ?: tileViewModel.tileSpec.spec
    val colors = TileDefaults.inactiveTileColors()

    Row(
        modifier = modifier.tileModifier(colors).semantics { this.contentDescription = label },
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = tileHorizontalArrangement(iconOnly)
    TileContainer(
        colors = colors,
        showLabels = showLabels,
        label = label,
        iconOnly = iconOnly,
        clickEnabled = clickEnabled,
        onClick = { onClick() },
        onLongClick = { onClick() },
        modifier = modifier,
    ) {
        TileContent(
        if (iconOnly) {
            TileIcon(
                icon = tileViewModel.icon,
                color = colors.icon,
                modifier = Modifier.align(Alignment.Center)
            )
        } else {
            LargeTileContent(
                label = label,
                secondaryLabel = tileViewModel.appName?.load(),
            colors = colors,
                icon = tileViewModel.icon,
            iconOnly = iconOnly,
            showLabels = showLabels,
            animateIconToEnd = true,
                colors = colors,
                clickEnabled = clickEnabled,
                onClick = { onClick() },
                onLongClick = { onClick() },
            )
        }
    }
}

enum class ClickAction {
    ADD,
    REMOVE,
}

@Composable
private fun getTileIcon(icon: Supplier<QSTile.Icon>): Icon {
    val context = LocalContext.current
    return icon.get().let {
        if (it is QSTileImpl.ResourceIcon) {
            Icon.Resource(it.resId, null)
        } else {
            Icon.Loaded(it.getDrawable(context), null)
        }
    }
}

@OptIn(ExperimentalAnimationGraphicsApi::class)
@Composable
private fun TileIcon(
    icon: Icon,
    color: Color,
    animateToEnd: Boolean = false,
    modifier: Modifier = Modifier,
) {
    val modifier = Modifier.size(dimensionResource(id = R.dimen.qs_icon_size))
    val iconModifier = modifier.size(dimensionResource(id = R.dimen.qs_icon_size))
    val context = LocalContext.current
    val loadedDrawable =
        remember(icon, context) {
@@ -338,7 +440,7 @@ private fun TileIcon(
        Icon(
            icon = icon,
            tint = color,
            modifier = modifier,
            modifier = iconModifier,
        )
    } else if (icon is Icon.Resource) {
        val image = AnimatedImageVector.animatedVectorResource(id = icon.res)
@@ -357,78 +459,23 @@ private fun TileIcon(
            painter = painter,
            contentDescription = null,
            colorFilter = ColorFilter.tint(color = color),
            modifier = modifier
            modifier = iconModifier
        )
    }
}

@Composable
private fun Modifier.tileModifier(colors: TileColors): Modifier {
    return fillMaxWidth()
        .clip(RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius)))
        .background(colors.background)
        .padding(horizontal = dimensionResource(id = R.dimen.qs_label_container_margin))
private fun Modifier.tilePadding(): Modifier {
    return padding(dimensionResource(id = R.dimen.qs_label_container_margin))
}

@Composable
private fun tileHorizontalArrangement(iconOnly: Boolean): Arrangement.Horizontal {
    val horizontalAlignment =
        if (iconOnly) {
            Alignment.CenterHorizontally
        } else {
            Alignment.Start
        }
private fun tileHorizontalArrangement(): Arrangement.Horizontal {
    return spacedBy(
        space = dimensionResource(id = R.dimen.qs_label_container_margin),
        alignment = horizontalAlignment
    )
}

@Composable
private fun TileContent(
    label: String,
    secondaryLabel: String?,
    icon: Icon,
    colors: TileColors,
    iconOnly: Boolean,
    showLabels: Boolean = false,
    animateIconToEnd: Boolean = false,
) {
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
        modifier = Modifier.fillMaxHeight()
    ) {
        TileIcon(icon, colors.icon, animateIconToEnd)

        if (iconOnly && showLabels) {
            Text(
                label,
                maxLines = 2,
                color = colors.label,
                overflow = TextOverflow.Ellipsis,
                textAlign = TextAlign.Center,
            )
        }
    }

    if (!iconOnly) {
        Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) {
            Text(
                label,
                color = colors.label,
                modifier = Modifier.basicMarquee(),
            )
            if (!TextUtils.isEmpty(secondaryLabel)) {
                Text(
                    secondaryLabel ?: "",
                    color = colors.secondaryLabel,
                    modifier = Modifier.basicMarquee(),
        alignment = Alignment.Start
    )
}
        }
    }
}

@Composable
fun tileHeight(iconWithLabel: Boolean = false): Dp {
@@ -441,20 +488,23 @@ fun tileHeight(iconWithLabel: Boolean = false): Dp {

private data class TileColors(
    val background: Color,
    val iconBackground: Color,
    val label: Color,
    val secondaryLabel: Color,
    val icon: Color,
)

private object TileDefaults {
    val IconTileWithLabelHeight = 100.dp
    val TileShape = CircleShape
    val IconTileWithLabelHeight = 140.dp

    @Composable
    fun activeTileColors(): TileColors =
        TileColors(
            background = MaterialTheme.colorScheme.primary,
            label = MaterialTheme.colorScheme.onPrimary,
            secondaryLabel = MaterialTheme.colorScheme.onPrimary,
            background = MaterialTheme.colorScheme.surfaceVariant,
            iconBackground = MaterialTheme.colorScheme.primary,
            label = MaterialTheme.colorScheme.onSurfaceVariant,
            secondaryLabel = MaterialTheme.colorScheme.onSurfaceVariant,
            icon = MaterialTheme.colorScheme.onPrimary,
        )

@@ -462,6 +512,7 @@ private object TileDefaults {
    fun inactiveTileColors(): TileColors =
        TileColors(
            background = MaterialTheme.colorScheme.surfaceVariant,
            iconBackground = MaterialTheme.colorScheme.surfaceVariant,
            label = MaterialTheme.colorScheme.onSurfaceVariant,
            secondaryLabel = MaterialTheme.colorScheme.onSurfaceVariant,
            icon = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -471,6 +522,7 @@ private object TileDefaults {
    fun unavailableTileColors(): TileColors =
        TileColors(
            background = MaterialTheme.colorScheme.surface,
            iconBackground = MaterialTheme.colorScheme.surface,
            label = MaterialTheme.colorScheme.onSurface,
            secondaryLabel = MaterialTheme.colorScheme.onSurface,
            icon = MaterialTheme.colorScheme.onSurface,
+4 −0
Original line number Diff line number Diff line
@@ -48,6 +48,10 @@ class TileViewModel(private val tile: QSTile, val spec: TileSpec) {
        tile.longClick(expandable)
    }

    fun onSecondaryClick(expandable: Expandable?) {
        tile.secondaryClick(expandable)
    }

    fun startListening(token: Any) = tile.setListening(token, true)

    fun stopListening(token: Any) = tile.setListening(token, false)