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

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

Merge "Small UI improvements for tiles" into main

parents fd85ef94 65198945
Loading
Loading
Loading
Loading
+1 −5
Original line number Diff line number Diff line
@@ -32,11 +32,7 @@ import com.android.systemui.common.shared.model.Icon
 * Note: You can use [Color.Unspecified] to disable the tint and keep the original icon colors.
 */
@Composable
fun Icon(
    icon: Icon,
    modifier: Modifier = Modifier,
    tint: Color = LocalContentColor.current,
) {
fun Icon(icon: Icon, modifier: Modifier = Modifier, tint: Color = LocalContentColor.current) {
    val contentDescription = icon.contentDescription?.load()
    when (icon) {
        is Icon.Loaded -> {
+25 −11
Original line number Diff line number Diff line
@@ -18,12 +18,12 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid

import android.graphics.drawable.Animatable
import android.text.TextUtils
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
import androidx.compose.animation.graphics.res.animatedVectorResource
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
import androidx.compose.animation.graphics.vector.AnimatedImageVector
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -32,8 +32,9 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -44,6 +45,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.Shape
@@ -57,7 +59,9 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.semantics.toggleableState
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.android.compose.modifiers.size
import com.android.compose.modifiers.thenIf
import com.android.systemui.Flags
import com.android.systemui.common.shared.model.Icon
@@ -88,12 +92,14 @@ fun LargeTileContent(
    ) {
        // Icon
        val longPressLabel = longPressLabel().takeIf { onLongClick != null }
        val animatedBackgroundColor by
            animateColorAsState(colors.iconBackground, label = "QSTileDualTargetBackgroundColor")
        Box(
            modifier =
                Modifier.size(CommonTileDefaults.ToggleTargetSize).thenIf(toggleClick != null) {
                    Modifier.clip(iconShape)
                        .verticalSquish(squishiness)
                        .background(colors.iconBackground)
                        .drawBehind { drawRect(animatedBackgroundColor) }
                        .combinedClickable(
                            onClick = toggleClick!!,
                            onLongClick = onLongClick,
@@ -117,6 +123,7 @@ fun LargeTileContent(
            SmallTileContent(
                icon = icon,
                color = colors.icon,
                size = { CommonTileDefaults.LargeTileIconSize },
                modifier = Modifier.align(Alignment.Center),
            )
        }
@@ -139,18 +146,22 @@ fun LargeTileLabels(
    modifier: Modifier = Modifier,
    accessibilityUiState: AccessibilityUiState? = null,
) {
    val animatedLabelColor by animateColorAsState(colors.label, label = "QSTileLabelColor")
    val animatedSecondaryLabelColor by
        animateColorAsState(colors.secondaryLabel, label = "QSTileSecondaryLabelColor")
    Column(verticalArrangement = Arrangement.Center, modifier = modifier.fillMaxHeight()) {
        Text(
        BasicText(
            label,
            style = MaterialTheme.typography.labelLarge,
            color = colors.label,
            color = { animatedLabelColor },
            maxLines = 1,
            overflow = TextOverflow.Ellipsis,
        )
        if (!TextUtils.isEmpty(secondaryLabel)) {
            Text(
            BasicText(
                secondaryLabel ?: "",
                color = colors.secondaryLabel,
                color = { animatedSecondaryLabelColor },
                maxLines = 1,
                style = MaterialTheme.typography.bodyMedium,
                modifier =
                    Modifier.thenIf(
@@ -170,9 +181,11 @@ fun SmallTileContent(
    modifier: Modifier = Modifier,
    icon: Icon,
    color: Color,
    size: () -> Dp = { CommonTileDefaults.IconSize },
    animateToEnd: Boolean = false,
) {
    val iconModifier = modifier.size(CommonTileDefaults.IconSize)
    val animatedColor by animateColorAsState(color, label = "QSTileIconColor")
    val iconModifier = modifier.size({ size().roundToPx() }, { size().roundToPx() })
    val context = LocalContext.current
    val loadedDrawable =
        remember(icon, context) {
@@ -182,7 +195,7 @@ fun SmallTileContent(
            }
        }
    if (loadedDrawable !is Animatable) {
        Icon(icon = icon, tint = color, modifier = iconModifier)
        Icon(icon = icon, tint = animatedColor, modifier = iconModifier)
    } else if (icon is Icon.Resource) {
        val image = AnimatedImageVector.animatedVectorResource(id = icon.res)
        val painter =
@@ -198,14 +211,15 @@ fun SmallTileContent(
        Image(
            painter = painter,
            contentDescription = icon.contentDescription?.load(),
            colorFilter = ColorFilter.tint(color = color),
            colorFilter = ColorFilter.tint(color = animatedColor),
            modifier = iconModifier,
        )
    }
}

object CommonTileDefaults {
    val IconSize = 24.dp
    val IconSize = 32.dp
    val LargeTileIconSize = 28.dp
    val ToggleTargetSize = 56.dp
    val TileHeight = 72.dp
    val TilePadding = 8.dp
+83 −35
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid

import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.fadeIn
@@ -54,7 +55,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
@@ -69,21 +69,22 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.BiasAlignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.positionInRoot
@@ -103,6 +104,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.fastMap
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.compose.animation.bounceable
import com.android.compose.modifiers.height
import com.android.systemui.common.ui.compose.load
@@ -134,9 +136,10 @@ import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.shared.model.groupAndSort
import com.android.systemui.res.R
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import com.android.app.tracing.coroutines.launchTraced as launch
import kotlinx.coroutines.flow.collectLatest

object TileType

@@ -409,7 +412,7 @@ private fun GridCell.key(index: Int, dragAndDropState: DragAndDropState): Any {
/**
 * Adds a list of [GridCell] to the lazy grid
 *
 * @param cells the pairs of [GridCell] to [BounceableTileViewModel]
 * @param cells the pairs of [GridCell] to [AnimatableTileViewModel]
 * @param dragAndDropState the [DragAndDropState] for this grid
 * @param selectionState the [MutableSelectionState] for this grid
 * @param onToggleSize the callback when a tile's size is toggled
@@ -545,9 +548,27 @@ private fun TileGridCell(
                    selectionState::unSelect,
                )
                .tileBackground(colors.background)
                .tilePadding()
        ) {
            EditTile(tile = cell.tile, iconOnly = cell.isIcon)
            val targetValue = if (cell.isIcon) 0f else 1f
            val animatedProgress = remember { Animatable(targetValue) }

            if (selected) {
                val resizingState = selectionState.resizingState
                LaunchedEffect(targetValue, resizingState) {
                    if (resizingState == null) {
                        animatedProgress.animateTo(targetValue)
                    } else {
                        snapshotFlow { resizingState.progression }
                            .collectLatest { animatedProgress.snapTo(it) }
                    }
                }
            }

            EditTile(
                tile = cell.tile,
                tileWidths = { tileWidths },
                progress = { animatedProgress.value },
            )
        }
    }
}
@@ -612,45 +633,72 @@ private fun SpacerGridCell(modifier: Modifier = Modifier) {
}

@Composable
fun BoxScope.EditTile(
fun EditTile(
    tile: EditTileViewModel,
    iconOnly: Boolean,
    tileWidths: () -> TileWidths?,
    progress: () -> Float,
    colors: TileColors = EditModeTileDefaults.editTileColors(),
) {
    // Animated horizontal alignment from center (0f) to start (-1f)
    val alignmentValue by
        animateFloatAsState(
            targetValue = if (iconOnly) 0f else -1f,
            label = "QSEditTileContentAlignment",
        )
    val alignment by remember {
        derivedStateOf { BiasAlignment(horizontalBias = alignmentValue, verticalBias = 0f) }
    val iconSizeDiff = CommonTileDefaults.IconSize - CommonTileDefaults.LargeTileIconSize
    Row(
        horizontalArrangement = spacedBy(6.dp),
        verticalAlignment = Alignment.CenterVertically,
        modifier =
            Modifier.layout { measurable, constraints ->
                    // Always display the tile using the large size and trust the parent composable
                    // to clip the content as needed. This stop the labels from being truncated.
                    val width = tileWidths()?.max ?: constraints.maxWidth
                    val placeable =
                        measurable.measure(constraints.copy(minWidth = width, maxWidth = width))
                    val currentProgress = progress()
                    val startPadding =
                        if (currentProgress == 0f) {
                            // Find the center of the max width when the tile is icon only
                            iconHorizontalCenter(constraints.maxWidth)
                        } else {
                            // Find the center of the minimum width to hold the same position as the
                            // tile is resized.
                            val basePadding =
                                tileWidths()?.min?.let { iconHorizontalCenter(it) } ?: 0f
                            // Large tiles, represented with a progress of 1f, have a 0.dp padding
                            basePadding * (1f - currentProgress)
                        }

                    layout(constraints.maxWidth, constraints.maxHeight) {
                        placeable.place(startPadding.roundToInt(), 0)
                    }
                }
                .tilePadding(),
    ) {
        // Icon
    Box(Modifier.size(ToggleTargetSize).align(alignment)) {
        Box(Modifier.size(ToggleTargetSize)) {
            SmallTileContent(
                icon = tile.icon,
                color = colors.icon,
                animateToEnd = true,
                size = { CommonTileDefaults.IconSize - iconSizeDiff * progress() },
                modifier = Modifier.align(Alignment.Center),
            )
        }

        // Labels, positioned after the icon
    AnimatedVisibility(visible = !iconOnly, enter = fadeIn(), exit = fadeOut()) {
        LargeTileLabels(
            label = tile.label.text,
            secondaryLabel = tile.appName?.text,
            colors = colors,
            modifier = Modifier.padding(start = ToggleTargetSize + TileArrangementPadding),
            modifier = Modifier.weight(1f).graphicsLayer { alpha = progress() },
        )
    }
}

private fun Modifier.tileBackground(color: Color): Modifier {
    return drawBehind {
        drawRoundRect(SolidColor(color), cornerRadius = CornerRadius(InactiveCornerRadius.toPx()))
private fun MeasureScope.iconHorizontalCenter(containerSize: Int): Float {
    return (containerSize - ToggleTargetSize.roundToPx()) / 2f -
        CommonTileDefaults.TilePadding.toPx()
}

private fun Modifier.tileBackground(color: Color): Modifier {
    // Clip tile contents from overflowing past the tile
    return clip(RoundedCornerShape(InactiveCornerRadius)).drawBehind { drawRect(color) }
}

private object EditModeTileDefaults {
+30 −8
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid
import android.content.res.Resources
import android.service.quicksettings.Tile.STATE_ACTIVE
import android.service.quicksettings.Tile.STATE_INACTIVE
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
@@ -61,6 +62,7 @@ import androidx.compose.ui.semantics.toggleableState
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.compose.animation.Expandable
import com.android.compose.animation.bounceable
import com.android.compose.modifiers.thenIf
@@ -74,6 +76,7 @@ import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.qs.panels.ui.compose.BounceableInfo
import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius
import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TileHeight
import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.longPressLabel
import com.android.systemui.qs.panels.ui.viewmodel.TileUiState
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
@@ -82,7 +85,6 @@ import com.android.systemui.qs.tileimpl.QSTileImpl
import com.android.systemui.res.R
import java.util.function.Supplier
import kotlinx.coroutines.CoroutineScope
import com.android.app.tracing.coroutines.launchTraced as launch

private const val TEST_TAG_SMALL = "qs_tile_small"
private const val TEST_TAG_LARGE = "qs_tile_large"
@@ -128,14 +130,18 @@ fun Tile(

    // TODO(b/361789146): Draw the shapes instead of clipping
    val tileShape = TileDefaults.animateTileShape(uiState.state)

    TileExpandable(
        color =
    val animatedColor by
        animateColorAsState(
            if (iconOnly || !uiState.handlesSecondaryClick) {
                colors.iconBackground
            } else {
                colors.background
            },
            label = "QSTileBackgroundColor",
        )

    TileExpandable(
        color = { animatedColor },
        shape = tileShape,
        squishiness = squishiness,
        hapticsViewModel = hapticsViewModel,
@@ -212,7 +218,7 @@ fun Tile(

@Composable
private fun TileExpandable(
    color: Color,
    color: () -> Color,
    shape: Shape,
    squishiness: () -> Float,
    hapticsViewModel: TileHapticsViewModel?,
@@ -220,7 +226,7 @@ private fun TileExpandable(
    content: @Composable (Expandable) -> Unit,
) {
    Expandable(
        color = color,
        color = color(),
        shape = shape,
        modifier = modifier.clip(shape).verticalSquish(squishiness),
    ) {
@@ -238,7 +244,7 @@ fun TileContainer(
) {
    Box(
        modifier =
            Modifier.height(CommonTileDefaults.TileHeight)
            Modifier.height(TileHeight)
                .fillMaxWidth()
                .tileCombinedClickable(
                    onClick = onClick,
@@ -335,6 +341,16 @@ private object TileDefaults {
            icon = MaterialTheme.colorScheme.onPrimary,
        )

    @Composable
    fun inactiveDualTargetTileColors(): TileColors =
        TileColors(
            background = MaterialTheme.colorScheme.surfaceVariant,
            iconBackground = MaterialTheme.colorScheme.surfaceContainerHighest,
            label = MaterialTheme.colorScheme.onSurfaceVariant,
            secondaryLabel = MaterialTheme.colorScheme.onSurfaceVariant,
            icon = MaterialTheme.colorScheme.onSurfaceVariant,
        )

    @Composable
    fun inactiveTileColors(): TileColors =
        TileColors(
@@ -365,7 +381,13 @@ private object TileDefaults {
                    activeTileColors()
                }
            }
            STATE_INACTIVE -> inactiveTileColors()
            STATE_INACTIVE -> {
                if (uiState.handlesSecondaryClick) {
                    inactiveDualTargetTileColors()
                } else {
                    inactiveTileColors()
                }
            }
            else -> unavailableTileColors()
        }
    }
+10 −5
Original line number Diff line number Diff line
@@ -17,25 +17,30 @@
package com.android.systemui.qs.panels.ui.compose.selection

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.setValue
import com.android.systemui.qs.panels.ui.compose.selection.ResizingDefaults.RESIZING_THRESHOLD

class ResizingState(private val widths: TileWidths, private val onResize: () -> Unit) {
    // Total drag offset of this resize operation
    private var totalOffset = 0f
    /** Total drag offset of this resize operation. */
    private var totalOffset by mutableFloatStateOf(0f)

    /** Width in pixels of the resizing tile. */
    var width by mutableIntStateOf(widths.base)

    /** Progression between icon (0) and large (1) sizes. */
    val progression
        get() = calculateProgression()

    // Whether the tile is currently over the threshold and should be a large tile
    private var passedThreshold: Boolean = passedThreshold(calculateProgression(width))
    private var passedThreshold: Boolean = passedThreshold(progression)

    fun onDrag(offset: Float) {
        totalOffset += offset
        width = (widths.base + totalOffset).toInt().coerceIn(widths.min, widths.max)

        passedThreshold(calculateProgression(width)).let {
        passedThreshold(progression).let {
            // Resize if we went over the threshold
            if (passedThreshold != it) {
                passedThreshold = it
@@ -49,7 +54,7 @@ class ResizingState(private val widths: TileWidths, private val onResize: () ->
    }

    /** The progression of the resizing tile between an icon tile (0f) and a large tile (1f) */
    private fun calculateProgression(width: Int): Float {
    private fun calculateProgression(): Float {
        return ((width - widths.min) / (widths.max - widths.min).toFloat()).coerceIn(0f, 1f)
    }
}
Loading