Evan Laird | 2259da4 | 2019-02-08 16:48:53 -0500 | [diff] [blame] | 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 | // Fill will cover the whole bounding rect of the fillMask, and be masked by the path |
| 48 | private val fillMask = Path() |
| 49 | private val scaledFill = Path() |
| 50 | // Based off of the mask, the fill will interpolate across this space |
| 51 | private val fillRect = RectF() |
| 52 | // Top of this rect changes based on level, 100% == fillRect |
| 53 | private val levelRect = RectF() |
| 54 | private val levelPath = Path() |
| 55 | // Updates the transform of the paths when our bounds change |
| 56 | private val scaleMatrix = Matrix() |
| 57 | private val padding = Rect() |
| 58 | // The net result of fill + perimeter paths |
| 59 | private val unifiedPath = Path() |
| 60 | |
| 61 | // Bolt path (used while charging) |
| 62 | private val boltPath = Path() |
| 63 | private val scaledBolt = Path() |
| 64 | |
| 65 | // Plus sign (used for power save mode) |
| 66 | private val plusPath = Path() |
| 67 | private val scaledPlus = Path() |
| 68 | |
| 69 | private var intrinsicHeight: Int |
| 70 | private var intrinsicWidth: Int |
| 71 | |
| 72 | // To implement hysteresis, keep track of the need to invert the interior icon of the battery |
| 73 | private var invertFillIcon = false |
| 74 | |
| 75 | // Colors can be configured based on battery level (see res/values/arrays.xml) |
| 76 | private var colorLevels: IntArray |
| 77 | |
| 78 | private var fillColor: Int = Color.MAGENTA |
| 79 | private var backgroundColor: Int = Color.MAGENTA |
| 80 | // updated whenever level changes |
| 81 | private var levelColor: Int = Color.MAGENTA |
| 82 | |
| 83 | // Dual tone implies that battery level is a clipped overlay over top of the whole shape |
| 84 | private var dualTone = false |
| 85 | |
| 86 | private val invalidateRunnable: () -> Unit = { |
| 87 | invalidateSelf() |
| 88 | } |
| 89 | |
| 90 | open var criticalLevel: Int = 0 |
| 91 | |
| 92 | var charging = false |
| 93 | set(value) { |
| 94 | field = value |
| 95 | postInvalidate() |
| 96 | } |
| 97 | |
| 98 | var powerSaveEnabled = false |
| 99 | set(value) { |
| 100 | field = value |
| 101 | postInvalidate() |
| 102 | } |
| 103 | |
| 104 | private val fillColorStrokePaint: Paint by lazy { |
| 105 | val p = Paint(Paint.ANTI_ALIAS_FLAG) |
| 106 | p.color = frameColor |
| 107 | p.isDither = true |
| 108 | p.strokeWidth = 5f |
| 109 | p.style = Paint.Style.STROKE |
| 110 | p.blendMode = BlendMode.SRC |
| 111 | p.strokeMiter = 5f |
| 112 | p |
| 113 | } |
| 114 | |
| 115 | private val fillColorStrokeProtection: Paint by lazy { |
| 116 | val p = Paint(Paint.ANTI_ALIAS_FLAG) |
| 117 | p.isDither = true |
| 118 | p.strokeWidth = 5f |
| 119 | p.style = Paint.Style.STROKE |
| 120 | p.blendMode = BlendMode.CLEAR |
| 121 | p.strokeMiter = 5f |
| 122 | p |
| 123 | } |
| 124 | |
| 125 | private val fillPaint: Paint by lazy { |
| 126 | val p = Paint(Paint.ANTI_ALIAS_FLAG) |
| 127 | p.color = frameColor |
| 128 | p.alpha = 255 |
| 129 | p.isDither = true |
| 130 | p.strokeWidth = 0f |
| 131 | p.style = Paint.Style.FILL_AND_STROKE |
| 132 | p |
| 133 | } |
| 134 | |
| 135 | // Only used if dualTone is set to true |
| 136 | private val dualToneBackgroundFill: Paint by lazy { |
| 137 | val p = Paint(Paint.ANTI_ALIAS_FLAG) |
| 138 | p.color = frameColor |
| 139 | p.alpha = 255 |
| 140 | p.isDither = true |
| 141 | p.strokeWidth = 0f |
| 142 | p.style = Paint.Style.FILL_AND_STROKE |
| 143 | p |
| 144 | } |
| 145 | |
| 146 | init { |
| 147 | val density = context.resources.displayMetrics.density |
| 148 | intrinsicHeight = (Companion.HEIGHT * density).toInt() |
| 149 | intrinsicWidth = (Companion.WIDTH * density).toInt() |
| 150 | |
| 151 | val res = context.resources |
| 152 | val levels = res.obtainTypedArray(R.array.batterymeter_color_levels) |
| 153 | val colors = res.obtainTypedArray(R.array.batterymeter_color_values) |
| 154 | val N = levels.length() |
| 155 | colorLevels = IntArray(2 * N) |
| 156 | for (i in 0 until N) { |
| 157 | colorLevels[2 * i] = levels.getInt(i, 0) |
| 158 | if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) { |
| 159 | colorLevels[2 * i + 1] = Utils.getColorAttrDefaultColor(context, |
| 160 | colors.getThemeAttributeId(i, 0)) |
| 161 | } else { |
| 162 | colorLevels[2 * i + 1] = colors.getColor(i, 0) |
| 163 | } |
| 164 | } |
| 165 | levels.recycle() |
| 166 | colors.recycle() |
| 167 | |
| 168 | criticalLevel = context.resources.getInteger( |
| 169 | com.android.internal.R.integer.config_criticalBatteryWarningLevel) |
| 170 | |
| 171 | loadPaths() |
| 172 | } |
| 173 | |
| 174 | override fun draw(c: Canvas) { |
| 175 | unifiedPath.reset() |
| 176 | levelPath.reset() |
| 177 | levelRect.set(fillRect) |
| 178 | val fillFraction = level / 100f |
| 179 | val fillTop = |
| 180 | if (level >= 95) |
| 181 | fillRect.top |
| 182 | else |
| 183 | fillRect.top + (fillRect.height() * (1 - fillFraction)) |
| 184 | |
| 185 | levelRect.top = Math.floor(fillTop.toDouble()).toFloat() |
| 186 | levelPath.addRect(levelRect, Path.Direction.CCW) |
| 187 | |
| 188 | // The perimeter should never change |
| 189 | unifiedPath.addPath(scaledPerimeter) |
| 190 | // IF drawing dual tone, the level is used only to clip the whole drawable path |
| 191 | if (!dualTone) { |
| 192 | unifiedPath.op(levelPath, Path.Op.UNION) |
| 193 | } |
| 194 | |
| 195 | fillPaint.color = levelColor |
| 196 | |
| 197 | // Deal with unifiedPath clipping before it draws |
| 198 | if (charging) { |
| 199 | // Clip out the bolt shape |
| 200 | unifiedPath.op(scaledBolt, Path.Op.DIFFERENCE) |
| 201 | if (!invertFillIcon) { |
| 202 | c.drawPath(scaledBolt, fillPaint) |
| 203 | } |
| 204 | } else if (powerSaveEnabled) { |
| 205 | // Clip out the plus shape |
| 206 | unifiedPath.op(scaledPlus, Path.Op.DIFFERENCE) |
| 207 | if (!invertFillIcon) { |
| 208 | c.drawPath(scaledPlus, 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 (level <= 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 | c.clipOutPath(scaledPlus) |
| 247 | if (invertFillIcon) { |
| 248 | c.drawPath(scaledPlus, fillColorStrokePaint) |
| 249 | } else { |
| 250 | c.drawPath(scaledPlus, fillColorStrokeProtection) |
| 251 | } |
| 252 | } |
| 253 | } |
| 254 | |
| 255 | private fun batteryColorForLevel(level: Int): Int { |
| 256 | return when { |
| 257 | charging || powerSaveEnabled -> fillPaint.color |
| 258 | else -> getColorForLevel(level) |
| 259 | } |
| 260 | } |
| 261 | |
| 262 | private fun getColorForLevel(level: Int): Int { |
| 263 | var thresh: Int |
| 264 | var color = 0 |
| 265 | var i = 0 |
| 266 | while (i < colorLevels.size) { |
| 267 | thresh = colorLevels[i] |
| 268 | color = colorLevels[i + 1] |
| 269 | if (level <= thresh) { |
| 270 | |
| 271 | // Respect tinting for "normal" level |
| 272 | return if (i == colorLevels.size - 2) { |
| 273 | fillColor |
| 274 | } else { |
| 275 | color |
| 276 | } |
| 277 | } |
| 278 | i += 2 |
| 279 | } |
| 280 | return color |
| 281 | } |
| 282 | |
| 283 | /** |
| 284 | * Alpha is unused internally, and should be defined in the colors passed to {@link setColors}. |
| 285 | * Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds |
| 286 | * defining the minimum background fill alpha. This is because fill + background must be equal |
| 287 | * to the net alpha passed in here. |
| 288 | */ |
| 289 | override fun setAlpha(alpha: Int) { |
| 290 | } |
| 291 | |
| 292 | override fun setColorFilter(colorFilter: ColorFilter?) { |
| 293 | fillPaint.colorFilter = colorFilter |
| 294 | fillColorStrokePaint.colorFilter = colorFilter |
| 295 | dualToneBackgroundFill.colorFilter = colorFilter |
| 296 | } |
| 297 | |
| 298 | /** |
| 299 | * Deprecated, but required by Drawable |
| 300 | */ |
| 301 | override fun getOpacity(): Int { |
| 302 | return PixelFormat.OPAQUE |
| 303 | } |
| 304 | |
| 305 | override fun getIntrinsicHeight(): Int { |
| 306 | return intrinsicHeight |
| 307 | } |
| 308 | |
| 309 | override fun getIntrinsicWidth(): Int { |
| 310 | return intrinsicWidth |
| 311 | } |
| 312 | |
| 313 | /** |
| 314 | * Set the fill level |
| 315 | */ |
| 316 | public open fun setBatteryLevel(l: Int) { |
| 317 | invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon |
| 318 | level = l |
| 319 | levelColor = batteryColorForLevel(level) |
| 320 | invalidateSelf() |
| 321 | } |
| 322 | |
| 323 | public fun getBatteryLevel(): Int { |
| 324 | return level |
| 325 | } |
| 326 | |
| 327 | override fun onBoundsChange(bounds: Rect?) { |
| 328 | super.onBoundsChange(bounds) |
| 329 | updateSize() |
| 330 | } |
| 331 | |
| 332 | fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { |
| 333 | padding.left = left |
| 334 | padding.top = top |
| 335 | padding.right = right |
| 336 | padding.bottom = bottom |
| 337 | |
| 338 | updateSize() |
| 339 | } |
| 340 | |
| 341 | fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) { |
| 342 | fillColor = if (dualTone) fgColor else singleToneColor |
| 343 | |
| 344 | fillPaint.color = fillColor |
| 345 | fillColorStrokePaint.color = fillColor |
| 346 | |
| 347 | backgroundColor = bgColor |
| 348 | dualToneBackgroundFill.color = bgColor |
| 349 | |
| 350 | invalidateSelf() |
| 351 | } |
| 352 | |
| 353 | private fun postInvalidate() { |
| 354 | unscheduleSelf(invalidateRunnable) |
| 355 | scheduleSelf(invalidateRunnable, 0) |
| 356 | } |
| 357 | |
| 358 | private fun updateSize() { |
| 359 | val b = bounds |
| 360 | if (b.isEmpty) { |
| 361 | scaleMatrix.setScale(1f, 1f) |
| 362 | } else { |
| 363 | scaleMatrix.setScale((b.right / Companion.WIDTH), (b.bottom / Companion.HEIGHT)) |
| 364 | } |
| 365 | |
| 366 | perimeterPath.transform(scaleMatrix, scaledPerimeter) |
| 367 | fillMask.transform(scaleMatrix, scaledFill) |
| 368 | scaledFill.computeBounds(fillRect, true) |
| 369 | boltPath.transform(scaleMatrix, scaledBolt) |
| 370 | plusPath.transform(scaleMatrix, scaledPlus) |
| 371 | } |
| 372 | |
| 373 | private fun loadPaths() { |
| 374 | val pathString = context.resources.getString( |
| 375 | com.android.internal.R.string.config_batterymeterPerimeterPath) |
| 376 | perimeterPath.set(PathParser.createPathFromPathData(pathString)) |
| 377 | val b = RectF() |
| 378 | perimeterPath.computeBounds(b, true) |
| 379 | |
| 380 | val fillMaskString = context.resources.getString( |
| 381 | com.android.internal.R.string.config_batterymeterFillMask) |
| 382 | fillMask.set(PathParser.createPathFromPathData(fillMaskString)) |
| 383 | // Set the fill rect so we can calculate the fill properly |
| 384 | fillMask.computeBounds(fillRect, true) |
| 385 | |
| 386 | val boltPathString = context.resources.getString( |
| 387 | com.android.internal.R.string.config_batterymeterBoltPath) |
| 388 | boltPath.set(PathParser.createPathFromPathData(boltPathString)) |
| 389 | |
| 390 | val plusPathString = context.resources.getString( |
| 391 | com.android.internal.R.string.config_batterymeterPowersavePath) |
| 392 | plusPath.set(PathParser.createPathFromPathData(plusPathString)) |
| 393 | |
| 394 | dualTone = context.resources.getBoolean( |
| 395 | com.android.internal.R.bool.config_batterymeterDualTone) |
| 396 | } |
| 397 | |
| 398 | companion object { |
| 399 | private const val TAG = "ThemedBatteryDrawable" |
| 400 | private const val WIDTH = 12f |
| 401 | private const val HEIGHT = 20f |
| 402 | private const val CRITICAL_LEVEL = 15 |
| 403 | } |
| 404 | } |