1 /* <lambda>null2 * 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