Create a battery icon that loads paths
Create a new battery drawable that will load enough resources from
frameworks/base/core/res/res/ to allow for theming overlays. The current
things are overlayable:
- Perimeter path: the outer shape of the battery (including the
terminal)
- Fill mask: path defining the shape of the fill
- Bolt path: charging bolt path. draws with appropriate protection for
visibilty
- Powersave path: path of the plus sign that draws when in powersave
mode. also draws with protection
Test: visual; sysui demo mode
Bug: 123705805
Change-Id: I2bb15fd10e3fec63cb115a8f216794933b717404
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index caa928f..730e9e1 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -29,7 +29,7 @@
resource_dirs: ["res"],
- srcs: ["src/**/*.java"],
+ srcs: ["src/**/*.java", "src/**/*.kt"],
min_sdk_version: "21",
diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt b/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt
new file mode 100644
index 0000000..337106b
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt
@@ -0,0 +1,404 @@
+/*
+ * 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
+ }
+}