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

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

Merge changes I4dc32744,I6f836b45,Iea897582 into main

* changes:
  Add marquee to QS tiles.
  Enable dual target UI for all large tiles
  Skip initial icon animation when first composed
parents b7c53bb9 68a37df2
Loading
Loading
Loading
Loading
+107 −44
Original line number Diff line number Diff line
@@ -26,12 +26,14 @@ 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.basicMarquee
import androidx.compose.foundation.combinedClickable
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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@@ -39,6 +41,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
@@ -49,8 +52,16 @@ 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.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorProducer
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
@@ -60,7 +71,7 @@ import androidx.compose.ui.semantics.role
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.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.android.compose.modifiers.size
@@ -73,6 +84,9 @@ import com.android.systemui.common.ui.compose.load
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.SideIconHeight
import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.SideIconWidth
import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TILE_INITIAL_DELAY_MILLIS
import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TILE_MARQUEE_ITERATIONS
import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TileLabelBlurWidth
import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.longPressLabel
import com.android.systemui.qs.panels.ui.viewmodel.AccessibilityUiState
import com.android.systemui.qs.ui.compose.borderOnFocus
@@ -104,11 +118,12 @@ fun LargeTileContent(
        val focusBorderColor = MaterialTheme.colorScheme.secondary
        Box(
            modifier =
                Modifier.size(CommonTileDefaults.ToggleTargetSize).thenIf(toggleClick != null) {
                    Modifier.borderOnFocus(color = focusBorderColor, iconShape.topEnd)
                Modifier.size(CommonTileDefaults.ToggleTargetSize)
                    .clip(iconShape)
                    .verticalSquish(squishiness)
                    .drawBehind { drawRect(animatedBackgroundColor) }
                    .thenIf(toggleClick != null) {
                        Modifier.borderOnFocus(color = focusBorderColor, iconShape.topEnd)
                            .combinedClickable(
                                onClick = toggleClick!!,
                                onLongClick = onLongClick,
@@ -167,18 +182,15 @@ fun LargeTileLabels(
    val animatedSecondaryLabelColor by
        animateColorAsState(colors.secondaryLabel, label = "QSTileSecondaryLabelColor")
    Column(verticalArrangement = Arrangement.Center, modifier = modifier.fillMaxHeight()) {
        BasicText(
            label,
        TileLabel(
            text = label,
            style = MaterialTheme.typography.labelLarge,
            color = { animatedLabelColor },
            maxLines = 1,
            overflow = TextOverflow.Ellipsis,
        )
        if (!TextUtils.isEmpty(secondaryLabel)) {
            BasicText(
            TileLabel(
                secondaryLabel ?: "",
                color = { animatedSecondaryLabelColor },
                maxLines = 1,
                style = MaterialTheme.typography.bodyMedium,
                modifier =
                    Modifier.thenIf(
@@ -194,9 +206,9 @@ fun LargeTileLabels(

@Composable
fun SmallTileContent(
    modifier: Modifier = Modifier,
    iconProvider: Context.() -> Icon,
    color: Color,
    modifier: Modifier = Modifier,
    size: () -> Dp = { CommonTileDefaults.IconSize },
    animateToEnd: Boolean = false,
) {
@@ -212,31 +224,39 @@ fun SmallTileContent(
            }
        }
    if (loadedDrawable is Animatable) {
        // Skip initial animation, icons should animate only as the state change
        // and not when first composed
        var shouldSkipInitialAnimation by remember { mutableStateOf(true) }
        LaunchedEffect(Unit) { shouldSkipInitialAnimation = animateToEnd }

        val painter =
            when (icon) {
                is Icon.Resource -> {
                    val image = AnimatedImageVector.animatedVectorResource(id = icon.res)
                    key(icon) {
                        if (animateToEnd) {
                            rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = true)
                        } else {
                            var atEnd by remember(icon.res) { mutableStateOf(false) }
                        var atEnd by remember(icon) { mutableStateOf(shouldSkipInitialAnimation) }
                        LaunchedEffect(key1 = icon.res) { atEnd = true }
                            rememberAnimatedVectorPainter(
                                animatedImageVector = image,
                                atEnd = atEnd,
                            )
                        }

                        rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd)
                    }
                }

                is Icon.Loaded -> {
                    LaunchedEffect(loadedDrawable) {
                    val painter = rememberDrawablePainter(loadedDrawable)

                    // rememberDrawablePainter automatically starts the animation. Using
                    // SideEffect here to immediately stop it if needed
                    DisposableEffect(painter) {
                        if (loadedDrawable is AnimatedVectorDrawable) {
                            loadedDrawable.forceAnimationOnUI()
                        }
                        if (shouldSkipInitialAnimation) {
                            loadedDrawable.stop()
                        }
                    rememberDrawablePainter(loadedDrawable)
                        onDispose {}
                    }

                    painter
                }
            }

@@ -251,6 +271,45 @@ fun SmallTileContent(
    }
}

@Composable
private fun TileLabel(
    text: String,
    color: ColorProducer,
    style: TextStyle,
    modifier: Modifier = Modifier,
) {
    BasicText(
        text = text,
        color = color,
        style = style,
        maxLines = 1,
        modifier =
            modifier
                .fillMaxWidth()
                .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen }
                .drawWithContent {
                    drawContent()
                    // Draw a blur over the end of the text
                    val edgeWidthPx = TileLabelBlurWidth.toPx()
                    drawRect(
                        topLeft = Offset(size.width - edgeWidthPx, 0f),
                        size = Size(edgeWidthPx, size.height),
                        brush =
                            Brush.horizontalGradient(
                                colors = listOf(Color.Transparent, Color.Black),
                                startX = size.width,
                                endX = size.width - edgeWidthPx,
                            ),
                        blendMode = BlendMode.DstIn,
                    )
                }
                .basicMarquee(
                    iterations = TILE_MARQUEE_ITERATIONS,
                    initialDelayMillis = TILE_INITIAL_DELAY_MILLIS,
                ),
    )
}

object CommonTileDefaults {
    val IconSize = 32.dp
    val LargeTileIconSize = 28.dp
@@ -258,9 +317,13 @@ object CommonTileDefaults {
    val SideIconHeight = 20.dp
    val ToggleTargetSize = 56.dp
    val TileHeight = 72.dp
    val TilePadding = 8.dp
    val TileStartPadding = 8.dp
    val TileEndPadding = 16.dp
    val TileArrangementPadding = 6.dp
    val InactiveCornerRadius = 50.dp
    val TileLabelBlurWidth = 32.dp
    const val TILE_MARQUEE_ITERATIONS = 1
    const val TILE_INITIAL_DELAY_MILLIS = 2000

    @Composable fun longPressLabel() = stringResource(id = R.string.accessibility_long_click_tile)
}
+3 −3
Original line number Diff line number Diff line
@@ -713,7 +713,7 @@ private fun AvailableTileGridCell(
    // Displays the tile as an icon tile with the label underneath
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = spacedBy(CommonTileDefaults.TilePadding, Alignment.Top),
        verticalArrangement = spacedBy(CommonTileDefaults.TileStartPadding, Alignment.Top),
        modifier =
            modifier
                .graphicsLayer { this.alpha = alpha }
@@ -813,7 +813,7 @@ fun EditTile(
                        placeable.place(startPadding.roundToInt(), 0)
                    }
                }
                .tilePadding(),
                .largeTilePadding(),
    ) {
        // Icon
        Box(Modifier.size(ToggleTargetSize)) {
@@ -847,7 +847,7 @@ private fun toAvailableTiles(

private fun MeasureScope.iconHorizontalCenter(containerSize: Int): Float {
    return (containerSize - ToggleTargetSize.roundToPx()) / 2f -
        CommonTileDefaults.TilePadding.toPx()
        CommonTileDefaults.TileStartPadding.toPx()
}

private fun Modifier.tileBackground(color: Color): Modifier {
+10 −8
Original line number Diff line number Diff line
@@ -84,7 +84,9 @@ import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.qs.flags.QsDetailedView
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.TileEndPadding
import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TileHeight
import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TileStartPadding
import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.longPressLabel
import com.android.systemui.qs.panels.ui.viewmodel.DetailsViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileUiState
@@ -270,7 +272,7 @@ fun TileContainer(
                    iconOnly = iconOnly,
                )
                .sysuiResTag(if (iconOnly) TEST_TAG_SMALL else TEST_TAG_LARGE)
                .tilePadding(),
                .thenIf(!iconOnly) { Modifier.largeTilePadding() }, // Icon tiles are center aligned
        content = content,
    )
}
@@ -284,7 +286,7 @@ fun LargeStaticTile(uiState: TileUiState, modifier: Modifier = Modifier) {
            .clip(TileDefaults.animateTileShapeAsState(state = uiState.state).value)
            .background(colors.background)
            .height(TileHeight)
            .tilePadding()
            .largeTilePadding()
    ) {
        LargeTileContent(
            label = uiState.label,
@@ -311,8 +313,8 @@ fun tileHorizontalArrangement(): Arrangement.Horizontal {
    return spacedBy(space = CommonTileDefaults.TileArrangementPadding, alignment = Alignment.Start)
}

fun Modifier.tilePadding(): Modifier {
    return padding(CommonTileDefaults.TilePadding)
fun Modifier.largeTilePadding(): Modifier {
    return padding(start = TileStartPadding, end = TileEndPadding)
}

@Composable
@@ -356,10 +358,10 @@ private object TileDefaults {
    val ActiveIconCornerRadius = 16.dp
    val ActiveTileCornerRadius = 24.dp

    /** An active tile without dual target uses the active color as background */
    /** An active icon tile uses the active color as background */
    @Composable
    @ReadOnlyComposable
    fun activeTileColors(): TileColors =
    fun activeIconTileColors(): TileColors =
        TileColors(
            background = MaterialTheme.colorScheme.primary,
            iconBackground = MaterialTheme.colorScheme.primary,
@@ -418,10 +420,10 @@ private object TileDefaults {
    fun getColorForState(uiState: TileUiState, iconOnly: Boolean): TileColors {
        return when (uiState.state) {
            STATE_ACTIVE -> {
                if (uiState.handlesSecondaryClick && !iconOnly) {
                if (!iconOnly) {
                    activeDualTargetTileColors()
                } else {
                    activeTileColors()
                    activeIconTileColors()
                }
            }