blob: 337106bb5f1bebe5df0a60a317d35f4e7d6bd8d8 [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
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}