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

Commit 2b9f786b authored by Evan Laird's avatar Evan Laird
Browse files

[Sb] New unified battery icon

This CL adds a new style of battery drawable, `BatteryLayersDrawable`,
which renders a battery meter via manual compositon and arrangement of
layers. The procss is similar to the existing ThemedBatteryDrawable,
where we use custom renering to achieve the correct UI encapsulated in a
drawable.

This new drawable employs the use of a view data class, `BatteryMeterState`, that can fully
describe the desired view state, and encapsulaes battery theme colors in
a `BatteryColors` class, of which there are light and dark variants.

Test: compile
Bug: 314812750
Flag: NONE
Change-Id: I62485c1797d0da680478e6d726f6d90df357ab1e
parent 7c7433ab
Loading
Loading
Loading
Loading
+34 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright (C) 2024 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.
  -->

<!-- Unified battery frame for BatteryLayersDrawable.kt -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="14dp"
    android:viewportWidth="24.0"
    android:viewportHeight="14.0">
    <!-- body -->
    <path
        android:pathData="@string/battery_unified_frame_path_string"
        android:strokeColor="#000"
        android:strokeWidth="1.5"
        />
    <!-- cap -->
    <path
        android:pathData="M0,4C0,3.448 0.448,3 1,3H1.5V11H1C0.448,11 0,10.552 0,10V4Z"
        android:fillColor="#000"/>
</vector>
+27 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright (C) 2024 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.
  -->

<!-- Vector description of the battery gutter: the background of the fill -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="14dp"
    android:viewportWidth="24.0"
    android:viewportHeight="14.0">
    <path
        android:pathData="@string/battery_unified_frame_path_string"
        android:fillColor="#fff" />
</vector>
+4 −0
Original line number Diff line number Diff line
@@ -34,6 +34,10 @@
        remaining [CHAR LIMIT=none]-->
    <string name="battery_low_percent_format"><xliff:g id="percentage">%s</xliff:g> remaining</string>

    <!-- SVG path description for the battery frame of the unified battery drawable
    (BatteryLayersDrawable.kt). Drawn on a 24x14 canvas. Not suitable outside in any other context -->
    <string name="battery_unified_frame_path_string" translatable="false">M2.75,3C2.75,1.757 3.757,0.75 5,0.75H20C21.795,0.75 23.25,2.205 23.25,4V10C23.25,11.795 21.795,13.25 20,13.25H5C3.757,13.25 2.75,12.243 2.75,11V3Z </string>

    <!-- A message that appears when the battery remaining estimate is low in a dialog.  This is
    appended to the subtitle of the low battery alert.  "percentage" is the percentage of battery
    remaining. "time" is the amount of time remaining before the phone runs out of battery [CHAR LIMIT=none]-->
+102 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.battery.unified

import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.graphics.drawable.DrawableWrapper
import android.view.Gravity
import kotlin.math.min
import kotlin.math.roundToInt

/**
 * A battery attribution is defined as a drawable that can display either alongside the percent text
 * or solely in the center of the battery frame.
 *
 * Attributions are given an explicit canvas of 18x8, or 6x6 depending on the display mode (centered
 * or right-aligned). The size is configured in [BatteryLayersDrawable] by changing this drawable
 * wrapper's bounds, and optionally setting the [gravity]
 */
@Suppress("RtlHardcoded")
class BatteryAttributionDrawable(dr: Drawable?) : DrawableWrapper(dr) {
    /** One of [CENTER, LEFT]. Note that RTL is handled in the parent */
    var gravity = Gravity.CENTER
        set(value) {
            field = value
            updateBoundsInner()
        }

    // Must be called if bounds change, gravity changes, or the wrapped drawable changes
    private fun updateBoundsInner() {
        val dr = drawable ?: return

        val hScale = bounds.width().toFloat() / dr.intrinsicWidth.toFloat()
        val vScale = bounds.height().toFloat() / dr.intrinsicHeight.toFloat()
        val scale = min(hScale, vScale)

        val dw = scale * dr.intrinsicWidth
        val dh = scale * dr.intrinsicHeight

        if (gravity == Gravity.CENTER) {
            val padLeft = (bounds.width() - dw) / 2
            val padTop = (bounds.height() - dh) / 2
            dr.setBounds(
                (bounds.left + padLeft).roundToInt(),
                (bounds.top + padTop).roundToInt(),
                (bounds.left + padLeft + dw).roundToInt(),
                (bounds.top + padTop + dh).roundToInt()
            )
        } else if (gravity == Gravity.LEFT) {
            dr.setBounds(
                bounds.left,
                bounds.top,
                (bounds.left + dw).roundToInt(),
                (bounds.top + dh).roundToInt()
            )
        }
    }

    override fun setDrawable(dr: Drawable?) {
        super.setDrawable(dr)
        updateBoundsInner()
    }

    override fun onBoundsChange(bounds: Rect) {
        updateBoundsInner()
    }

    /**
     * DrawableWrapper allows for a null constructor, but this method assumes that the drawable is
     * non-null. It is called by LayerDrawable on init, so we have to handle null here specifically
     */
    override fun getChangingConfigurations(): Int = drawable?.changingConfigurations ?: 0

    override fun draw(canvas: Canvas) {
        drawable?.draw(canvas)
    }

    // Deprecated, but needed for Drawable implementation
    override fun getOpacity() = PixelFormat.OPAQUE

    // We don't use this
    override fun setAlpha(alpha: Int) {}

    override fun setColorFilter(colorFilter: ColorFilter?) {}
}
+130 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.battery.unified

import android.graphics.Color
import android.graphics.drawable.Drawable

/**
 * Encapsulates all drawing information needed by BatteryMeterDrawable to render properly. Rendered
 * state will be equivalent to the most recent state passed in.
 */
data class BatteryDrawableState(
    /** [0-100] description of the battery level */
    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,

    /**
     * An attribution is a drawable that shows either alongside the percent, or centered in the
     * foreground of the overall drawable.
     *
     * When space sharing with the percent text, the default rect is 6x6, positioned directly next
     * to the percent and left-aligned.
     *
     * When the attribution is the only foreground layer, then we use a 16x8 canvas and center this
     * drawable.
     *
     * In both cases, we use a FIT_CENTER style scaling. Note that for now the attributions will
     * have to configure their own padding inside of their vector definitions. Future versions
     * should abstract the side- and center- canvases and allow attributions to be defined with
     * separate designs for each case.
     */
    val attribution: Drawable?
) {
    fun hasForegroundContent() = showPercent || attribution != null

    companion object {
        val DefaultInitialState =
            BatteryDrawableState(
                level = 50,
                showPercent = false,
                showErrorState = false,
                attribution = null,
            )
    }
}

sealed interface BatteryColors {
    /** The color for the frame and any foreground attributions for the battery */
    val fg: Int
    /**
     * Default color for the frame background. Configured to be a transparent white or black that
     * matches the current mode (white for light theme, black for dark theme) and provides extra
     * contrast for the drawable
     */
    val bg: Int

    /** Color for the level fill when there is an attribution on top */
    val fill: Int
    /**
     * When there is no attribution, [fillOnlyColor] describes an opaque color with more contrast
     */
    val fillOnly: Int

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

    /** Currently unused */
    val warnBackground: Int

    /** Color scheme appropriate for light mode (dark icons) */
    data object LightThemeColors : BatteryColors {
        override val fg = Color.BLACK
        // 22% alpha white
        override val bg: Int = Color.valueOf(1f, 1f, 1f, 0.22f).toArgb()

        // 18% alpha black
        override val fill = Color.valueOf(0f, 0f, 0f, 0.18f).toArgb()
        // GM Gray 500
        override val fillOnly = Color.parseColor("#9AA0A6")

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

        // GM Yellow 500
        override val warnBackground = Color.parseColor("#FBBC04")
    }

    /** Color scheme appropriate for dark mode (light icons) */
    data object DarkThemeColors : BatteryColors {
        override val fg = Color.WHITE
        // 18% alpha black
        override val bg: Int = Color.valueOf(0f, 0f, 0f, 0.18f).toArgb()

        // 22% alpha white
        override val fill = Color.valueOf(1f, 1f, 1f, 0.22f).toArgb()
        // GM Gray 600
        override val fillOnly = Color.parseColor("#80868B")

        // GM Red 600
        override val errorForeground = Color.parseColor("#D93025")
        // GM Red 200
        override val errorBackground = Color.parseColor("#F6AEA9")
        // GM Yellow
        override val warnBackground = Color.parseColor("#FBBC04")
    }
}
Loading