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

Commit 0ced3da6 authored by Lucas Dupin's avatar Lucas Dupin
Browse files

SmartScrim on ambientcue

New type of scrim that has two components:
- A glow under the navbar or recents button
- 2 glows under the action list

It doesn't animate anymore, which is better for performance and battery
life. The turbulence shader was also removed.

Fixes: 423382448
Test: atest ShortPillScreenshotTest
Test: atest NavBarPillScreenshotTest
Test: atest ActionListScreenshotTest
Flag: com.android.systemui.enable_underlay
Change-Id: Ie1fff47dba2afba155d458648dfd556a8215a8f4
parent e608e21b
Loading
Loading
Loading
Loading
+86 −14
Original line number Diff line number Diff line
@@ -28,9 +28,9 @@ import androidx.compose.foundation.gestures.snapping.SnapPosition.End
import androidx.compose.foundation.gestures.snapping.SnapPosition.Start
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
@@ -42,13 +42,16 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import com.android.compose.modifiers.padding
import com.android.systemui.ambientcue.ui.viewmodel.ActionViewModel
import kotlin.math.abs
import kotlin.math.max
@@ -57,14 +60,29 @@ import kotlin.math.max
fun ActionList(
    actions: List<ActionViewModel>,
    visible: Boolean,
    expanded: Boolean,
    onDismiss: () -> Unit,
    modifier: Modifier = Modifier,
    padding: PaddingValues = PaddingValues(0.dp),
    horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
) {
    val density = LocalDensity.current
    val minOverscrollDelta = (-8).dp
    val maxOverscrollDelta = 0.dp
    val columnSpacing = 8.dp
    val topPadding = 32.dp
    val minGradientHeight = 70.dp
    val edgeAligned = horizontalAlignment == Alignment.Start || horizontalAlignment == Alignment.End
    val smartScrimAlpha by
        animateFloatAsState(
            if (expanded) {
                0.25f
            } else if (visible && !edgeAligned) {
                0.1f
            } else {
                0f
            }
        )

    val scaleStiffnessMultiplier = 1000
    val scaleDampingRatio = 0.83f
@@ -72,14 +90,18 @@ fun ActionList(
    val overscrollStiffness = 2063f
    var containerHeightPx by remember { mutableIntStateOf(0) }

    val leftGradientColor = MaterialTheme.colorScheme.tertiary
    val rightGradientColor = MaterialTheme.colorScheme.primaryFixedDim

    // User should be able to drag down vertically to dismiss the action list.
    // The list will shrink as the user drags.
    val anchoredDraggableState = remember {
        AnchoredDraggableState(initialValue = if (visible) End else Start)
        AnchoredDraggableState(initialValue = if (visible && expanded) End else Start)
    }
    val minOverscrollDeltaPx = with(density) { minOverscrollDelta.toPx() }
    val maxOverscrollDeltaPx = with(density) { maxOverscrollDelta.toPx() }
    val columnSpacingPx = with(LocalDensity.current) { columnSpacing.toPx() }
    val columnSpacingPx = with(density) { columnSpacing.toPx() }
    val minGradientHeightPx = with(density) { minGradientHeight.toPx() }

    val scope = rememberCoroutineScope()
    val overscrollEffect = remember {
@@ -106,7 +128,19 @@ fun ActionList(
        }
    }

    LaunchedEffect(visible) { anchoredDraggableState.animateTo(if (visible) End else Start) }
    val scrimProgress by
        animateFloatAsState(
            progress,
            animationSpec =
                spring(
                    dampingRatio = Spring.DampingRatioMediumBouncy,
                    stiffness = Spring.StiffnessLow,
                ),
        )

    LaunchedEffect(visible, expanded) {
        anchoredDraggableState.animateTo(if (visible && expanded) End else Start)
    }

    Column(
        modifier =
@@ -114,7 +148,7 @@ fun ActionList(
                .anchoredDraggable(
                    state = anchoredDraggableState,
                    orientation = Orientation.Vertical,
                    enabled = visible,
                    enabled = expanded,
                    overscrollEffect = overscrollEffect,
                )
                .onGloballyPositioned { layoutCoordinates ->
@@ -126,8 +160,50 @@ fun ActionList(
                        }
                    )
                }
                .defaultMinSize(minHeight = 200.dp)
                .fillMaxWidth(),
                .drawBehind {
                    val sidePaddingPx =
                        with(density) { padding.calculateLeftPadding(layoutDirection).toPx() }
                    val radius = size.width - sidePaddingPx * 2f
                    val minScaleY = minGradientHeightPx / size.height
                    val scaleY = max(minScaleY, size.height / (radius * 2f) * scrimProgress)

                    if (!(radius > 0)) return@drawBehind

                    scale(scaleX = 1f, scaleY = scaleY, pivot = Offset(0f, size.height)) {
                        val leftGradientCenter =
                            Offset(size.width / 2 + sidePaddingPx, size.height - radius)
                        val rightGradientCenter =
                            Offset(size.width / 2 - sidePaddingPx, size.height - radius)
                        val leftBrush =
                            Brush.radialGradient(
                                colors =
                                    listOf(leftGradientColor, leftGradientColor.copy(alpha = 0f)),
                                center = rightGradientCenter,
                                radius = radius,
                            )
                        val rightBrush =
                            Brush.radialGradient(
                                colors =
                                    listOf(rightGradientColor, rightGradientColor.copy(alpha = 0f)),
                                center = leftGradientCenter,
                                radius = radius,
                            )
                        drawCircle(
                            brush = rightBrush,
                            alpha = smartScrimAlpha,
                            radius = radius,
                            center = leftGradientCenter,
                        )
                        drawCircle(
                            brush = leftBrush,
                            alpha = smartScrimAlpha,
                            radius = radius,
                            center = rightGradientCenter,
                        )
                    }
                }
                .padding(top = topPadding)
                .padding(padding),
        verticalArrangement = Arrangement.spacedBy(columnSpacing, Alignment.Bottom),
        horizontalAlignment = horizontalAlignment,
    ) {
@@ -164,11 +240,7 @@ fun ActionList(
            Chip(
                action = action,
                modifier =
                    Modifier.then(
                            if (index == actions.size - 1) Modifier.padding(bottom = 16.dp)
                            else Modifier
                        )
                        .onSizeChanged { childHeights[index] = it.height }
                    Modifier.onSizeChanged { childHeights[index] = it.height }
                        .graphicsLayer {
                            translationY = (1f - translation) * appxColumnY
                            scaleX = scale
+20 −25
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.systemui.ambientcue.ui.compose
import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
@@ -36,7 +37,6 @@ import androidx.compose.ui.layout.boundsInParent
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
import com.android.compose.windowsizeclass.calculateWindowSizeClass
@@ -47,9 +47,9 @@ import com.android.systemui.lifecycle.rememberViewModel

@Composable
fun AmbientCueContainer(
    modifier: Modifier = Modifier,
    ambientCueViewModelFactory: AmbientCueViewModel.Factory,
    onShouldInterceptTouches: (Boolean, Rect?) -> Unit,
    modifier: Modifier = Modifier,
) {
    val viewModel = rememberViewModel("AmbientCueContainer") { ambientCueViewModelFactory.create() }

@@ -118,36 +118,30 @@ private fun TaskBarAnd3ButtonAmbientCue(
) {
    val configuration = LocalConfiguration.current
    val density = LocalDensity.current
    val actionsVerticalPaddingPx = with(density) { SHORT_PILL_ACTIONS_VERTICAL_PADDING.dp.toPx() }
    val actionsHorizontalPaddingPx = with(density) { ACTIONS_HORIZONTAL_PADDING.dp.toPx() }
    val portrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT
    var pillCenter by remember { mutableStateOf(Offset.Zero) }
    val screenHeightPx = with(density) { configuration.screenHeightDp.dp.toPx() }
    val screenWidthPx = with(density) { configuration.screenWidthDp.dp.toPx() }
    var touchableRegion by remember { mutableStateOf<Rect?>(null) }
    LaunchedEffect(expanded, touchableRegion) {
        onShouldInterceptTouches(true, if (expanded) null else touchableRegion)
    }
    BackgroundGlow(
        visible = visible,
        expanded = expanded,
        collapsedOffset = IntOffset(0, 110),
        modifier =
            modifier.graphicsLayer {
                translationX = -size.width / 2 + pillCenter.x
                translationY = screenHeightPx - size.height
            },
    )
    ActionList(
        actions = actions,
        visible = visible && expanded,
        onDismiss = { viewModel.collapse() },
        visible = visible,
        expanded = expanded,
        horizontalAlignment = Alignment.End,
        onDismiss = { viewModel.collapse() },
        modifier =
            modifier.graphicsLayer {
                translationX = screenWidthPx - size.width - actionsHorizontalPaddingPx
                translationY = pillCenter.y - size.height - actionsVerticalPaddingPx
                translationX = screenWidthPx - size.width
                translationY = pillCenter.y - size.height
            },
        padding =
            PaddingValues(
                start = ACTIONS_HORIZONTAL_PADDING.dp,
                end = ACTIONS_HORIZONTAL_PADDING.dp,
                bottom = SHORT_PILL_ACTIONS_VERTICAL_PADDING.dp,
            ),
    )
    ShortPill(
        actions = actions,
@@ -202,13 +196,14 @@ private fun NavBarAmbientCue(
        else NAV_BAR_PILL_LARGE_WIDTH_DP.dp

    LaunchedEffect(expanded) { onShouldInterceptTouches(expanded, null) }
    BackgroundGlow(visible = visible, expanded = expanded, modifier = modifier)
    ActionList(
        actions = actions,
        visible = visible && expanded,
        visible = visible,
        expanded = expanded,
        onDismiss = { viewModel.collapse() },
        modifier =
            modifier.padding(
        modifier = modifier,
        padding =
            PaddingValues(
                bottom = NAV_BAR_ACTIONS_PADDING.dp,
                start = ACTIONS_HORIZONTAL_PADDING.dp,
                end = ACTIONS_HORIZONTAL_PADDING.dp,
@@ -219,7 +214,7 @@ private fun NavBarAmbientCue(
        navBarWidth = navBarWidth,
        visible = visible,
        expanded = expanded,
        modifier = modifier.padding(bottom = 4.dp),
        modifier = modifier,
        onClick = { viewModel.expand() },
        onCloseClick = { viewModel.hide() },
    )
@@ -232,5 +227,5 @@ private const val NAV_BAR_PILL_LARGE_WIDTH_DP = NAV_BAR_LARGE_WIDTH_DP + 4

private const val NAV_BAR_HEIGHT_DP = 24 // R.dimen.taskbar_stashed_size from Launcher
private const val SHORT_PILL_ACTIONS_VERTICAL_PADDING = 38
private const val NAV_BAR_ACTIONS_PADDING = NAV_BAR_HEIGHT_DP + 12
private const val NAV_BAR_ACTIONS_PADDING = NAV_BAR_HEIGHT_DP + 16
private const val ACTIONS_HORIZONTAL_PADDING = 32
+0 −113
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.ambientcue.ui.compose

import android.graphics.RuntimeShader
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.animateIntOffsetAsState
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.rememberTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.android.systemui.ambientcue.ui.shader.BackgroundGlowShader
import com.android.systemui.ambientcue.ui.utils.AiColorUtils.boostChroma

@Composable
fun BackgroundGlow(
    visible: Boolean,
    expanded: Boolean,
    collapsedOffset: IntOffset = IntOffset(0, 110),
    modifier: Modifier,
) {
    val density = LocalDensity.current
    val turbulenceDisplacementPx = with(density) { Defaults.TURBULENCE_DISPLACEMENT_DP.dp.toPx() }
    val gradientRadiusPx = with(density) { Defaults.GRADIENT_RADIUS.dp.toPx() }
    val glowHeightInDp = (Defaults.GRADIENT_RADIUS + Defaults.TURBULENCE_SIZE).dp

    val visibleState = remember { MutableTransitionState(false) }
    visibleState.targetState = visible

    val transition = rememberTransition(visibleState)
    val alpha by transition.animateFloat(transitionSpec = { tween(750) }) { if (it) 0.55f else 0f }
    val verticalOffset by
        animateIntOffsetAsState(if (expanded) IntOffset.Zero else collapsedOffset, tween(350))

    // Infinite animation responsible for the "vapor" effect distorting the radial gradient
    val infiniteTransition = rememberInfiniteTransition(label = "backgroundGlow")
    val turbulencePhase by
        infiniteTransition.animateFloat(
            initialValue = 1f,
            targetValue = 10f,
            animationSpec =
                infiniteRepeatable(
                    animation = tween(Defaults.ONE_MINUTE_MS, easing = LinearEasing),
                    repeatMode = RepeatMode.Reverse,
                ),
            label = "turbulencePhase",
        )

    val color1 = Color(boostChroma(MaterialTheme.colorScheme.secondaryContainer.toArgb()))
    val color2 = Color(boostChroma(MaterialTheme.colorScheme.primary.toArgb()))
    val color3 = Color(boostChroma(MaterialTheme.colorScheme.tertiary.toArgb()))

    val shader = remember { RuntimeShader(BackgroundGlowShader.FRAG_SHADER) }
    val shaderBrush = remember { ShaderBrush(shader) }

    Box(
        modifier.size(400.dp, glowHeightInDp).alpha(alpha).drawWithCache {
            onDrawWithContent {
                val offsetX = with(density) { verticalOffset.x.dp.toPx() }
                val offsetY = with(density) { verticalOffset.y.dp.toPx() }
                shader.setFloatUniform("resolution", size.width, size.height)
                shader.setColorUniform("color1", color1.toArgb())
                shader.setColorUniform("color2", color2.toArgb())
                shader.setColorUniform("color3", color3.toArgb())
                shader.setFloatUniform("origin", size.width / 2 + offsetX, size.height + offsetY)
                shader.setFloatUniform("radius", gradientRadiusPx)
                shader.setFloatUniform("turbulenceAmount", turbulenceDisplacementPx)
                shader.setFloatUniform("turbulencePhase", turbulencePhase)
                shader.setFloatUniform("turbulenceSize", Defaults.TURBULENCE_SIZE)
                drawRect(shaderBrush)
            }
        }
    )
}

private object Defaults {
    const val TURBULENCE_SIZE = 4.7f
    const val TURBULENCE_DISPLACEMENT_DP = 30
    const val GRADIENT_RADIUS = 200
    const val ONE_MINUTE_MS = 60 * 1000
}
+2 −0
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
@@ -55,6 +56,7 @@ fun Chip(action: ActionViewModel, modifier: Modifier = Modifier) {
                .clip(RoundedCornerShape(24.dp))
                .background(backgroundColor)
                .defaultMinSize(minHeight = 48.dp)
                .widthIn(max = 288.dp)
                .combinedClickable(onClick = action.onClick, onLongClick = action.onLongClick)
                .padding(start = 12.dp, end = 16.dp, top = 4.dp, bottom = 4.dp),
    ) {
+79 −22

File changed.

Preview size limit exceeded, changes collapsed.

Loading