blob: 3a2397f671e0047499b09bc8e01409dffb460dfc [file] [log] [blame]
jackqdyulei597b10f2017-01-27 13:31:40 -08001/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.settingslib.graph;
18
19import android.animation.ArgbEvaluator;
20import android.annotation.Nullable;
21import android.content.Context;
22import android.content.res.Resources;
23import android.content.res.TypedArray;
24import android.graphics.Canvas;
25import android.graphics.Color;
26import android.graphics.ColorFilter;
27import android.graphics.Paint;
28import android.graphics.Path;
29import android.graphics.RectF;
30import android.graphics.Typeface;
31import android.graphics.drawable.Drawable;
Jason Monk58be7a62017-02-01 20:17:51 -050032import android.util.TypedValue;
33
jackqdyulei597b10f2017-01-27 13:31:40 -080034import com.android.settingslib.R;
35import com.android.settingslib.Utils;
36
37public class BatteryMeterDrawableBase extends Drawable {
38
39 private static final float ASPECT_RATIO = 9.5f / 14.5f;
40 public static final String TAG = BatteryMeterDrawableBase.class.getSimpleName();
jackqdyulei597b10f2017-01-27 13:31:40 -080041
42 protected final Context mContext;
jackqdyuleiff5bd942017-04-04 10:54:21 -070043 protected final Paint mFramePaint;
44 protected final Paint mBatteryPaint;
45 protected final Paint mWarningTextPaint;
46 protected final Paint mTextPaint;
47 protected final Paint mBoltPaint;
48 protected final Paint mPlusPaint;
jackqdyulei597b10f2017-01-27 13:31:40 -080049
Dan Sandler6aa6e6e2017-02-07 19:12:25 -080050 private int mLevel = -1;
jackqdyuleiff5bd942017-04-04 10:54:21 -070051 private boolean mCharging;
Dan Sandler6aa6e6e2017-02-07 19:12:25 -080052 private boolean mPowerSaveEnabled;
53 private boolean mShowPercent;
jackqdyulei597b10f2017-01-27 13:31:40 -080054
55 private static final boolean SINGLE_DIGIT_PERCENT = false;
56
57 private static final int FULL = 96;
58
59 private static final float BOLT_LEVEL_THRESHOLD = 0.3f; // opaque bolt below this fraction
60
61 private final int[] mColors;
62 private final int mIntrinsicWidth;
63 private final int mIntrinsicHeight;
64
65 private float mButtonHeightFraction;
66 private float mSubpixelSmoothingLeft;
67 private float mSubpixelSmoothingRight;
jackqdyulei597b10f2017-01-27 13:31:40 -080068 private float mTextHeight, mWarningTextHeight;
69 private int mIconTint = Color.WHITE;
Jason Monkd866b8a2017-03-29 15:30:44 -040070 private float mOldDarkIntensity = -1f;
jackqdyulei597b10f2017-01-27 13:31:40 -080071
72 private int mHeight;
73 private int mWidth;
74 private String mWarningString;
75 private final int mCriticalLevel;
76 private int mChargeColor;
77 private final float[] mBoltPoints;
78 private final Path mBoltPath = new Path();
79 private final float[] mPlusPoints;
80 private final Path mPlusPath = new Path();
81
82 private final RectF mFrame = new RectF();
83 private final RectF mButtonFrame = new RectF();
84 private final RectF mBoltFrame = new RectF();
85 private final RectF mPlusFrame = new RectF();
86
87 private final Path mShapePath = new Path();
88 private final Path mClipPath = new Path();
89 private final Path mTextPath = new Path();
90
jackqdyulei597b10f2017-01-27 13:31:40 -080091 public BatteryMeterDrawableBase(Context context, int frameColor) {
92 mContext = context;
93 final Resources res = context.getResources();
94 TypedArray levels = res.obtainTypedArray(R.array.batterymeter_color_levels);
95 TypedArray colors = res.obtainTypedArray(R.array.batterymeter_color_values);
96
97 final int N = levels.length();
98 mColors = new int[2 * N];
Jason Monk58be7a62017-02-01 20:17:51 -050099 for (int i=0; i < N; i++) {
jackqdyulei597b10f2017-01-27 13:31:40 -0800100 mColors[2 * i] = levels.getInt(i, 0);
Jason Monk58be7a62017-02-01 20:17:51 -0500101 if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) {
Jason Monk7c8564e2017-03-29 15:21:36 -0400102 mColors[2 * i + 1] = Utils.getColorAttr(context, colors.getThemeAttributeId(i, 0));
Jason Monk58be7a62017-02-01 20:17:51 -0500103 } else {
104 mColors[2 * i + 1] = colors.getColor(i, 0);
105 }
jackqdyulei597b10f2017-01-27 13:31:40 -0800106 }
107 levels.recycle();
108 colors.recycle();
Dan Sandler6aa6e6e2017-02-07 19:12:25 -0800109
jackqdyulei597b10f2017-01-27 13:31:40 -0800110 mWarningString = context.getString(R.string.battery_meter_very_low_overlay_symbol);
111 mCriticalLevel = mContext.getResources().getInteger(
112 com.android.internal.R.integer.config_criticalBatteryWarningLevel);
113 mButtonHeightFraction = context.getResources().getFraction(
114 R.fraction.battery_button_height_fraction, 1, 1);
115 mSubpixelSmoothingLeft = context.getResources().getFraction(
116 R.fraction.battery_subpixel_smoothing_left, 1, 1);
117 mSubpixelSmoothingRight = context.getResources().getFraction(
118 R.fraction.battery_subpixel_smoothing_right, 1, 1);
119
120 mFramePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
121 mFramePaint.setColor(frameColor);
122 mFramePaint.setDither(true);
123 mFramePaint.setStrokeWidth(0);
124 mFramePaint.setStyle(Paint.Style.FILL_AND_STROKE);
125
126 mBatteryPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
127 mBatteryPaint.setDither(true);
128 mBatteryPaint.setStrokeWidth(0);
129 mBatteryPaint.setStyle(Paint.Style.FILL_AND_STROKE);
130
131 mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
132 Typeface font = Typeface.create("sans-serif-condensed", Typeface.BOLD);
133 mTextPaint.setTypeface(font);
134 mTextPaint.setTextAlign(Paint.Align.CENTER);
135
136 mWarningTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
137 font = Typeface.create("sans-serif", Typeface.BOLD);
138 mWarningTextPaint.setTypeface(font);
139 mWarningTextPaint.setTextAlign(Paint.Align.CENTER);
140 if (mColors.length > 1) {
141 mWarningTextPaint.setColor(mColors[1]);
142 }
143
144 mChargeColor = Utils.getDefaultColor(mContext, R.color.batterymeter_charge_color);
145
146 mBoltPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
147 mBoltPaint.setColor(Utils.getDefaultColor(mContext, R.color.batterymeter_bolt_color));
Dan Sandler6aa6e6e2017-02-07 19:12:25 -0800148 mBoltPoints = loadPoints(res, R.array.batterymeter_bolt_points);
jackqdyulei597b10f2017-01-27 13:31:40 -0800149
150 mPlusPaint = new Paint(mBoltPaint);
Dan Sandler6aa6e6e2017-02-07 19:12:25 -0800151 mPlusPoints = loadPoints(res, R.array.batterymeter_plus_points);
jackqdyulei597b10f2017-01-27 13:31:40 -0800152
jackqdyulei597b10f2017-01-27 13:31:40 -0800153 mIntrinsicWidth = context.getResources().getDimensionPixelSize(R.dimen.battery_width);
154 mIntrinsicHeight = context.getResources().getDimensionPixelSize(R.dimen.battery_height);
155 }
156
157 @Override
158 public int getIntrinsicHeight() {
159 return mIntrinsicHeight;
160 }
161
162 @Override
163 public int getIntrinsicWidth() {
164 return mIntrinsicWidth;
165 }
166
Dan Sandler6aa6e6e2017-02-07 19:12:25 -0800167 public void setShowPercent(boolean show) {
168 mShowPercent = show;
jackqdyulei597b10f2017-01-27 13:31:40 -0800169 postInvalidate();
170 }
171
jackqdyuleiff5bd942017-04-04 10:54:21 -0700172 public void setCharging(boolean val) {
173 mCharging = val;
Dan Sandler6aa6e6e2017-02-07 19:12:25 -0800174 postInvalidate();
175 }
176
jackqdyuleiff5bd942017-04-04 10:54:21 -0700177 public boolean getCharging() {
178 return mCharging;
179 }
180
Dan Sandler6aa6e6e2017-02-07 19:12:25 -0800181 public void setBatteryLevel(int val) {
182 mLevel = val;
183 postInvalidate();
184 }
185
jackqdyuleiff5bd942017-04-04 10:54:21 -0700186 public int getBatteryLevel() {
187 return mLevel;
188 }
189
Dan Sandler6aa6e6e2017-02-07 19:12:25 -0800190 public void setPowerSave(boolean val) {
191 mPowerSaveEnabled = val;
192 postInvalidate();
193 }
194
195 // an approximation of View.postInvalidate()
jackqdyulei597b10f2017-01-27 13:31:40 -0800196 protected void postInvalidate() {
Dan Sandler6aa6e6e2017-02-07 19:12:25 -0800197 unscheduleSelf(this::invalidateSelf);
jackqdyulei597b10f2017-01-27 13:31:40 -0800198 scheduleSelf(this::invalidateSelf, 0);
199 }
200
Dan Sandler6aa6e6e2017-02-07 19:12:25 -0800201 private static float[] loadPoints(Resources res, int pointArrayRes) {
202 final int[] pts = res.getIntArray(pointArrayRes);
jackqdyulei597b10f2017-01-27 13:31:40 -0800203 int maxX = 0, maxY = 0;
204 for (int i = 0; i < pts.length; i += 2) {
205 maxX = Math.max(maxX, pts[i]);
206 maxY = Math.max(maxY, pts[i + 1]);
207 }
208 final float[] ptsF = new float[pts.length];
209 for (int i = 0; i < pts.length; i += 2) {
210 ptsF[i] = (float) pts[i] / maxX;
211 ptsF[i + 1] = (float) pts[i + 1] / maxY;
212 }
213 return ptsF;
214 }
215
216 @Override
217 public void setBounds(int left, int top, int right, int bottom) {
218 super.setBounds(left, top, right, bottom);
219 mHeight = bottom - top;
220 mWidth = right - left;
221 mWarningTextPaint.setTextSize(mHeight * 0.75f);
222 mWarningTextHeight = -mWarningTextPaint.getFontMetrics().ascent;
223 }
224
jackqdyulei597b10f2017-01-27 13:31:40 -0800225 private int getColorForLevel(int percent) {
226 // If we are in power save mode, always use the normal color.
227 if (mPowerSaveEnabled) {
Jason Monkbb769052017-05-10 10:39:16 -0400228 return mIconTint;
jackqdyulei597b10f2017-01-27 13:31:40 -0800229 }
230 int thresh, color = 0;
231 for (int i = 0; i < mColors.length; i += 2) {
232 thresh = mColors[i];
233 color = mColors[i + 1];
234 if (percent <= thresh) {
235
236 // Respect tinting for "normal" level
237 if (i == mColors.length - 2) {
238 return mIconTint;
239 } else {
240 return color;
241 }
242 }
243 }
244 return color;
245 }
246
jackqdyulei597b10f2017-01-27 13:31:40 -0800247 public void setColors(int fillColor, int backgroundColor) {
248 mIconTint = fillColor;
249 mFramePaint.setColor(backgroundColor);
250 mBoltPaint.setColor(fillColor);
251 mChargeColor = fillColor;
252 invalidateSelf();
253 }
254
jackqdyulei597b10f2017-01-27 13:31:40 -0800255 @Override
256 public void draw(Canvas c) {
257 final int level = mLevel;
258
259 if (level == -1) return;
260
261 float drawFrac = (float) level / 100f;
262 final int height = mHeight;
263 final int width = (int) (ASPECT_RATIO * mHeight);
264 int px = (mWidth - width) / 2;
265
266 final int buttonHeight = (int) (height * mButtonHeightFraction);
267
268 mFrame.set(0, 0, width, height);
269 mFrame.offset(px, 0);
270
271 // button-frame: area above the battery body
272 mButtonFrame.set(
273 mFrame.left + Math.round(width * 0.25f),
274 mFrame.top,
275 mFrame.right - Math.round(width * 0.25f),
276 mFrame.top + buttonHeight);
277
278 mButtonFrame.top += mSubpixelSmoothingLeft;
279 mButtonFrame.left += mSubpixelSmoothingLeft;
280 mButtonFrame.right -= mSubpixelSmoothingRight;
281
282 // frame: battery body area
283 mFrame.top += buttonHeight;
284 mFrame.left += mSubpixelSmoothingLeft;
285 mFrame.top += mSubpixelSmoothingLeft;
286 mFrame.right -= mSubpixelSmoothingRight;
287 mFrame.bottom -= mSubpixelSmoothingRight;
288
289 // set the battery charging color
jackqdyuleiff5bd942017-04-04 10:54:21 -0700290 mBatteryPaint.setColor(mCharging ? mChargeColor : getColorForLevel(level));
jackqdyulei597b10f2017-01-27 13:31:40 -0800291
292 if (level >= FULL) {
293 drawFrac = 1f;
294 } else if (level <= mCriticalLevel) {
295 drawFrac = 0f;
296 }
297
298 final float levelTop = drawFrac == 1f ? mButtonFrame.top
299 : (mFrame.top + (mFrame.height() * (1f - drawFrac)));
300
301 // define the battery shape
302 mShapePath.reset();
303 mShapePath.moveTo(mButtonFrame.left, mButtonFrame.top);
304 mShapePath.lineTo(mButtonFrame.right, mButtonFrame.top);
305 mShapePath.lineTo(mButtonFrame.right, mFrame.top);
306 mShapePath.lineTo(mFrame.right, mFrame.top);
307 mShapePath.lineTo(mFrame.right, mFrame.bottom);
308 mShapePath.lineTo(mFrame.left, mFrame.bottom);
309 mShapePath.lineTo(mFrame.left, mFrame.top);
310 mShapePath.lineTo(mButtonFrame.left, mFrame.top);
311 mShapePath.lineTo(mButtonFrame.left, mButtonFrame.top);
312
jackqdyuleiff5bd942017-04-04 10:54:21 -0700313 if (mCharging) {
jackqdyulei597b10f2017-01-27 13:31:40 -0800314 // define the bolt shape
315 final float bl = mFrame.left + mFrame.width() / 4f;
316 final float bt = mFrame.top + mFrame.height() / 6f;
317 final float br = mFrame.right - mFrame.width() / 4f;
318 final float bb = mFrame.bottom - mFrame.height() / 10f;
319 if (mBoltFrame.left != bl || mBoltFrame.top != bt
320 || mBoltFrame.right != br || mBoltFrame.bottom != bb) {
321 mBoltFrame.set(bl, bt, br, bb);
322 mBoltPath.reset();
323 mBoltPath.moveTo(
324 mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(),
325 mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height());
326 for (int i = 2; i < mBoltPoints.length; i += 2) {
327 mBoltPath.lineTo(
328 mBoltFrame.left + mBoltPoints[i] * mBoltFrame.width(),
329 mBoltFrame.top + mBoltPoints[i + 1] * mBoltFrame.height());
330 }
331 mBoltPath.lineTo(
332 mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(),
333 mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height());
334 }
335
336 float boltPct = (mBoltFrame.bottom - levelTop) / (mBoltFrame.bottom - mBoltFrame.top);
337 boltPct = Math.min(Math.max(boltPct, 0), 1);
338 if (boltPct <= BOLT_LEVEL_THRESHOLD) {
339 // draw the bolt if opaque
340 c.drawPath(mBoltPath, mBoltPaint);
341 } else {
342 // otherwise cut the bolt out of the overall shape
343 mShapePath.op(mBoltPath, Path.Op.DIFFERENCE);
344 }
345 } else if (mPowerSaveEnabled) {
346 // define the plus shape
347 final float pw = mFrame.width() * 2 / 3;
348 final float pl = mFrame.left + (mFrame.width() - pw) / 2;
349 final float pt = mFrame.top + (mFrame.height() - pw) / 2;
350 final float pr = mFrame.right - (mFrame.width() - pw) / 2;
351 final float pb = mFrame.bottom - (mFrame.height() - pw) / 2;
352 if (mPlusFrame.left != pl || mPlusFrame.top != pt
353 || mPlusFrame.right != pr || mPlusFrame.bottom != pb) {
354 mPlusFrame.set(pl, pt, pr, pb);
355 mPlusPath.reset();
356 mPlusPath.moveTo(
357 mPlusFrame.left + mPlusPoints[0] * mPlusFrame.width(),
358 mPlusFrame.top + mPlusPoints[1] * mPlusFrame.height());
359 for (int i = 2; i < mPlusPoints.length; i += 2) {
360 mPlusPath.lineTo(
361 mPlusFrame.left + mPlusPoints[i] * mPlusFrame.width(),
362 mPlusFrame.top + mPlusPoints[i + 1] * mPlusFrame.height());
363 }
364 mPlusPath.lineTo(
365 mPlusFrame.left + mPlusPoints[0] * mPlusFrame.width(),
366 mPlusFrame.top + mPlusPoints[1] * mPlusFrame.height());
367 }
368
369 float boltPct = (mPlusFrame.bottom - levelTop) / (mPlusFrame.bottom - mPlusFrame.top);
370 boltPct = Math.min(Math.max(boltPct, 0), 1);
371 if (boltPct <= BOLT_LEVEL_THRESHOLD) {
372 // draw the bolt if opaque
373 c.drawPath(mPlusPath, mPlusPaint);
374 } else {
375 // otherwise cut the bolt out of the overall shape
376 mShapePath.op(mPlusPath, Path.Op.DIFFERENCE);
377 }
378 }
379
380 // compute percentage text
381 boolean pctOpaque = false;
382 float pctX = 0, pctY = 0;
383 String pctText = null;
jackqdyuleiff5bd942017-04-04 10:54:21 -0700384 if (!mCharging && !mPowerSaveEnabled && level > mCriticalLevel && mShowPercent) {
jackqdyulei597b10f2017-01-27 13:31:40 -0800385 mTextPaint.setColor(getColorForLevel(level));
386 mTextPaint.setTextSize(height *
387 (SINGLE_DIGIT_PERCENT ? 0.75f
388 : (mLevel == 100 ? 0.38f : 0.5f)));
389 mTextHeight = -mTextPaint.getFontMetrics().ascent;
390 pctText = String.valueOf(SINGLE_DIGIT_PERCENT ? (level / 10) : level);
391 pctX = mWidth * 0.5f;
392 pctY = (mHeight + mTextHeight) * 0.47f;
393 pctOpaque = levelTop > pctY;
394 if (!pctOpaque) {
395 mTextPath.reset();
396 mTextPaint.getTextPath(pctText, 0, pctText.length(), pctX, pctY, mTextPath);
397 // cut the percentage text out of the overall shape
398 mShapePath.op(mTextPath, Path.Op.DIFFERENCE);
399 }
400 }
401
402 // draw the battery shape background
403 c.drawPath(mShapePath, mFramePaint);
404
405 // draw the battery shape, clipped to charging level
406 mFrame.top = levelTop;
407 mClipPath.reset();
408 mClipPath.addRect(mFrame, Path.Direction.CCW);
409 mShapePath.op(mClipPath, Path.Op.INTERSECT);
410 c.drawPath(mShapePath, mBatteryPaint);
411
jackqdyuleiff5bd942017-04-04 10:54:21 -0700412 if (!mCharging && !mPowerSaveEnabled) {
jackqdyulei597b10f2017-01-27 13:31:40 -0800413 if (level <= mCriticalLevel) {
414 // draw the warning text
415 final float x = mWidth * 0.5f;
416 final float y = (mHeight + mWarningTextHeight) * 0.48f;
417 c.drawText(mWarningString, x, y, mWarningTextPaint);
418 } else if (pctOpaque) {
419 // draw the percentage text
420 c.drawText(pctText, pctX, pctY, mTextPaint);
421 }
422 }
423 }
424
425 // Some stuff required by Drawable.
426 @Override
427 public void setAlpha(int alpha) {
428 }
429
430 @Override
431 public void setColorFilter(@Nullable ColorFilter colorFilter) {
432 mFramePaint.setColorFilter(colorFilter);
433 mBatteryPaint.setColorFilter(colorFilter);
434 mWarningTextPaint.setColorFilter(colorFilter);
435 mBoltPaint.setColorFilter(colorFilter);
436 mPlusPaint.setColorFilter(colorFilter);
437 }
438
439 @Override
440 public int getOpacity() {
441 return 0;
442 }
jackqdyuleiff5bd942017-04-04 10:54:21 -0700443
444 public int getCriticalLevel() {
445 return mCriticalLevel;
446 }
jackqdyulei597b10f2017-01-27 13:31:40 -0800447}