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

Commit 9204a808 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Create a battery icon that loads paths"

parents 26ab14c9 2259da40
Loading
Loading
Loading
Loading
+7 −5
Original line number Diff line number Diff line
<!--
    Copyright (C) 2018 The Android Open Source Project
    Copyright (C) 2019 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.
@@ -21,8 +21,10 @@
        android:viewportHeight="24.0"
        android:tint="?android:attr/colorControlNormal">
    <path
            android:pathData="M5,3
            l3.5,0 l0,-1.5 l7,0 l0,1.5 l3.5,0 l0,19.5 l-14,0z
            M10.5,8.5 l0,3 l-3,0 l0,3 l3,0 l0,3 l3,0 l0,-3 l3,0 l0,-3 l-3,0 l0,-3 z"
            android:fillColor="#FFFFFF"/>
        android:fillColor="#FFF"
        android:pathData="M16.67,4H14.5V2h-5v2H7.33C6.6,4 6,4.6 6,5.33V15v5.67C6,21.4 6.6,22 7.33,22h9.33C17.4,22 18,21.4 18,20.67V15V5.33C18,4.6 17.4,4 16.67,4zM16,15v5H8v-5V6h8V15z"/>
    <path
        android:fillColor="#FFF"
        android:pathData="M15,12l-2,0l0,-2l-2,0l0,2l-2,0l0,2l2,0l0,2l2,0l0,-2l2,0z"/>

</vector>
+21 −0
Original line number Diff line number Diff line
@@ -3935,4 +3935,25 @@
         The same restrictions apply to this array. -->
    <array name="config_displayWhiteBalanceDisplayColorTemperatures">
    </array>

    <!-- All of the paths defined for the batterymeter are defined on a 12x20 canvas, and must
     be parsable by android.utill.PathParser -->
    <string name="config_batterymeterPerimeterPath" translatable="false">
		M3.5,2 v0 H1.33 C0.6,2 0,2.6 0,3.33 V13v5.67 C0,19.4 0.6,20 1.33,20 h9.33 C11.4,20 12,19.4 12,18.67 V13V3.33 C12,2.6 11.4,2 10.67,2 H8.5 V0 H3.5 z M2,18v-7V4h8v9v5H2L2,18z
    </string>

    <string name="config_batterymeterFillMask" translatable="false">
        M2,18 v-14 h8 v14 z
    </string>
    <string name="config_batterymeterBoltPath" translatable="false">
        M5,17.5 V12 H3 L7,4.5 V10 h2 L5,17.5 z
    </string>
    <string name="config_batterymeterPowersavePath" translatable="false">
		M9,10l-2,0l0,-2l-2,0l0,2l-2,0l0,2l2,0l0,2l2,0l0,-2l2,0z
    </string>

    <!-- A dual tone battery meter draws the perimeter path twice - once to define the shape
     and a second time clipped to the fill level to indicate charge -->
    <bool name="config_batterymeterDualTone">false</bool>

</resources>
+5 −0
Original line number Diff line number Diff line
@@ -3197,6 +3197,11 @@
  <java-symbol type="raw" name="fallback_categories" />

  <java-symbol type="string" name="config_icon_mask" />
  <java-symbol type="string" name="config_batterymeterPerimeterPath" />
  <java-symbol type="string" name="config_batterymeterFillMask" />
  <java-symbol type="string" name="config_batterymeterBoltPath" />
  <java-symbol type="string" name="config_batterymeterPowersavePath" />
  <java-symbol type="bool" name="config_batterymeterDualTone" />

  <!-- Accessibility Shortcut -->
  <java-symbol type="string" name="accessibility_shortcut_warning_dialog_title" />
+1 −1
Original line number Diff line number Diff line
@@ -29,7 +29,7 @@ android_library {

    resource_dirs: ["res"],

    srcs: ["src/**/*.java"],
    srcs: ["src/**/*.java", "src/**/*.kt"],

    min_sdk_version: "21",

+404 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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.settingslib.graph

import android.content.Context
import android.graphics.BlendMode
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.util.PathParser
import android.util.TypedValue

import com.android.settingslib.R
import com.android.settingslib.Utils

/**
 * A battery meter drawable that respects paths configured in
 * frameworks/base/core/res/res/values/config.xml to allow for an easily overrideable battery icon
 */
open class ThemedBatteryDrawable(private val context: Context, frameColor: Int) : Drawable() {

    // Need to load:
    // 1. perimeter shape
    // 2. fill mask (if smaller than perimeter, this would create a fill that
    //    doesn't touch the walls
    private val perimeterPath = Path()
    private val scaledPerimeter = Path()
    // Fill will cover the whole bounding rect of the fillMask, and be masked by the path
    private val fillMask = Path()
    private val scaledFill = Path()
    // Based off of the mask, the fill will interpolate across this space
    private val fillRect = RectF()
    // Top of this rect changes based on level, 100% == fillRect
    private val levelRect = RectF()
    private val levelPath = Path()
    // Updates the transform of the paths when our bounds change
    private val scaleMatrix = Matrix()
    private val padding = Rect()
    // The net result of fill + perimeter paths
    private val unifiedPath = Path()

    // Bolt path (used while charging)
    private val boltPath = Path()
    private val scaledBolt = Path()

    // Plus sign (used for power save mode)
    private val plusPath = Path()
    private val scaledPlus = Path()

    private var intrinsicHeight: Int
    private var intrinsicWidth: Int

    // To implement hysteresis, keep track of the need to invert the interior icon of the battery
    private var invertFillIcon = false

    // Colors can be configured based on battery level (see res/values/arrays.xml)
    private var colorLevels: IntArray

    private var fillColor: Int = Color.MAGENTA
    private var backgroundColor: Int = Color.MAGENTA
    // updated whenever level changes
    private var levelColor: Int = Color.MAGENTA

    // Dual tone implies that battery level is a clipped overlay over top of the whole shape
    private var dualTone = false

    private val invalidateRunnable: () -> Unit = {
        invalidateSelf()
    }

    open var criticalLevel: Int = 0

    var charging = false
        set(value) {
            field = value
            postInvalidate()
        }

    var powerSaveEnabled = false
        set(value) {
            field = value
            postInvalidate()
        }

    private val fillColorStrokePaint: Paint by lazy {
        val p = Paint(Paint.ANTI_ALIAS_FLAG)
        p.color = frameColor
        p.isDither = true
        p.strokeWidth = 5f
        p.style = Paint.Style.STROKE
        p.blendMode = BlendMode.SRC
        p.strokeMiter = 5f
        p
    }

    private val fillColorStrokeProtection: Paint by lazy {
        val p = Paint(Paint.ANTI_ALIAS_FLAG)
        p.isDither = true
        p.strokeWidth = 5f
        p.style = Paint.Style.STROKE
        p.blendMode = BlendMode.CLEAR
        p.strokeMiter = 5f
        p
    }

    private val fillPaint: Paint by lazy {
        val p = Paint(Paint.ANTI_ALIAS_FLAG)
        p.color = frameColor
        p.alpha = 255
        p.isDither = true
        p.strokeWidth = 0f
        p.style = Paint.Style.FILL_AND_STROKE
        p
    }

    // Only used if dualTone is set to true
    private val dualToneBackgroundFill: Paint by lazy {
        val p = Paint(Paint.ANTI_ALIAS_FLAG)
        p.color = frameColor
        p.alpha = 255
        p.isDither = true
        p.strokeWidth = 0f
        p.style = Paint.Style.FILL_AND_STROKE
        p
    }

    init {
        val density = context.resources.displayMetrics.density
        intrinsicHeight = (Companion.HEIGHT * density).toInt()
        intrinsicWidth = (Companion.WIDTH * density).toInt()

        val res = context.resources
        val levels = res.obtainTypedArray(R.array.batterymeter_color_levels)
        val colors = res.obtainTypedArray(R.array.batterymeter_color_values)
        val N = levels.length()
        colorLevels = IntArray(2 * N)
        for (i in 0 until N) {
            colorLevels[2 * i] = levels.getInt(i, 0)
            if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) {
                colorLevels[2 * i + 1] = Utils.getColorAttrDefaultColor(context,
                        colors.getThemeAttributeId(i, 0))
            } else {
                colorLevels[2 * i + 1] = colors.getColor(i, 0)
            }
        }
        levels.recycle()
        colors.recycle()

        criticalLevel = context.resources.getInteger(
                com.android.internal.R.integer.config_criticalBatteryWarningLevel)

        loadPaths()
    }

    override fun draw(c: Canvas) {
        unifiedPath.reset()
        levelPath.reset()
        levelRect.set(fillRect)
        val fillFraction = level / 100f
        val fillTop =
                if (level >= 95)
                    fillRect.top
                else
                    fillRect.top + (fillRect.height() * (1 - fillFraction))

        levelRect.top = Math.floor(fillTop.toDouble()).toFloat()
        levelPath.addRect(levelRect, Path.Direction.CCW)

        // The perimeter should never change
        unifiedPath.addPath(scaledPerimeter)
        // IF drawing dual tone, the level is used only to clip the whole drawable path
        if (!dualTone) {
            unifiedPath.op(levelPath, Path.Op.UNION)
        }

        fillPaint.color = levelColor

        // Deal with unifiedPath clipping before it draws
        if (charging) {
            // Clip out the bolt shape
            unifiedPath.op(scaledBolt, Path.Op.DIFFERENCE)
            if (!invertFillIcon) {
                c.drawPath(scaledBolt, fillPaint)
            }
        } else if (powerSaveEnabled) {
            // Clip out the plus shape
            unifiedPath.op(scaledPlus, Path.Op.DIFFERENCE)
            if (!invertFillIcon) {
                c.drawPath(scaledPlus, fillPaint)
            }
        }

        if (dualTone) {
            // Dual tone means we draw the shape again, clipped to the charge level
            c.drawPath(unifiedPath, dualToneBackgroundFill)
            c.save()
            c.clipRect(0f,
                    bounds.bottom - bounds.height() * fillFraction,
                    bounds.right.toFloat(),
                    bounds.bottom.toFloat())
            c.drawPath(unifiedPath, fillPaint)
            c.restore()
        } else {
            // Non dual-tone means we draw the perimeter (with the level fill), and potentially
            // draw the fill again with a critical color
            fillPaint.color = fillColor
            c.drawPath(unifiedPath, fillPaint)
            fillPaint.color = levelColor

            // Show colorError below this level
            if (level <= Companion.CRITICAL_LEVEL && !charging) {
                c.save()
                c.clipPath(scaledFill)
                c.drawPath(levelPath, fillPaint)
                c.restore()
            }
        }

        if (charging) {
            c.clipOutPath(scaledBolt)
            if (invertFillIcon) {
                c.drawPath(scaledBolt, fillColorStrokePaint)
            } else {
                c.drawPath(scaledBolt, fillColorStrokeProtection)
            }
        } else if (powerSaveEnabled) {
            c.clipOutPath(scaledPlus)
            if (invertFillIcon) {
                c.drawPath(scaledPlus, fillColorStrokePaint)
            } else {
                c.drawPath(scaledPlus, fillColorStrokeProtection)
            }
        }
    }

    private fun batteryColorForLevel(level: Int): Int {
        return when {
            charging || powerSaveEnabled -> fillPaint.color
            else -> getColorForLevel(level)
        }
    }

    private fun getColorForLevel(level: Int): Int {
        var thresh: Int
        var color = 0
        var i = 0
        while (i < colorLevels.size) {
            thresh = colorLevels[i]
            color = colorLevels[i + 1]
            if (level <= thresh) {

                // Respect tinting for "normal" level
                return if (i == colorLevels.size - 2) {
                    fillColor
                } else {
                    color
                }
            }
            i += 2
        }
        return color
    }

    /**
     * Alpha is unused internally, and should be defined in the colors passed to {@link setColors}.
     * Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds
     * defining the minimum background fill alpha. This is because fill + background must be equal
     * to the net alpha passed in here.
     */
    override fun setAlpha(alpha: Int) {
    }

    override fun setColorFilter(colorFilter: ColorFilter?) {
        fillPaint.colorFilter = colorFilter
        fillColorStrokePaint.colorFilter = colorFilter
        dualToneBackgroundFill.colorFilter = colorFilter
    }

    /**
     * Deprecated, but required by Drawable
     */
    override fun getOpacity(): Int {
        return PixelFormat.OPAQUE
    }

    override fun getIntrinsicHeight(): Int {
        return intrinsicHeight
    }

    override fun getIntrinsicWidth(): Int {
        return intrinsicWidth
    }

    /**
     * Set the fill level
     */
    public open fun setBatteryLevel(l: Int) {
        invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon
        level = l
        levelColor = batteryColorForLevel(level)
        invalidateSelf()
    }

    public fun getBatteryLevel(): Int {
        return level
    }

    override fun onBoundsChange(bounds: Rect?) {
        super.onBoundsChange(bounds)
        updateSize()
    }

    fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
        padding.left = left
        padding.top = top
        padding.right = right
        padding.bottom = bottom

        updateSize()
    }

    fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) {
        fillColor = if (dualTone) fgColor else singleToneColor

        fillPaint.color = fillColor
        fillColorStrokePaint.color = fillColor

        backgroundColor = bgColor
        dualToneBackgroundFill.color = bgColor

        invalidateSelf()
    }

    private fun postInvalidate() {
        unscheduleSelf(invalidateRunnable)
        scheduleSelf(invalidateRunnable, 0)
    }

    private fun updateSize() {
        val b = bounds
        if (b.isEmpty) {
            scaleMatrix.setScale(1f, 1f)
        } else {
            scaleMatrix.setScale((b.right / Companion.WIDTH), (b.bottom / Companion.HEIGHT))
        }

        perimeterPath.transform(scaleMatrix, scaledPerimeter)
        fillMask.transform(scaleMatrix, scaledFill)
        scaledFill.computeBounds(fillRect, true)
        boltPath.transform(scaleMatrix, scaledBolt)
        plusPath.transform(scaleMatrix, scaledPlus)
    }

    private fun loadPaths() {
        val pathString = context.resources.getString(
                com.android.internal.R.string.config_batterymeterPerimeterPath)
        perimeterPath.set(PathParser.createPathFromPathData(pathString))
        val b = RectF()
        perimeterPath.computeBounds(b, true)

        val fillMaskString = context.resources.getString(
                com.android.internal.R.string.config_batterymeterFillMask)
        fillMask.set(PathParser.createPathFromPathData(fillMaskString))
        // Set the fill rect so we can calculate the fill properly
        fillMask.computeBounds(fillRect, true)

        val boltPathString = context.resources.getString(
                com.android.internal.R.string.config_batterymeterBoltPath)
        boltPath.set(PathParser.createPathFromPathData(boltPathString))

        val plusPathString = context.resources.getString(
                com.android.internal.R.string.config_batterymeterPowersavePath)
        plusPath.set(PathParser.createPathFromPathData(plusPathString))

        dualTone = context.resources.getBoolean(
                com.android.internal.R.bool.config_batterymeterDualTone)
    }

    companion object {
        private const val TAG = "ThemedBatteryDrawable"
        private const val WIDTH = 12f
        private const val HEIGHT = 20f
        private const val CRITICAL_LEVEL = 15
    }
}
Loading