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

Commit 1d312ed1 authored by Evan Laird's avatar Evan Laird
Browse files

[Battery] New icon updates

- Changed the BatteryFillDrawable to support dynamic inset between the
  fill and the frame. This amounts to calculating the drawing insets
  when the value changes, and storing local references to the non-scaled
  values so that we can update them.
- Dynamic fill is now set to (if attribution-showing?
                               0
                               1.5)
- Upgraded `showErrorState` to a `ColorProfile` type
- BatteryMeterView now calculates the color profile for any given state.
- Battery fill is now the only thing we color for different profiles.
  Everything else is using the foreground color
- Lastly, reduced the height of the status bar battery from 14sp ->
  12sp. This yields some gnarly decimal width, but such is life

Test: manual using `adb shell cmd battery` and subcommands `unplug`,
`set level N`.
Test: manual by setting battery saver mode, plugging / unplugging the
device
Test: go/battery-2024-companion
Bug: 330707481
Flag: ACONFIG com.android.settingslib.flags.new_status_bar_icons TEAMFOOD

Change-Id: Idd651a023fee4a7c796c46d5990f7a90df92dc76
parent 925ff714
Loading
Loading
Loading
Loading
+3 −2
Original line number Diff line number Diff line
@@ -164,8 +164,9 @@
    so the width of the icon should be 13.0sp * (12.0 / 20.0) -->
    <dimen name="status_bar_battery_icon_width">7.8sp</dimen>

    <dimen name="status_bar_battery_unified_icon_width">24sp</dimen>
    <dimen name="status_bar_battery_unified_icon_height">14sp</dimen>
    <!-- Original canvas is 24x14. These dimens reflect that ratio, with 12sp height instead  -->
    <dimen name="status_bar_battery_unified_icon_width">20.6sp</dimen>
    <dimen name="status_bar_battery_unified_icon_height">12sp</dimen>

    <!-- The battery icon is 13sp tall, but the other system icons are 15sp tall (see
         @*android:dimen/status_bar_system_icon_size) with some top and bottom padding embedded in
+35 −5
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import android.animation.ObjectAnimator;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
@@ -52,6 +53,7 @@ import com.android.systemui.DualToneHandler;
import com.android.systemui.battery.unified.BatteryColors;
import com.android.systemui.battery.unified.BatteryDrawableState;
import com.android.systemui.battery.unified.BatteryLayersDrawable;
import com.android.systemui.battery.unified.ColorProfile;
import com.android.systemui.plugins.DarkIconDispatcher;
import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
import com.android.systemui.res.R;
@@ -252,7 +254,7 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver {
                    new BatteryDrawableState(
                            level,
                            mUnifiedBatteryState.getShowPercent(),
                            level <= 20,
                            getCurrentColorProfile(),
                            attr
                    );

@@ -261,6 +263,7 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver {
    }

    // Potentially reloads any attribution. Should not be called if the state hasn't changed
    @SuppressLint("UseCompatLoadingForDrawables")
    private Drawable getBatteryAttribution(boolean isCharging) {
        if (!newStatusBarIcons()) return null;

@@ -281,6 +284,30 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver {
        return attr;
    }

    /** Calculate the appropriate color for the current state */
    private ColorProfile getCurrentColorProfile() {
        return getColorProfile(
                mPowerSaveEnabled,
                mIsBatteryDefender && mDisplayShieldEnabled,
                mPluggedIn,
                mLevel <= 20);
    }

    /** pure function to compute the correct color profile for our battery icon */
    private ColorProfile getColorProfile(
            boolean isPowerSave,
            boolean isBatteryDefender,
            boolean isCharging,
            boolean isLowBattery
    ) {
        if (isCharging)  return ColorProfile.Active;
        if (isPowerSave) return ColorProfile.Warning;
        if (isBatteryDefender) return ColorProfile.None;
        if (isLowBattery) return ColorProfile.Error;

        return ColorProfile.None;
    }

    void onPowerSaveChanged(boolean isPowerSave) {
        if (isPowerSave == mPowerSaveEnabled) {
            return;
@@ -293,7 +320,7 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver {
                    new BatteryDrawableState(
                            mUnifiedBatteryState.getLevel(),
                            mUnifiedBatteryState.getShowPercent(),
                            mUnifiedBatteryState.getShowErrorState(),
                            getCurrentColorProfile(),
                            getBatteryAttribution(isCharging())
                    )
            );
@@ -318,7 +345,7 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver {
                    new BatteryDrawableState(
                            mUnifiedBatteryState.getLevel(),
                            mUnifiedBatteryState.getShowPercent(),
                            mUnifiedBatteryState.getShowErrorState(),
                            getCurrentColorProfile(),
                            getBatteryAttribution(isCharging())
                    )
            );
@@ -334,7 +361,7 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver {
                        new BatteryDrawableState(
                                mUnifiedBatteryState.getLevel(),
                                mUnifiedBatteryState.getShowPercent(),
                                mUnifiedBatteryState.getShowErrorState(),
                                getCurrentColorProfile(),
                                getBatteryAttribution(isCharging())
                        )
                );
@@ -522,7 +549,7 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver {
                new BatteryDrawableState(
                        mUnifiedBatteryState.getLevel(),
                        shouldShow,
                        mUnifiedBatteryState.getShowErrorState(),
                        mUnifiedBatteryState.getColor(),
                        mUnifiedBatteryState.getAttribution()
                )
        );
@@ -755,6 +782,9 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver {
        pw.println("    mPluggedIn: " + mPluggedIn);
        pw.println("    mLevel: " + mLevel);
        pw.println("    mMode: " + mShowPercentMode);
        if (newStatusBarIcons()) {
            pw.println("    mUnifiedBatteryState: " + mUnifiedBatteryState);
        }
    }

    @VisibleForTesting
+35 −23
Original line number Diff line number Diff line
@@ -19,6 +19,21 @@ package com.android.systemui.battery.unified
import android.graphics.Color
import android.graphics.drawable.Drawable

/**
 * States that might set a color profile (e.g., red for low battery) and are mutually exclusive.
 * This enum allows us to address which colors we want to use based on their function.
 */
enum class ColorProfile {
    // Grayscale is the default color
    None,
    // Green for e.g., charging
    Active,
    // Yellow for e.g., battery saver
    Warning,
    // Red for e.t., low battery
    Error,
}

/**
 * Encapsulates all drawing information needed by BatteryMeterDrawable to render properly. Rendered
 * state will be equivalent to the most recent state passed in.
@@ -28,12 +43,9 @@ data class BatteryDrawableState(
    val level: Int,
    /** Whether or not to render the percent as a foreground text layer */
    val showPercent: Boolean,
    /**
     * In an error state, the drawable will use the error colors and removes the third layer. If
     * [showPercent] is false, then the fill will be rendered in the foreground error color. Else
     * the fill is not rendered.
     */
    val showErrorState: Boolean,

    /** Set the [ColorProfile] to get the appropriate fill colors */
    val color: ColorProfile = ColorProfile.None,

    /**
     * An attribution is a drawable that shows either alongside the percent, or centered in the
@@ -59,7 +71,6 @@ data class BatteryDrawableState(
            BatteryDrawableState(
                level = 50,
                showPercent = false,
                showErrorState = false,
                attribution = null,
            )
    }
@@ -82,12 +93,14 @@ sealed interface BatteryColors {
     */
    val fillOnly: Int

    /** Error colors are used for low battery states typically */
    val errorForeground: Int
    val errorBackground: Int
    /** Used when charging */
    val activeFill: Int

    /** Currently unused */
    val warnBackground: Int
    /** Warning color is used for battery saver mode */
    val warnFill: Int

    /** Error colors are used for low battery states typically */
    val errorFill: Int

    /** Color scheme appropriate for light mode (dark icons) */
    data object LightThemeColors : BatteryColors {
@@ -100,13 +113,12 @@ sealed interface BatteryColors {
        // GM Gray 700
        override val fillOnly = Color.parseColor("#5F6368")

        // GM Red 600
        override val errorForeground = Color.parseColor("#D93025")
        // GM Red 100
        override val errorBackground = Color.parseColor("#FAD2CF")

        // GM Green 700
        override val activeFill = Color.parseColor("#188038")
        // GM Yellow 500
        override val warnBackground = Color.parseColor("#FBBC04")
        override val warnFill = Color.parseColor("#FBBC04")
        // GM Red 600
        override val errorFill = Color.parseColor("#D93025")
    }

    /** Color scheme appropriate for dark mode (light icons) */
@@ -120,12 +132,12 @@ sealed interface BatteryColors {
        // GM Gray 400
        override val fillOnly = Color.parseColor("#BDC1C6")

        // GM Red 600
        override val errorForeground = Color.parseColor("#D93025")
        // GM Red 200
        override val errorBackground = Color.parseColor("#F6AEA9")
        // GM Green 500
        override val activeFill = Color.parseColor("#34A853")
        // GM Yellow
        override val warnBackground = Color.parseColor("#FBBC04")
        override val warnFill = Color.parseColor("#FBBC04")
        // GM Red 600
        override val errorFill = Color.parseColor("#D93025")
    }

    companion object {
+49 −19
Original line number Diff line number Diff line
@@ -44,6 +44,29 @@ class BatteryFillDrawable(private val framePath: Path) : Drawable() {
    private var scaledLeftOffset = 0f
    private var scaledRightInset = 0f

    /** Scale this to the viewport so we fill correctly! */
    private val fillRectNotScaled = RectF()
    private var leftInsetNotScaled = 0f
    private var rightInsetNotScaled = 0f

    /**
     * Configure how much space between the battery frame (drawn at 1.5dp stroke width) and the
     * inner fill. This is accomplished by tracing the exact same path as the frame, but using
     * [BlendMode.CLEAR] as the blend mode.
     *
     * This value also affects the overall width of the fill, so it requires us to re-draw
     * everything
     */
    var fillInsetAmount = -1f
        set(value) {
            if (field != value) {
                field = value
                updateInsets()
                updateScale()
                invalidateSelf()
            }
        }

    // Drawable.level cannot be overloaded
    var batteryLevel = 0
        set(value) {
@@ -87,15 +110,32 @@ class BatteryFillDrawable(private val framePath: Path) : Drawable() {
        updateScale()
    }

    /**
     * To support dynamic insets, we have to keep mutable references to the left/right unscaled
     * insets, as well as the fill rect.
     */
    private fun updateInsets() {
        leftInsetNotScaled = LeftFillOffsetExcludingPadding + fillInsetAmount
        rightInsetNotScaled = RightFillInsetExcludingPadding + fillInsetAmount

        fillRectNotScaled.set(
            leftInsetNotScaled,
            0f,
            Metrics.ViewportWidth - rightInsetNotScaled,
            Metrics.ViewportHeight
        )
    }

    private fun updateScale() {
        framePath.transform(/* matrix = */ scaleMatrix, /* dst = */ scaledPath)
        scaleMatrix.mapRect(/* dst = */ scaledFillRect, /* src = */ FillRect)
        scaleMatrix.mapRect(/* dst = */ scaledFillRect, /* src = */ fillRectNotScaled)

        scaledLeftOffset = LeftFillOffset * hScale
        scaledRightInset = RightFillInset * hScale
        scaledLeftOffset = leftInsetNotScaled * hScale
        scaledRightInset = rightInsetNotScaled * hScale

        // Ensure 0.5dp space between the frame stroke and the fill
        clearPaint.strokeWidth = 2.5f * hScale
        // stroke width = 1.5 (same as the outer frame) + 2x fillInsetAmount, since N px of padding
        // requires the entire stroke to be 2N px wider
        clearPaint.strokeWidth = (1.5f + 2 * fillInsetAmount) * hScale
    }

    override fun draw(canvas: Canvas) {
@@ -157,23 +197,13 @@ class BatteryFillDrawable(private val framePath: Path) : Drawable() {
    override fun setAlpha(alpha: Int) {}

    companion object {
        // 4f =
        // 3.5f =
        //       2.75 (left-most edge of the frame path)
        //     + 0.75 (1/2 of the stroke width)
        //     + 0.5  (padding between stroke and fill edge)
        private const val LeftFillOffset = 4f
        private const val LeftFillOffsetExcludingPadding = 3.5f

        // 2, calculated the same way, but from the right edge (without the battery cap), which
        // 1.5, calculated the same way, but from the right edge (without the battery cap), which
        // consumes 2 units of width.
        private const val RightFillInset = 2f

        /** Scale this to the viewport so we fill correctly! */
        private val FillRect =
            RectF(
                LeftFillOffset,
                0f,
                Metrics.ViewportWidth - RightFillInset,
                Metrics.ViewportHeight
            )
        private const val RightFillInsetExcludingPadding = 1.5f
    }
}
+44 −34
Original line number Diff line number Diff line
@@ -56,9 +56,6 @@ import kotlin.math.roundToInt
 *          - The internal space is divided into 12x10 and 6x6 rectangles
 *          - The attribution is aligned left
 *          - The percent text is scaled based on the number of characters (1,2, or 3) in the string
 *
 * When [BatteryDrawableState.showErrorState] is true, we will only show either the percent text OR
 * the battery fill, in order to maximize contrast when using the error colors.
 */
@Suppress("RtlHardcoded")
class BatteryLayersDrawable(
@@ -91,7 +88,7 @@ class BatteryLayersDrawable(
    var colors: BatteryColors = BatteryColors.LightThemeColors
        set(value) {
            field = value
            updateColors(batteryState.showErrorState, value)
            updateColorProfile(batteryState.hasForegroundContent(), batteryState.color, value)
        }

    init {
@@ -101,51 +98,64 @@ class BatteryLayersDrawable(
    }

    private fun handleUpdateState(old: BatteryDrawableState, new: BatteryDrawableState) {
        if (new.showErrorState != old.showErrorState) {
            updateColors(new.showErrorState, colors)
        }

        if (new.level != old.level) {
            fill.batteryLevel = new.level
            textOnly.batteryLevel = new.level
            spaceSharingText.batteryLevel = new.level
        }

        val shouldUpdateColors =
            new.color != old.color ||
                new.attribution != attribution.drawable ||
                new.hasForegroundContent() != old.hasForegroundContent()

        if (new.attribution != null && new.attribution != attribution.drawable) {
            attribution.drawable = new.attribution
            updateColors(new.showErrorState, colors)
        }

        if (new.hasForegroundContent() != old.hasForegroundContent()) {
            setFillColor(new.hasForegroundContent(), new.showErrorState, colors)
        }
            setFillInsets(new.hasForegroundContent())
        }

    /** In error states, we don't draw fill unless there is no foreground content (e.g., percent) */
    private fun updateColors(showErrorState: Boolean, colorInfo: BatteryColors) {
        frameBg.setTint(if (showErrorState) colorInfo.errorBackground else colorInfo.bg)
        frame.setTint(colorInfo.fg)
        attribution.setTint(if (showErrorState) colorInfo.errorForeground else colorInfo.fg)
        textOnly.setTint(if (showErrorState) colorInfo.errorForeground else colorInfo.fg)
        spaceSharingText.setTint(if (showErrorState) colorInfo.errorForeground else colorInfo.fg)
        setFillColor(batteryState.hasForegroundContent(), showErrorState, colorInfo)
        // Finally, update colors last if any of the above conditions were met, so that everything
        // is properly tinted
        if (shouldUpdateColors) {
            updateColorProfile(new.hasForegroundContent(), new.color, colors)
        }
    }

    /**
     * If there is a foreground layer, then we draw the fill with the low opacity
     * [BatteryColors.fill] color. Otherwise, if there is no other foreground layer, we will use
     * either the error or fillOnly colors for more contrast
     */
    private fun setFillColor(
    private fun updateColorProfile(
        hasFg: Boolean,
        error: Boolean,
        color: ColorProfile,
        colorInfo: BatteryColors,
    ) {
        if (hasFg) {
            fill.fillColor = colorInfo.fill
        } else {
            fill.fillColor = if (error) colorInfo.errorForeground else colorInfo.fillOnly
        frame.setTint(colorInfo.fg)
        frameBg.setTint(colorInfo.bg)
        textOnly.setTint(colorInfo.fg)
        spaceSharingText.setTint(colorInfo.fg)
        attribution.setTint(colorInfo.fg)

        when (color) {
            ColorProfile.None -> {
                fill.fillColor = if (hasFg) colorInfo.fill else colorInfo.fillOnly
            }
            ColorProfile.Active -> {
                fill.fillColor = colorInfo.activeFill
            }
            ColorProfile.Warning -> {
                fill.fillColor = colorInfo.warnFill
            }
            ColorProfile.Error -> {
                fill.fillColor = colorInfo.errorFill
            }
        }
    }

    private fun setFillInsets(
        hasFg: Boolean,
    ) {
        // Extra padding around the fill if there is nothing in the foreground
        fill.fillInsetAmount = if (hasFg) 0f else 1.5f
    }

    override fun onBoundsChange(bounds: Rect) {
@@ -200,10 +210,9 @@ class BatteryLayersDrawable(
        // 2. Then the frame itself
        frame.draw(canvas)

        // 3. Fill it the appropriate amount if non-error state or error + no attribute
        if (!batteryState.showErrorState || !batteryState.hasForegroundContent()) {
        // 3. Fill it the appropriate amount
        fill.draw(canvas)
        }

        // 4. Decide what goes inside
        if (batteryState.showPercent && batteryState.attribution != null) {
            // 4a. percent & attribution. Implies space-sharing
@@ -309,6 +318,7 @@ class BatteryLayersDrawable(
         *
         * See [BatteryDrawableState] for how to set the properties of the resulting class
         */
        @Suppress("UseCompatLoadingForDrawables")
        fun newBatteryDrawable(
            context: Context,
            initialState: BatteryDrawableState = BatteryDrawableState.DefaultInitialState,