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

Commit 9aa6ba33 authored by Lucas Dupin's avatar Lucas Dupin Committed by Android (Google) Code Review
Browse files

Merge "Add Overlay compose into Underlay directory. The design of underlay is...

Merge "Add Overlay compose into Underlay directory. The design of underlay is obselete. The Underlay folder will be renamed as Overlay in the future." into main
parents 592ace54 768c4449
Loading
Loading
Loading
Loading
+56 −0
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.underlay.ui.compose

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.android.systemui.underlay.ui.viewmodel.ActionViewModel

@Composable
fun ActionList(actions: List<ActionViewModel>, visible: Boolean, modifier: Modifier = Modifier) {
    val density = LocalDensity.current
    Column(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(8.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        actions.fastForEachIndexed { index, action ->
            val delay = 50 * (actions.size - index)
            AnimatedVisibility(
                visible = visible,
                enter =
                    slideInVertically(tween(450, delayMillis = delay)) {
                        with(density) { 15.dp.roundToPx() }
                    } + fadeIn(tween(450, delayMillis = delay)),
                exit = fadeOut(tween(250)),
            ) {
                Chip(action)
            }
        }
    }
}
+82 −0
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.underlay.ui.compose

import androidx.compose.animation.core.animateFloatAsState
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.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp
import androidx.core.graphics.ColorUtils

@Composable
fun BackgroundGlow(visible: Boolean, modifier: Modifier) {
    val alpha by animateFloatAsState(if (visible) 1f else 0f, animationSpec = tween(750))
    val blurScale = 1.3f

    val primaryBoosted = Color(boostChroma(MaterialTheme.colorScheme.primary.toArgb()))
    val primaryFixedBoosted = Color(boostChroma(MaterialTheme.colorScheme.primary.toArgb()))
    val tertiaryBoosted = Color(boostChroma(MaterialTheme.colorScheme.tertiaryContainer.toArgb()))

    val gradient1Brush =
        Brush.radialGradient(
            listOf(primaryFixedBoosted.copy(alpha = 0.3f), primaryFixedBoosted.copy(alpha = 0f))
        )
    val gradient2Brush =
        Brush.radialGradient(
            listOf(primaryBoosted.copy(alpha = 0.4f), primaryBoosted.copy(alpha = 0f))
        )
    val gradient3Brush =
        Brush.radialGradient(
            listOf(tertiaryBoosted.copy(alpha = 0.3f), tertiaryBoosted.copy(alpha = 0f))
        )

    // The glow is made of 3 radial gradients.
    // All gradients are in the same box to make it simpler to move them around
    Box(
        modifier.size(372.dp, 68.dp).alpha(alpha).drawBehind {
            scale(2.12f * blurScale, 1f) {
                translate(0f, size.height * 0.8f) { drawCircle(gradient1Brush) }
            }
            scale(4.59f * blurScale, 1f) {
                translate(0f, size.height * 0.45f) { drawOval(gradient2Brush) }
            }
            scale(2.41f * blurScale, 1f) { drawOval(gradient3Brush) }
        }
    )
}

private fun boostChroma(color: Int): Int {
    val outColor = FloatArray(3)
    ColorUtils.colorToM3HCT(color, outColor)
    val chroma = outColor[1]
    if (chroma <= 5) {
        return color
    }
    return ColorUtils.M3HCTToColor(outColor[0], 120f, outColor[2])
}
+73 −0
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.underlay.ui.compose

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.ColorFilter
import androidx.compose.ui.unit.dp
import com.android.compose.ui.graphics.painter.rememberDrawablePainter
import com.android.systemui.underlay.ui.viewmodel.ActionViewModel

@Composable
fun Chip(action: ActionViewModel, modifier: Modifier = Modifier) {
    val outlineColor = MaterialTheme.colorScheme.onBackground
    val backgroundColor = MaterialTheme.colorScheme.background

    Row(
        horizontalArrangement = Arrangement.spacedBy(4.dp),
        verticalAlignment = Alignment.CenterVertically,
        modifier =
            modifier
                .clip(RoundedCornerShape(24.dp))
                .background(backgroundColor)
                .clickable { action.onClick() }
                .padding(horizontal = 8.dp, vertical = 8.dp),
    ) {
        val painter = rememberDrawablePainter(action.icon)
        Image(
            painter = painter,
            colorFilter = if (action.attribution != null) ColorFilter.tint(outlineColor) else null,
            contentDescription = action.label,
            modifier = Modifier.size(24.dp).clip(CircleShape),
        )

        Text(action.label, style = MaterialTheme.typography.labelLarge, color = outlineColor)
        if (action.attribution != null) {
            Text(
                action.attribution,
                style = MaterialTheme.typography.labelLarge,
                color = outlineColor,
                modifier = Modifier.padding(start = 4.dp).alpha(0.4f),
            )
        }
    }
}
+138 −0
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.underlay.ui.compose

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
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.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import androidx.compose.ui.util.lerp
import com.android.compose.ui.graphics.painter.rememberDrawablePainter
import com.android.systemui.underlay.ui.viewmodel.ActionViewModel

@Composable
fun NavBarPill(
    actions: List<ActionViewModel>,
    navBarWidth: Dp,
    modifier: Modifier = Modifier,
    visible: Boolean = true,
    expanded: Boolean = false,
    onClick: () -> Unit = {},
) {
    val outlineColor = Color.White
    val backgroundColor = Color.Black

    val density = LocalDensity.current
    val collapsedWidthPx = with(density) { navBarWidth.toPx() }
    var expandedSize by remember { mutableStateOf(IntSize.Zero) }
    val enterProgress by
        animateFloatAsState(
            if (visible) 1f else 0f,
            animationSpec = tween(250, delayMillis = 200),
            label = "enter",
        )
    val expansionAlpha by
        animateFloatAsState(
            if (expanded) 0f else 1f,
            animationSpec = tween(250, delayMillis = 200),
            label = "expansion",
        )

    Box(
        modifier =
            modifier.graphicsLayer {
                alpha = enterProgress * expansionAlpha
                scaleY = enterProgress
                scaleX =
                    if (expandedSize.width != 0) {
                        val initialScale = collapsedWidthPx / expandedSize.width
                        lerp(initialScale, 1f, enterProgress)
                    } else {
                        1f
                    }
            }
    ) {
        Row(
            horizontalArrangement = Arrangement.spacedBy(4.dp),
            verticalAlignment = Alignment.CenterVertically,
            modifier =
                Modifier.clip(RoundedCornerShape(16.dp))
                    .border(2.dp, outlineColor, RoundedCornerShape(16.dp))
                    .background(backgroundColor)
                    .clickable { onClick() }
                    .padding(horizontal = 8.dp, vertical = 6.dp)
                    .onGloballyPositioned { expandedSize = it.size },
        ) {
            actions.fastForEachIndexed { _, action ->
                val painter = rememberDrawablePainter(action.icon)
                Image(
                    painter = painter,
                    colorFilter =
                        if (action.attribution != null) ColorFilter.tint(outlineColor) else null,
                    contentDescription = action.label,
                    modifier = Modifier.size(16.dp).clip(CircleShape),
                )
            }

            if (actions.size == 1 || (actions.isNotEmpty() && actions.last().attribution != null)) {
                val action = actions.last()
                Text(
                    action.label,
                    style = MaterialTheme.typography.labelSmall,
                    color = outlineColor,
                )
                if (action.attribution != null) {
                    Text(
                        action.attribution,
                        style = MaterialTheme.typography.labelSmall,
                        color = outlineColor,
                        modifier = Modifier.padding(start = 4.dp).alpha(0.4f),
                    )
                }
            }
        }
    }
}
+59 −0
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.underlay.ui.compose

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.underlay.ui.viewmodel.OverlayViewModel

@Composable
fun OverlayContainer(
    modifier: Modifier = Modifier,
    overlayViewModelFactory: OverlayViewModel.Factory,
) {
    val viewModel = rememberViewModel("OverlayContainer") { overlayViewModelFactory.create() }

    val visible = viewModel.isOverlayVisible
    val expanded = viewModel.isOverlayExpanded
    val actions = viewModel.actions

    // TODO: b/414507396 - Replace with the height of the navbar
    val chipsBottomPadding = 46.dp

    Box(modifier.clickable(expanded) { viewModel.collapse() }) {
        BackgroundGlow(visible, Modifier.align(Alignment.BottomCenter))
        NavBarPill(
            actions = actions,
            navBarWidth = 90.dp, // TODO: b/414507396 - Replace with the width of the navbar
            visible = visible,
            expanded = expanded,
            modifier = Modifier.align(Alignment.BottomCenter),
            onClick = { viewModel.expand() },
        )
        ActionList(
            actions = actions,
            visible = visible && expanded,
            modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = chipsBottomPadding),
        )
    }
}
Loading