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

Commit 3ac3d7b9 authored by Evan Laird's avatar Evan Laird
Browse files

[battery] batteries are like onions

They have layers

This CL does a few things:
- update the battery colors:
  - new green, yellow, red
  - low-alpha for the percentage numbers
  - the non-filled portion of the light-mode icons is bumped down to 20%
    alpha from 35%
- Updated the number glyphs and attribution glyphs
- The battery is now 13sp tall and 26.5sp wide
- Attributions are drawn as a layer on top of the battery, with 20%
  overlap
- The battery cap only draws if there is no attribution
- The percentage will show even at 100%
- The battery composable is drawn as a Layout so that positioning can
  take place without recomposition
- Recomposition will not take place when the level changes

A note on testing: I added logs to the layout, composition, and drawing
stages of the compose pipeline, and manually verified the cases when
recomposition takes place. This is not the most robust way to validate
the performance, but it is a relatively reliable metric for figuring out
when we are doing too much work.

This composable will not recompose or relayout when the color or levels
change, and should only recompose when the battery state changes. This
is necessary since the width will also change in those cases.

Test: screenshot tests
Test: BatteryViewModelTest
Test: manually via `adb shell cmd battery` commands
Bug: 404930547
Flag: com.android.settingslib.flags.new_status_bar_icons
Flag: com.android.systemui.status_bar_root_modernization
Change-Id: I766fb04d296a3ec1b71cc3f92f6846a5a4d573bb
parent 4637a403
Loading
Loading
Loading
Loading
+16 −26
Original line number Diff line number Diff line
@@ -24,9 +24,9 @@ import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -43,9 +43,8 @@ import com.android.systemui.statusbar.core.RudimentaryBattery
import com.android.systemui.statusbar.events.BackgroundAnimatableView
import com.android.systemui.statusbar.pipeline.battery.domain.interactor.BatteryInteractor
import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryColors
import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryFrame
import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryGlyph
import com.android.systemui.statusbar.pipeline.battery.ui.composable.BatteryCanvas
import com.android.systemui.statusbar.pipeline.battery.ui.composable.BatteryLayout
import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel
import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.UnifiedBatteryViewModel.Companion.glyphRepresentation
import java.text.NumberFormat
@@ -105,20 +104,15 @@ constructor(level: Int, context: Context, attrs: AttributeSet? = null) :
private fun UnifiedBatteryChip(level: Int) {
    val isFull = BatteryInteractor.isBatteryFull(level)
    val height = with(LocalDensity.current) { BatteryViewModel.STATUS_BAR_BATTERY_HEIGHT.toDp() }
    BatteryCanvas(
        modifier = Modifier.height(height).aspectRatio(BatteryViewModel.ASPECT_RATIO),
        path = BatteryFrame.pathSpec,
    BatteryLayout(
        attribution = BatteryGlyph.Bolt, // Always charging
        levelProvider = { level },
        isFullProvider = { isFull },
        glyphsProvider = { level.glyphRepresentation() },
        colorsProvider = { BatteryColors.DarkTheme.Charging },
        modifier = Modifier.height(height).wrapContentWidth(),
        // TODO(b/394659067): get a content description for this chip
        contentDescription = "",
        innerWidth = BatteryFrame.innerWidth,
        innerHeight = BatteryFrame.innerHeight,
        // This event only happens when plugged in, so we always show it as charging
        glyphs =
            if (isFull) listOf(BatteryGlyph.Bolt)
            else level.glyphRepresentation() + BatteryGlyph.Bolt,
        level = level,
        isFull = isFull,
        colorsProvider = { BatteryColors.DarkTheme.Charging },
    )
}

@@ -128,18 +122,14 @@ private fun BatteryAndPercentChip(level: Int) {
    val isFull = BatteryInteractor.isBatteryFull(level)
    val height = with(LocalDensity.current) { BatteryViewModel.STATUS_BAR_BATTERY_HEIGHT.toDp() }
    Row(verticalAlignment = Alignment.CenterVertically) {
        BatteryCanvas(
            modifier = Modifier.height(height).aspectRatio(BatteryViewModel.ASPECT_RATIO),
            path = BatteryFrame.pathSpec,
            // TODO(b/394659067): get a content description for this chip
            contentDescription = "",
            innerWidth = BatteryFrame.innerWidth,
            innerHeight = BatteryFrame.innerHeight,
            // This event only happens when plugged in, so we always show it as charging
            glyphs = listOf(BatteryGlyph.Bolt),
            level = level,
            isFull = isFull,
        BatteryLayout(
            attribution = BatteryGlyph.Bolt, // Always charging
            levelProvider = { level },
            isFullProvider = { isFull },
            glyphsProvider = { emptyList() },
            colorsProvider = { BatteryColors.DarkTheme.Charging },
            modifier = Modifier.height(height).wrapContentWidth(),
            contentDescription = "",
        )
        Spacer(modifier = Modifier.width(4.dp))
        Text(
+13 −13
Original line number Diff line number Diff line
@@ -38,6 +38,9 @@ sealed interface BatteryColors {
    /** Background color suitable for providing contrast with the [glyph] color */
    val backgroundWithGlyph: Color

    /** The layered attribution. Should match the tint of other status bar icons */
    val attribution: Color

    /**
     * Light theme: light background, dark icons
     *
@@ -47,11 +50,13 @@ sealed interface BatteryColors {
     * thus allow for higher contrast with the darker [glyph] colors.
     */
    sealed class LightTheme : BatteryColors {
        override val attribution = Color.Black
        override val glyph = Color.Black.copy(alpha = 0.75f)
        override val backgroundOnly = lowAlphaBg
        override val backgroundWithGlyph = lowAlphaBg

        data object Default : LightTheme() {
            override val glyph = Color.White
            override val glyph = Color.White.copy(alpha = 0.9f)
            override val fill = Color.Black

            /** Use a higher opacity here because the foreground is white */
@@ -59,22 +64,19 @@ sealed interface BatteryColors {
        }

        data object Charging : LightTheme() {
            override val glyph = Color(0xFF162100)
            override val fill = Color(0xFFB4FF1E)
            override val fill = Color(0xFF18CC47)
        }

        data object Error : LightTheme() {
            override val glyph = Color(0xFF3A0907)
            override val fill = Color(0xFFFF0101)
            override val fill = Color(0xFFFF0E01)
        }

        data object PowerSave : LightTheme() {
            override val glyph = Color(0xFF2F1400)
            override val fill = Color(0xFFFFC917)
        }

        companion object {
            private val lowAlphaBg = Color.Black.copy(alpha = 0.30f)
            private val lowAlphaBg = Color.Black.copy(alpha = 0.20f)
            private val highAlphaBg = Color.Black.copy(alpha = 0.55f)
        }
    }
@@ -88,26 +90,24 @@ sealed interface BatteryColors {
     * contrast.
     */
    sealed class DarkTheme : BatteryColors {
        override val attribution = Color.White
        override val backgroundOnly = lowAlphaBg
        override val backgroundWithGlyph = highAlphaBg
        override val glyph = Color.Black.copy(alpha = 0.75f)

        data object Default : DarkTheme() {
            override val glyph = Color.Black
            override val fill = Color.White
        }

        data object Charging : DarkTheme() {
            override val glyph = Color(0xFF162100)
            override val fill = Color(0xFFB4FF1E)
            override val fill = Color(0xFF18CC47)
        }

        data object Error : DarkTheme() {
            override val glyph = Color(0xFF3A0907)
            override val fill = Color(0xFFFF0101)
            override val fill = Color(0xFFFF0E01)
        }

        data object PowerSave : DarkTheme() {
            override val glyph = Color(0xFF2F1400)
            override val fill = Color(0xFFFFC917)
        }

+39 −2
Original line number Diff line number Diff line
@@ -16,6 +16,10 @@

package com.android.systemui.statusbar.pipeline.battery.shared.ui

import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.RoundRect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.addSvg
import androidx.compose.ui.unit.Dp
@@ -36,11 +40,41 @@ object BatteryFrame {
            viewportWidth = 21.dp,
        )

    val bodyPathSpec: PathSpec =
        PathSpec(
            path =
                Path().apply {
                    addRoundRect(
                        RoundRect(
                            rect = Rect(0f, 0f, 24f, 13f),
                            topLeft = CornerRadius(4f),
                            topRight = CornerRadius(4f),
                            bottomRight = CornerRadius(4f),
                            bottomLeft = CornerRadius(4f),
                        )
                    )
                },
            viewportWidth = 24.dp,
            viewportHeight = 13.dp,
        )

    val capPathSpec: PathSpec =
        PathSpec(
            path =
                Path().apply {
                    addSvg(
                        "M0.3333,-0.0037L0,-0.0037L0,6.0234L0.3333,6.0234C0.9777,6.0234 1.5,5.5011 1.5,4.8567L1.5,1.163C1.5,0.5187 0.9777,-0.0037 0.3333,-0.0037Z"
                    )
                },
            viewportWidth = 1.5.dp,
            viewportHeight = 6.03.dp,
        )

    /** The width of the drawable that is usable for inside elements */
    const val innerWidth = 19.5f
    const val innerWidth = 24f

    /** The height of the drawable that is usable for inside elements */
    const val innerHeight = 12f
    const val innerHeight = 13f
}

/**
@@ -57,4 +91,7 @@ data class PathSpec(val path: Path, val viewportWidth: Dp, val viewportHeight: D

        return min(xScale, yScale)
    }

    fun scaledSize(scale: Float): Size =
        Size(width = viewportWidth.value * scale, height = viewportHeight.value * scale)
}
+74 −136

File changed.

Preview size limit exceeded, changes collapsed.

+2 −2
Original line number Diff line number Diff line
@@ -16,8 +16,8 @@

package com.android.systemui.statusbar.pipeline.battery.ui.binder

import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
@@ -62,7 +62,7 @@ object UnifiedBatteryViewBinder {
                            UnifiedBattery(
                                modifier =
                                    Modifier.height(height)
                                        .aspectRatio(BatteryViewModel.ASPECT_RATIO)
                                        .wrapContentWidth()
                                        .sysuiResTag(BatteryViewModel.TEST_TAG),
                                viewModelFactory = viewModelFactory,
                                isDarkProvider = { isDark },
Loading