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.alpha = 255 112 p.isDither = true 113 p.strokeWidth = 5f 114 p.style = Paint.Style.STROKE 115 p.blendMode = BlendMode.SRC 116 p.strokeMiter = 5f 117 p.strokeJoin = Paint.Join.ROUND 118 } 119 120 private val fillColorStrokeProtection = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> 121 p.isDither = true 122 p.strokeWidth = 5f 123 p.style = Paint.Style.STROKE 124 p.blendMode = BlendMode.CLEAR 125 p.strokeMiter = 5f 126 p.strokeJoin = Paint.Join.ROUND 127 } 128 129 private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> 130 p.color = frameColor 131 p.alpha = 255 132 p.isDither = true 133 p.strokeWidth = 0f 134 p.style = Paint.Style.FILL_AND_STROKE 135 } 136 137 private val errorPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> 138 p.color = Utils.getColorStateListDefaultColor(context, R.color.batterymeter_saver_color) 139 p.alpha = 255 140 p.isDither = true 141 p.strokeWidth = 0f 142 p.style = Paint.Style.FILL_AND_STROKE 143 p.blendMode = BlendMode.SRC 144 } 145 146 // Only used if dualTone is set to true 147 private val dualToneBackgroundFill = Paint(Paint.ANTI_ALIAS_FLAG).also { p -> 148 p.color = frameColor 149 p.alpha = 85 // ~0.3 alpha by default 150 p.isDither = true 151 p.strokeWidth = 0f 152 p.style = Paint.Style.FILL_AND_STROKE 153 } 154 155 init { 156 val density = context.resources.displayMetrics.density 157 intrinsicHeight = (Companion.HEIGHT * density).toInt() 158 intrinsicWidth = (Companion.WIDTH * density).toInt() 159 160 val res = context.resources 161 val levels = res.obtainTypedArray(R.array.batterymeter_color_levels) 162 val colors = res.obtainTypedArray(R.array.batterymeter_color_values) 163 val N = levels.length() 164 colorLevels = IntArray(2 * N) 165 for (i in 0 until N) { 166 colorLevels[2 * i] = levels.getInt(i, 0) 167 if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) { 168 colorLevels[2 * i + 1] = Utils.getColorAttrDefaultColor(context, 169 colors.getThemeAttributeId(i, 0)) 170 } else { 171 colorLevels[2 * i + 1] = colors.getColor(i, 0) 172 } 173 } 174 levels.recycle() 175 colors.recycle() 176 177 loadPaths() 178 } 179 180 override fun draw(c: Canvas) { 181 c.saveLayer(null, null) 182 unifiedPath.reset() 183 levelPath.reset() 184 levelRect.set(fillRect) 185 val fillFraction = batteryLevel / 100f 186 val fillTop = 187 if (batteryLevel >= 95) 188 fillRect.top 189 else 190 fillRect.top + (fillRect.height() * (1 - fillFraction)) 191 192 levelRect.top = Math.floor(fillTop.toDouble()).toFloat() 193 levelPath.addRect(levelRect, Path.Direction.CCW) 194 195 // The perimeter should never change 196 unifiedPath.addPath(scaledPerimeter) 197 // If drawing dual tone, the level is used only to clip the whole drawable path 198 if (!dualTone) { 199 unifiedPath.op(levelPath, Path.Op.UNION) 200 } 201 202 fillPaint.color = levelColor 203 204 // Deal with unifiedPath clipping before it draws 205 if (charging) { 206 // Clip out the bolt shape 207 unifiedPath.op(scaledBolt, Path.Op.DIFFERENCE) 208 if (!invertFillIcon) { 209 c.drawPath(scaledBolt, fillPaint) 210 } 211 } 212 213 if (dualTone) { 214 // Dual tone means we draw the shape again, clipped to the charge level 215 c.drawPath(unifiedPath, dualToneBackgroundFill) 216 c.save() 217 c.clipRect(0f, 218 bounds.bottom - bounds.height() * fillFraction, 219 bounds.right.toFloat(), 220 bounds.bottom.toFloat()) 221 c.drawPath(unifiedPath, fillPaint) 222 c.restore() 223 } else { 224 // Non dual-tone means we draw the perimeter (with the level fill), and potentially 225 // draw the fill again with a critical color 226 fillPaint.color = fillColor 227 c.drawPath(unifiedPath, fillPaint) 228 fillPaint.color = levelColor 229 230 // Show colorError below this level 231 if (batteryLevel <= Companion.CRITICAL_LEVEL && !charging) { 232 c.save() 233 c.clipPath(scaledFill) 234 c.drawPath(levelPath, fillPaint) 235 c.restore() 236 } 237 } 238 239 if (charging) { 240 c.clipOutPath(scaledBolt) 241 if (invertFillIcon) { 242 c.drawPath(scaledBolt, fillColorStrokePaint) 243 } else { 244 c.drawPath(scaledBolt, fillColorStrokeProtection) 245 } 246 } else if (powerSaveEnabled) { 247 // If power save is enabled draw the level path with colorError 248 c.drawPath(levelPath, errorPaint) 249 // And draw the plus sign on top of the fill 250 fillPaint.color = fillColor 251 c.drawPath(scaledPlus, fillPaint) 252 } 253 c.restore() 254 } 255 256 private fun batteryColorForLevel(level: Int): Int { 257 return when { 258 charging || powerSaveEnabled -> fillColor 259 else -> getColorForLevel(level) 260 } 261 } 262 263 private fun getColorForLevel(level: Int): Int { 264 var thresh: Int 265 var color = 0 266 var i = 0 267 while (i < colorLevels.size) { 268 thresh = colorLevels[i] 269 color = colorLevels[i + 1] 270 if (level <= thresh) { 271 272 // Respect tinting for "normal" level 273 return if (i == colorLevels.size - 2) { 274 fillColor 275 } else { 276 color 277 } 278 } 279 i += 2 280 } 281 return color 282 } 283 284 /** 285 * Alpha is unused internally, and should be defined in the colors passed to {@link setColors}. 286 * Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds 287 * defining the minimum background fill alpha. This is because fill + background must be equal 288 * to the net alpha passed in here. 289 */ 290 override fun setAlpha(alpha: Int) { 291 } 292 293 override fun setColorFilter(colorFilter: ColorFilter?) { 294 fillPaint.colorFilter = colorFilter 295 fillColorStrokePaint.colorFilter = colorFilter 296 dualToneBackgroundFill.colorFilter = colorFilter 297 } 298 299 /** 300 * Deprecated, but required by Drawable 301 */ 302 override fun getOpacity(): Int { 303 return PixelFormat.OPAQUE 304 } 305 306 override fun getIntrinsicHeight(): Int { 307 return intrinsicHeight 308 } 309 310 override fun getIntrinsicWidth(): Int { 311 return intrinsicWidth 312 } 313 314 /** 315 * Set the fill level 316 */ 317 public open fun setBatteryLevel(l: Int) { 318 invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon 319 batteryLevel = l 320 levelColor = batteryColorForLevel(batteryLevel) 321 invalidateSelf() 322 } 323 324 public fun getBatteryLevel(): Int { 325 return batteryLevel 326 } 327 328 override fun onBoundsChange(bounds: Rect) { 329 super.onBoundsChange(bounds) 330 updateSize() 331 } 332 333 fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { 334 padding.left = left 335 padding.top = top 336 padding.right = right 337 padding.bottom = bottom 338 339 updateSize() 340 } 341 342 fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) { 343 fillColor = if (dualTone) fgColor else singleToneColor 344 345 fillPaint.color = fillColor 346 fillColorStrokePaint.color = fillColor 347 348 backgroundColor = bgColor 349 dualToneBackgroundFill.color = bgColor 350 351 // Also update the level color, since fillColor may have changed 352 levelColor = batteryColorForLevel(batteryLevel) 353 354 invalidateSelf() 355 } 356 357 private fun postInvalidate() { 358 unscheduleSelf(invalidateRunnable) 359 scheduleSelf(invalidateRunnable, 0) 360 } 361 362 private fun updateSize() { 363 val b = bounds 364 if (b.isEmpty) { 365 scaleMatrix.setScale(1f, 1f) 366 } else { 367 scaleMatrix.setScale((b.right / WIDTH), (b.bottom / HEIGHT)) 368 } 369 370 perimeterPath.transform(scaleMatrix, scaledPerimeter) 371 errorPerimeterPath.transform(scaleMatrix, scaledErrorPerimeter) 372 fillMask.transform(scaleMatrix, scaledFill) 373 scaledFill.computeBounds(fillRect, true) 374 boltPath.transform(scaleMatrix, scaledBolt) 375 plusPath.transform(scaleMatrix, scaledPlus) 376 377 // It is expected that this view only ever scale by the same factor in each dimension, so 378 // just pick one to scale the strokeWidths 379 val scaledStrokeWidth = 380 Math.max(b.right / WIDTH * PROTECTION_STROKE_WIDTH, PROTECTION_MIN_STROKE_WIDTH) 381 382 fillColorStrokePaint.strokeWidth = scaledStrokeWidth 383 fillColorStrokeProtection.strokeWidth = scaledStrokeWidth 384 } 385 386 private fun loadPaths() { 387 val pathString = context.resources.getString( 388 com.android.internal.R.string.config_batterymeterPerimeterPath) 389 perimeterPath.set(PathParser.createPathFromPathData(pathString)) 390 perimeterPath.computeBounds(RectF(), true) 391 392 val errorPathString = context.resources.getString( 393 com.android.internal.R.string.config_batterymeterErrorPerimeterPath) 394 errorPerimeterPath.set(PathParser.createPathFromPathData(errorPathString)) 395 errorPerimeterPath.computeBounds(RectF(), true) 396 397 val fillMaskString = context.resources.getString( 398 com.android.internal.R.string.config_batterymeterFillMask) 399 fillMask.set(PathParser.createPathFromPathData(fillMaskString)) 400 // Set the fill rect so we can calculate the fill properly 401 fillMask.computeBounds(fillRect, true) 402 403 val boltPathString = context.resources.getString( 404 com.android.internal.R.string.config_batterymeterBoltPath) 405 boltPath.set(PathParser.createPathFromPathData(boltPathString)) 406 407 val plusPathString = context.resources.getString( 408 com.android.internal.R.string.config_batterymeterPowersavePath) 409 plusPath.set(PathParser.createPathFromPathData(plusPathString)) 410 411 dualTone = context.resources.getBoolean( 412 com.android.internal.R.bool.config_batterymeterDualTone) 413 } 414 415 companion object { 416 const val WIDTH = 12f 417 const val HEIGHT = 20f 418 private const val CRITICAL_LEVEL = 20 419 // On a 12x20 grid, how wide to make the fill protection stroke. 420 // Scales when our size changes 421 private const val PROTECTION_STROKE_WIDTH = 3f 422 // Arbitrarily chosen for visibility at small sizes 423 const val PROTECTION_MIN_STROKE_WIDTH = 6f 424 } 425 } 426