blob: a62872cd0ffddfa1c19ce6a91caabb3f1836acbd [file] [log] [blame]
Evan Laird2259da42019-02-08 16:48:53 -05001/*
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
15package com.android.settingslib.graph
16
17import android.content.Context
18import android.graphics.BlendMode
19import android.graphics.Canvas
20import android.graphics.Color
21import android.graphics.ColorFilter
22import android.graphics.Matrix
23import android.graphics.Paint
24import android.graphics.Path
25import android.graphics.PixelFormat
26import android.graphics.Rect
27import android.graphics.RectF
28import android.graphics.drawable.Drawable
29import android.util.PathParser
30import android.util.TypedValue
31
32import com.android.settingslib.R
33import 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 */
39open 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
Evan Lairdf4f8a562019-02-27 14:34:19 -050090 open var criticalLevel: Int = context.resources.getInteger(
91 com.android.internal.R.integer.config_criticalBatteryWarningLevel)
Evan Laird2259da42019-02-08 16:48:53 -050092
93 var charging = false
94 set(value) {
95 field = value
96 postInvalidate()
97 }
98
99 var powerSaveEnabled = false
100 set(value) {
101 field = value
102 postInvalidate()
103 }
104
Evan Lairdf4f8a562019-02-27 14:34:19 -0500105 private val fillColorStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
Evan Laird2259da42019-02-08 16:48:53 -0500106 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
Evan Lairdf4f8a562019-02-27 14:34:19 -0500112 p.strokeJoin = Paint.Join.ROUND
Evan Laird2259da42019-02-08 16:48:53 -0500113 }
114
Evan Lairdf4f8a562019-02-27 14:34:19 -0500115 private val fillColorStrokeProtection = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
Evan Laird2259da42019-02-08 16:48:53 -0500116 p.isDither = true
117 p.strokeWidth = 5f
118 p.style = Paint.Style.STROKE
119 p.blendMode = BlendMode.CLEAR
120 p.strokeMiter = 5f
Evan Lairdf4f8a562019-02-27 14:34:19 -0500121 p.strokeJoin = Paint.Join.ROUND
Evan Laird2259da42019-02-08 16:48:53 -0500122 }
123
Evan Lairdf4f8a562019-02-27 14:34:19 -0500124 private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
Evan Laird2259da42019-02-08 16:48:53 -0500125 p.color = frameColor
126 p.alpha = 255
127 p.isDither = true
128 p.strokeWidth = 0f
129 p.style = Paint.Style.FILL_AND_STROKE
Evan Laird2259da42019-02-08 16:48:53 -0500130 }
131
Evan Laird64e86d62019-03-06 20:45:36 -0500132 private val errorPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
133 p.color = Utils.getColorErrorDefaultColor(context)
134 p.alpha = 255
135 p.isDither = true
136 p.strokeWidth = 0f
137 p.style = Paint.Style.FILL_AND_STROKE
138 }
139
Evan Laird2259da42019-02-08 16:48:53 -0500140 // Only used if dualTone is set to true
Evan Lairdf4f8a562019-02-27 14:34:19 -0500141 private val dualToneBackgroundFill = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
Evan Laird2259da42019-02-08 16:48:53 -0500142 p.color = frameColor
143 p.alpha = 255
144 p.isDither = true
145 p.strokeWidth = 0f
146 p.style = Paint.Style.FILL_AND_STROKE
Evan Laird2259da42019-02-08 16:48:53 -0500147 }
148
149 init {
150 val density = context.resources.displayMetrics.density
151 intrinsicHeight = (Companion.HEIGHT * density).toInt()
152 intrinsicWidth = (Companion.WIDTH * density).toInt()
153
154 val res = context.resources
155 val levels = res.obtainTypedArray(R.array.batterymeter_color_levels)
156 val colors = res.obtainTypedArray(R.array.batterymeter_color_values)
157 val N = levels.length()
158 colorLevels = IntArray(2 * N)
159 for (i in 0 until N) {
160 colorLevels[2 * i] = levels.getInt(i, 0)
161 if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) {
162 colorLevels[2 * i + 1] = Utils.getColorAttrDefaultColor(context,
163 colors.getThemeAttributeId(i, 0))
164 } else {
165 colorLevels[2 * i + 1] = colors.getColor(i, 0)
166 }
167 }
168 levels.recycle()
169 colors.recycle()
170
Evan Laird2259da42019-02-08 16:48:53 -0500171 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)
Evan Laird64e86d62019-03-06 20:45:36 -0500190 // If drawing dual tone, the level is used only to clip the whole drawable path
Evan Laird2259da42019-02-08 16:48:53 -0500191 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)
Evan Laird64e86d62019-03-06 20:45:36 -0500207 c.drawPath(scaledPlus, errorPaint)
Evan Laird2259da42019-02-08 16:48:53 -0500208 }
209
210 if (dualTone) {
211 // Dual tone means we draw the shape again, clipped to the charge level
212 c.drawPath(unifiedPath, dualToneBackgroundFill)
213 c.save()
214 c.clipRect(0f,
215 bounds.bottom - bounds.height() * fillFraction,
216 bounds.right.toFloat(),
217 bounds.bottom.toFloat())
218 c.drawPath(unifiedPath, fillPaint)
219 c.restore()
220 } else {
221 // Non dual-tone means we draw the perimeter (with the level fill), and potentially
222 // draw the fill again with a critical color
223 fillPaint.color = fillColor
224 c.drawPath(unifiedPath, fillPaint)
225 fillPaint.color = levelColor
226
227 // Show colorError below this level
228 if (level <= Companion.CRITICAL_LEVEL && !charging) {
229 c.save()
230 c.clipPath(scaledFill)
231 c.drawPath(levelPath, fillPaint)
232 c.restore()
233 }
234 }
235
236 if (charging) {
237 c.clipOutPath(scaledBolt)
238 if (invertFillIcon) {
239 c.drawPath(scaledBolt, fillColorStrokePaint)
240 } else {
241 c.drawPath(scaledBolt, fillColorStrokeProtection)
242 }
243 } else if (powerSaveEnabled) {
Evan Laird64e86d62019-03-06 20:45:36 -0500244 // If power save is enabled draw the perimeter path with colorError
245 c.drawPath(scaledPerimeter, errorPaint)
246
247 // But always put path protection around the plus sign
Evan Laird2259da42019-02-08 16:48:53 -0500248 c.clipOutPath(scaledPlus)
Evan Laird64e86d62019-03-06 20:45:36 -0500249 c.drawPath(scaledPlus, fillColorStrokeProtection)
Evan Laird2259da42019-02-08 16:48:53 -0500250 }
251 }
252
253 private fun batteryColorForLevel(level: Int): Int {
254 return when {
Evan Lairdf4f8a562019-02-27 14:34:19 -0500255 charging || powerSaveEnabled -> fillColor
Evan Laird2259da42019-02-08 16:48:53 -0500256 else -> getColorForLevel(level)
257 }
258 }
259
260 private fun getColorForLevel(level: Int): Int {
261 var thresh: Int
262 var color = 0
263 var i = 0
264 while (i < colorLevels.size) {
265 thresh = colorLevels[i]
266 color = colorLevels[i + 1]
267 if (level <= thresh) {
268
269 // Respect tinting for "normal" level
270 return if (i == colorLevels.size - 2) {
271 fillColor
272 } else {
273 color
274 }
275 }
276 i += 2
277 }
278 return color
279 }
280
281 /**
282 * Alpha is unused internally, and should be defined in the colors passed to {@link setColors}.
283 * Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds
284 * defining the minimum background fill alpha. This is because fill + background must be equal
285 * to the net alpha passed in here.
286 */
287 override fun setAlpha(alpha: Int) {
288 }
289
290 override fun setColorFilter(colorFilter: ColorFilter?) {
291 fillPaint.colorFilter = colorFilter
292 fillColorStrokePaint.colorFilter = colorFilter
293 dualToneBackgroundFill.colorFilter = colorFilter
294 }
295
296 /**
297 * Deprecated, but required by Drawable
298 */
299 override fun getOpacity(): Int {
300 return PixelFormat.OPAQUE
301 }
302
303 override fun getIntrinsicHeight(): Int {
304 return intrinsicHeight
305 }
306
307 override fun getIntrinsicWidth(): Int {
308 return intrinsicWidth
309 }
310
311 /**
312 * Set the fill level
313 */
314 public open fun setBatteryLevel(l: Int) {
315 invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon
316 level = l
317 levelColor = batteryColorForLevel(level)
318 invalidateSelf()
319 }
320
321 public fun getBatteryLevel(): Int {
322 return level
323 }
324
325 override fun onBoundsChange(bounds: Rect?) {
326 super.onBoundsChange(bounds)
327 updateSize()
328 }
329
330 fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
331 padding.left = left
332 padding.top = top
333 padding.right = right
334 padding.bottom = bottom
335
336 updateSize()
337 }
338
339 fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) {
340 fillColor = if (dualTone) fgColor else singleToneColor
341
342 fillPaint.color = fillColor
343 fillColorStrokePaint.color = fillColor
344
345 backgroundColor = bgColor
346 dualToneBackgroundFill.color = bgColor
347
Evan Lairdf4f8a562019-02-27 14:34:19 -0500348 // Also update the level color, since fillColor may have changed
349 levelColor = batteryColorForLevel(level)
350
Evan Laird2259da42019-02-08 16:48:53 -0500351 invalidateSelf()
352 }
353
354 private fun postInvalidate() {
355 unscheduleSelf(invalidateRunnable)
356 scheduleSelf(invalidateRunnable, 0)
357 }
358
359 private fun updateSize() {
360 val b = bounds
361 if (b.isEmpty) {
362 scaleMatrix.setScale(1f, 1f)
363 } else {
Evan Lairdf4f8a562019-02-27 14:34:19 -0500364 scaleMatrix.setScale((b.right / WIDTH), (b.bottom / HEIGHT))
Evan Laird2259da42019-02-08 16:48:53 -0500365 }
366
367 perimeterPath.transform(scaleMatrix, scaledPerimeter)
368 fillMask.transform(scaleMatrix, scaledFill)
369 scaledFill.computeBounds(fillRect, true)
370 boltPath.transform(scaleMatrix, scaledBolt)
371 plusPath.transform(scaleMatrix, scaledPlus)
Evan Lairdf4f8a562019-02-27 14:34:19 -0500372
373 // It is expected that this view only ever scale by the same factor in each dimension, so
374 // just pick one to scale the strokeWidths
375 val scaledStrokeWidth =
376 Math.max(b.right / WIDTH * PROTECTION_STROKE_WIDTH, PROTECTION_MIN_STROKE_WIDTH)
377
378 fillColorStrokePaint.strokeWidth = scaledStrokeWidth
379 fillColorStrokeProtection.strokeWidth = scaledStrokeWidth
Evan Laird2259da42019-02-08 16:48:53 -0500380 }
381
382 private fun loadPaths() {
383 val pathString = context.resources.getString(
384 com.android.internal.R.string.config_batterymeterPerimeterPath)
385 perimeterPath.set(PathParser.createPathFromPathData(pathString))
386 val b = RectF()
387 perimeterPath.computeBounds(b, true)
388
389 val fillMaskString = context.resources.getString(
390 com.android.internal.R.string.config_batterymeterFillMask)
391 fillMask.set(PathParser.createPathFromPathData(fillMaskString))
392 // Set the fill rect so we can calculate the fill properly
393 fillMask.computeBounds(fillRect, true)
394
395 val boltPathString = context.resources.getString(
396 com.android.internal.R.string.config_batterymeterBoltPath)
397 boltPath.set(PathParser.createPathFromPathData(boltPathString))
398
399 val plusPathString = context.resources.getString(
400 com.android.internal.R.string.config_batterymeterPowersavePath)
401 plusPath.set(PathParser.createPathFromPathData(plusPathString))
402
403 dualTone = context.resources.getBoolean(
404 com.android.internal.R.bool.config_batterymeterDualTone)
405 }
406
407 companion object {
408 private const val TAG = "ThemedBatteryDrawable"
409 private const val WIDTH = 12f
410 private const val HEIGHT = 20f
411 private const val CRITICAL_LEVEL = 15
Evan Lairdf4f8a562019-02-27 14:34:19 -0500412 // On a 12x20 grid, how wide to make the fill protection stroke.
413 // Scales when our size changes
414 private const val PROTECTION_STROKE_WIDTH = 1.4f
415 // Arbitrarily chosen for visibility at small sizes
416 private const val PROTECTION_MIN_STROKE_WIDTH = 5f
Evan Laird2259da42019-02-08 16:48:53 -0500417 }
418}