Home | History | Annotate | Download | only in graph
      1 /*
      2  * Copyright (C) 2019 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
      5  * except in compliance with the License. You may obtain a copy of the License at
      6  *
      7  *      http://www.apache.org/licenses/LICENSE-2.0
      8  *
      9  * Unless required by applicable law or agreed to in writing, software distributed under the
     10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
     11  * KIND, either express or implied. See the License for the specific language governing
     12  * permissions and limitations under the License.
     13  */
     14 
     15 package com.android.settingslib.graph
     16 
     17 import android.content.Context
     18 import android.graphics.BlendMode
     19 import android.graphics.Canvas
     20 import android.graphics.Color
     21 import android.graphics.ColorFilter
     22 import android.graphics.Matrix
     23 import android.graphics.Paint
     24 import android.graphics.Path
     25 import android.graphics.PixelFormat
     26 import android.graphics.Rect
     27 import android.graphics.RectF
     28 import android.graphics.drawable.Drawable
     29 import android.util.PathParser
     30 import android.util.TypedValue
     31 
     32 import com.android.settingslib.R
     33 import com.android.settingslib.Utils
     34 
     35 /**
     36  * A battery meter drawable that respects paths configured in
     37  * frameworks/base/core/res/res/values/config.xml to allow for an easily overrideable battery icon
     38  */
     39 open class ThemedBatteryDrawable(private val context: Context, frameColor: Int) : Drawable() {
     40 
     41     // Need to load:
     42     // 1. perimeter shape
     43     // 2. fill mask (if smaller than perimeter, this would create a fill that
     44     //    doesn't touch the walls
     45     private val perimeterPath = Path()
     46     private val scaledPerimeter = Path()
     47     private val errorPerimeterPath = Path()
     48     private val scaledErrorPerimeter = Path()
     49     // Fill will cover the whole bounding rect of the fillMask, and be masked by the path
     50     private val fillMask = Path()
     51     private val scaledFill = Path()
     52     // Based off of the mask, the fill will interpolate across this space
     53     private val fillRect = RectF()
     54     // Top of this rect changes based on level, 100% == fillRect
     55     private val levelRect = RectF()
     56     private val levelPath = Path()
     57     // Updates the transform of the paths when our bounds change
     58     private val scaleMatrix = Matrix()
     59     private val padding = Rect()
     60     // The net result of fill + perimeter paths
     61     private val unifiedPath = Path()
     62 
     63     // Bolt path (used while charging)
     64     private val boltPath = Path()
     65     private val scaledBolt = Path()
     66 
     67     // Plus sign (used for power save mode)
     68     private val plusPath = Path()
     69     private val scaledPlus = Path()
     70 
     71     private var intrinsicHeight: Int
     72     private var intrinsicWidth: Int
     73 
     74     // To implement hysteresis, keep track of the need to invert the interior icon of the battery
     75     private var invertFillIcon = false
     76 
     77     // Colors can be configured based on battery level (see res/values/arrays.xml)
     78     private var colorLevels: IntArray
     79 
     80     private var fillColor: Int = Color.MAGENTA
     81     private var backgroundColor: Int = Color.MAGENTA
     82     // updated whenever level changes
     83     private var levelColor: Int = Color.MAGENTA
     84 
     85     // Dual tone implies that battery level is a clipped overlay over top of the whole shape
     86     private var dualTone = false
     87 
     88     private var batteryLevel = 0
     89 
     90     private val invalidateRunnable: () -> Unit = {
     91         invalidateSelf()
     92     }
     93 
     94     open var criticalLevel: Int = context.resources.getInteger(
     95             com.android.internal.R.integer.config_criticalBatteryWarningLevel)
     96 
     97     var charging = false
     98         set(value) {
     99             field = value
    100             postInvalidate()
    101         }
    102 
    103     var powerSaveEnabled = false
    104         set(value) {
    105             field = value
    106             postInvalidate()
    107         }
    108 
    109     private val fillColorStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
    110         p.color = frameColor
    111         p.isDither = true
    112         p.strokeWidth = 5f
    113         p.style = Paint.Style.STROKE
    114         p.blendMode = BlendMode.SRC
    115         p.strokeMiter = 5f
    116         p.strokeJoin = Paint.Join.ROUND
    117     }
    118 
    119     private val fillColorStrokeProtection = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
    120         p.isDither = true
    121         p.strokeWidth = 5f
    122         p.style = Paint.Style.STROKE
    123         p.blendMode = BlendMode.CLEAR
    124         p.strokeMiter = 5f
    125         p.strokeJoin = Paint.Join.ROUND
    126     }
    127 
    128     private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
    129         p.color = frameColor
    130         p.alpha = 255
    131         p.isDither = true
    132         p.strokeWidth = 0f
    133         p.style = Paint.Style.FILL_AND_STROKE
    134     }
    135 
    136     private val errorPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
    137         p.color = Utils.getColorStateListDefaultColor(context, R.color.batterymeter_plus_color)
    138         p.alpha = 255
    139         p.isDither = true
    140         p.strokeWidth = 0f
    141         p.style = Paint.Style.FILL_AND_STROKE
    142         p.blendMode = BlendMode.SRC
    143     }
    144 
    145     // Only used if dualTone is set to true
    146     private val dualToneBackgroundFill = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
    147         p.color = frameColor
    148         p.alpha = 255
    149         p.isDither = true
    150         p.strokeWidth = 0f
    151         p.style = Paint.Style.FILL_AND_STROKE
    152     }
    153 
    154     init {
    155         val density = context.resources.displayMetrics.density
    156         intrinsicHeight = (Companion.HEIGHT * density).toInt()
    157         intrinsicWidth = (Companion.WIDTH * density).toInt()
    158 
    159         val res = context.resources
    160         val levels = res.obtainTypedArray(R.array.batterymeter_color_levels)
    161         val colors = res.obtainTypedArray(R.array.batterymeter_color_values)
    162         val N = levels.length()
    163         colorLevels = IntArray(2 * N)
    164         for (i in 0 until N) {
    165             colorLevels[2 * i] = levels.getInt(i, 0)
    166             if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) {
    167                 colorLevels[2 * i + 1] = Utils.getColorAttrDefaultColor(context,
    168                         colors.getThemeAttributeId(i, 0))
    169             } else {
    170                 colorLevels[2 * i + 1] = colors.getColor(i, 0)
    171             }
    172         }
    173         levels.recycle()
    174         colors.recycle()
    175 
    176         loadPaths()
    177     }
    178 
    179     override fun draw(c: Canvas) {
    180         c.saveLayer(null, null)
    181         unifiedPath.reset()
    182         levelPath.reset()
    183         levelRect.set(fillRect)
    184         val fillFraction = batteryLevel / 100f
    185         val fillTop =
    186                 if (batteryLevel >= 95)
    187                     fillRect.top
    188                 else
    189                     fillRect.top + (fillRect.height() * (1 - fillFraction))
    190 
    191         levelRect.top = Math.floor(fillTop.toDouble()).toFloat()
    192         levelPath.addRect(levelRect, Path.Direction.CCW)
    193 
    194         // The perimeter should never change
    195         unifiedPath.addPath(scaledPerimeter)
    196         // If drawing dual tone, the level is used only to clip the whole drawable path
    197         if (!dualTone) {
    198             unifiedPath.op(levelPath, Path.Op.UNION)
    199         }
    200 
    201         fillPaint.color = levelColor
    202 
    203         // Deal with unifiedPath clipping before it draws
    204         if (charging) {
    205             // Clip out the bolt shape
    206             unifiedPath.op(scaledBolt, Path.Op.DIFFERENCE)
    207             if (!invertFillIcon) {
    208                 c.drawPath(scaledBolt, fillPaint)
    209             }
    210         }
    211 
    212         if (dualTone) {
    213             // Dual tone means we draw the shape again, clipped to the charge level
    214             c.drawPath(unifiedPath, dualToneBackgroundFill)
    215             c.save()
    216             c.clipRect(0f,
    217                     bounds.bottom - bounds.height() * fillFraction,
    218                     bounds.right.toFloat(),
    219                     bounds.bottom.toFloat())
    220             c.drawPath(unifiedPath, fillPaint)
    221             c.restore()
    222         } else {
    223             // Non dual-tone means we draw the perimeter (with the level fill), and potentially
    224             // draw the fill again with a critical color
    225             fillPaint.color = fillColor
    226             c.drawPath(unifiedPath, fillPaint)
    227             fillPaint.color = levelColor
    228 
    229             // Show colorError below this level
    230             if (batteryLevel <= Companion.CRITICAL_LEVEL && !charging) {
    231                 c.save()
    232                 c.clipPath(scaledFill)
    233                 c.drawPath(levelPath, fillPaint)
    234                 c.restore()
    235             }
    236         }
    237 
    238         if (charging) {
    239             c.clipOutPath(scaledBolt)
    240             if (invertFillIcon) {
    241                 c.drawPath(scaledBolt, fillColorStrokePaint)
    242             } else {
    243                 c.drawPath(scaledBolt, fillColorStrokeProtection)
    244             }
    245         } else if (powerSaveEnabled) {
    246             // If power save is enabled draw the perimeter path with colorError
    247             c.drawPath(scaledErrorPerimeter, errorPaint)
    248             // And draw the plus sign on top of the fill
    249             c.drawPath(scaledPlus, errorPaint)
    250         }
    251         c.restore()
    252     }
    253 
    254     private fun batteryColorForLevel(level: Int): Int {
    255         return when {
    256             charging || powerSaveEnabled -> fillColor
    257             else -> getColorForLevel(level)
    258         }
    259     }
    260 
    261     private fun getColorForLevel(level: Int): Int {
    262         var thresh: Int
    263         var color = 0
    264         var i = 0
    265         while (i < colorLevels.size) {
    266             thresh = colorLevels[i]
    267             color = colorLevels[i + 1]
    268             if (level <= thresh) {
    269 
    270                 // Respect tinting for "normal" level
    271                 return if (i == colorLevels.size - 2) {
    272                     fillColor
    273                 } else {
    274                     color
    275                 }
    276             }
    277             i += 2
    278         }
    279         return color
    280     }
    281 
    282     /**
    283      * Alpha is unused internally, and should be defined in the colors passed to {@link setColors}.
    284      * Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds
    285      * defining the minimum background fill alpha. This is because fill + background must be equal
    286      * to the net alpha passed in here.
    287      */
    288     override fun setAlpha(alpha: Int) {
    289     }
    290 
    291     override fun setColorFilter(colorFilter: ColorFilter?) {
    292         fillPaint.colorFilter = colorFilter
    293         fillColorStrokePaint.colorFilter = colorFilter
    294         dualToneBackgroundFill.colorFilter = colorFilter
    295     }
    296 
    297     /**
    298      * Deprecated, but required by Drawable
    299      */
    300     override fun getOpacity(): Int {
    301         return PixelFormat.OPAQUE
    302     }
    303 
    304     override fun getIntrinsicHeight(): Int {
    305         return intrinsicHeight
    306     }
    307 
    308     override fun getIntrinsicWidth(): Int {
    309         return intrinsicWidth
    310     }
    311 
    312     /**
    313      * Set the fill level
    314      */
    315     public open fun setBatteryLevel(l: Int) {
    316         invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon
    317         batteryLevel = l
    318         levelColor = batteryColorForLevel(batteryLevel)
    319         invalidateSelf()
    320     }
    321 
    322     public fun getBatteryLevel(): Int {
    323         return batteryLevel
    324     }
    325 
    326     override fun onBoundsChange(bounds: Rect?) {
    327         super.onBoundsChange(bounds)
    328         updateSize()
    329     }
    330 
    331     fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
    332         padding.left = left
    333         padding.top = top
    334         padding.right = right
    335         padding.bottom = bottom
    336 
    337         updateSize()
    338     }
    339 
    340     fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) {
    341         fillColor = if (dualTone) fgColor else singleToneColor
    342 
    343         fillPaint.color = fillColor
    344         fillColorStrokePaint.color = fillColor
    345 
    346         backgroundColor = bgColor
    347         dualToneBackgroundFill.color = bgColor
    348 
    349         // Also update the level color, since fillColor may have changed
    350         levelColor = batteryColorForLevel(batteryLevel)
    351 
    352         invalidateSelf()
    353     }
    354 
    355     private fun postInvalidate() {
    356         unscheduleSelf(invalidateRunnable)
    357         scheduleSelf(invalidateRunnable, 0)
    358     }
    359 
    360     private fun updateSize() {
    361         val b = bounds
    362         if (b.isEmpty) {
    363             scaleMatrix.setScale(1f, 1f)
    364         } else {
    365             scaleMatrix.setScale((b.right / WIDTH), (b.bottom / HEIGHT))
    366         }
    367 
    368         perimeterPath.transform(scaleMatrix, scaledPerimeter)
    369         errorPerimeterPath.transform(scaleMatrix, scaledErrorPerimeter)
    370         fillMask.transform(scaleMatrix, scaledFill)
    371         scaledFill.computeBounds(fillRect, true)
    372         boltPath.transform(scaleMatrix, scaledBolt)
    373         plusPath.transform(scaleMatrix, scaledPlus)
    374 
    375         // It is expected that this view only ever scale by the same factor in each dimension, so
    376         // just pick one to scale the strokeWidths
    377         val scaledStrokeWidth =
    378                 Math.max(b.right / WIDTH * PROTECTION_STROKE_WIDTH, PROTECTION_MIN_STROKE_WIDTH)
    379 
    380         fillColorStrokePaint.strokeWidth = scaledStrokeWidth
    381         fillColorStrokeProtection.strokeWidth = scaledStrokeWidth
    382     }
    383 
    384     private fun loadPaths() {
    385         val pathString = context.resources.getString(
    386                 com.android.internal.R.string.config_batterymeterPerimeterPath)
    387         perimeterPath.set(PathParser.createPathFromPathData(pathString))
    388         perimeterPath.computeBounds(RectF(), true)
    389 
    390         val errorPathString = context.resources.getString(
    391                 com.android.internal.R.string.config_batterymeterErrorPerimeterPath)
    392         errorPerimeterPath.set(PathParser.createPathFromPathData(errorPathString))
    393         errorPerimeterPath.computeBounds(RectF(), true)
    394 
    395         val fillMaskString = context.resources.getString(
    396                 com.android.internal.R.string.config_batterymeterFillMask)
    397         fillMask.set(PathParser.createPathFromPathData(fillMaskString))
    398         // Set the fill rect so we can calculate the fill properly
    399         fillMask.computeBounds(fillRect, true)
    400 
    401         val boltPathString = context.resources.getString(
    402                 com.android.internal.R.string.config_batterymeterBoltPath)
    403         boltPath.set(PathParser.createPathFromPathData(boltPathString))
    404 
    405         val plusPathString = context.resources.getString(
    406                 com.android.internal.R.string.config_batterymeterPowersavePath)
    407         plusPath.set(PathParser.createPathFromPathData(plusPathString))
    408 
    409         dualTone = context.resources.getBoolean(
    410                 com.android.internal.R.bool.config_batterymeterDualTone)
    411     }
    412 
    413     companion object {
    414         private const val TAG = "ThemedBatteryDrawable"
    415         private const val WIDTH = 12f
    416         private const val HEIGHT = 20f
    417         private const val CRITICAL_LEVEL = 15
    418         // On a 12x20 grid, how wide to make the fill protection stroke.
    419         // Scales when our size changes
    420         private const val PROTECTION_STROKE_WIDTH = 3f
    421         // Arbitrarily chosen for visibility at small sizes
    422         private const val PROTECTION_MIN_STROKE_WIDTH = 6f
    423     }
    424 }
    425