1 /*
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 
17 package com.android.settingslib.graph;
18 
19 import android.annotation.Nullable;
20 import android.content.Context;
21 import android.content.res.Resources;
22 import android.content.res.TypedArray;
23 import android.graphics.Canvas;
24 import android.graphics.Color;
25 import android.graphics.ColorFilter;
26 import android.graphics.Paint;
27 import android.graphics.Paint.Style;
28 import android.graphics.Path;
29 import android.graphics.Path.Direction;
30 import android.graphics.Path.FillType;
31 import android.graphics.Path.Op;
32 import android.graphics.Rect;
33 import android.graphics.RectF;
34 import android.graphics.Typeface;
35 import android.graphics.drawable.Drawable;
36 import android.util.TypedValue;
37 
38 import com.android.settingslib.R;
39 import com.android.settingslib.Utils;
40 
41 public class BatteryMeterDrawableBase extends Drawable {
42 
43     private static final float ASPECT_RATIO = .58f;
44     public static final String TAG = BatteryMeterDrawableBase.class.getSimpleName();
45     private static final float RADIUS_RATIO = 1.0f / 17f;
46 
47     protected final Context mContext;
48     protected final Paint mFramePaint;
49     protected final Paint mBatteryPaint;
50     protected final Paint mWarningTextPaint;
51     protected final Paint mTextPaint;
52     protected final Paint mBoltPaint;
53     protected final Paint mPlusPaint;
54     protected final Paint mPowersavePaint;
55     protected float mButtonHeightFraction;
56 
57     private int mLevel = -1;
58     private boolean mCharging;
59     private boolean mPowerSaveEnabled;
60     protected boolean mPowerSaveAsColorError = true;
61     private boolean mShowPercent;
62 
63     private static final boolean SINGLE_DIGIT_PERCENT = false;
64 
65     private static final int FULL = 96;
66 
67     private static final float BOLT_LEVEL_THRESHOLD = 0.3f;  // opaque bolt below this fraction
68 
69     private final int[] mColors;
70     private final int mIntrinsicWidth;
71     private final int mIntrinsicHeight;
72 
73     private float mSubpixelSmoothingLeft;
74     private float mSubpixelSmoothingRight;
75     private float mTextHeight, mWarningTextHeight;
76     private int mIconTint = Color.WHITE;
77     private float mOldDarkIntensity = -1f;
78 
79     private int mHeight;
80     private int mWidth;
81     private String mWarningString;
82     private final int mCriticalLevel;
83     private int mChargeColor;
84     private final float[] mBoltPoints;
85     private final Path mBoltPath = new Path();
86     private final float[] mPlusPoints;
87     private final Path mPlusPath = new Path();
88 
89     private final Rect mPadding = new Rect();
90     private final RectF mFrame = new RectF();
91     private final RectF mButtonFrame = new RectF();
92     private final RectF mBoltFrame = new RectF();
93     private final RectF mPlusFrame = new RectF();
94 
95     private final Path mShapePath = new Path();
96     private final Path mOutlinePath = new Path();
97     private final Path mTextPath = new Path();
98 
BatteryMeterDrawableBase(Context context, int frameColor)99     public BatteryMeterDrawableBase(Context context, int frameColor) {
100         mContext = context;
101         final Resources res = context.getResources();
102         TypedArray levels = res.obtainTypedArray(R.array.batterymeter_color_levels);
103         TypedArray colors = res.obtainTypedArray(R.array.batterymeter_color_values);
104 
105         final int N = levels.length();
106         mColors = new int[2 * N];
107         for (int i = 0; i < N; i++) {
108             mColors[2 * i] = levels.getInt(i, 0);
109             if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) {
110                 mColors[2 * i + 1] = Utils.getColorAttrDefaultColor(context,
111                         colors.getThemeAttributeId(i, 0));
112             } else {
113                 mColors[2 * i + 1] = colors.getColor(i, 0);
114             }
115         }
116         levels.recycle();
117         colors.recycle();
118 
119         mWarningString = context.getString(R.string.battery_meter_very_low_overlay_symbol);
120         mCriticalLevel = mContext.getResources().getInteger(
121                 com.android.internal.R.integer.config_criticalBatteryWarningLevel);
122         mButtonHeightFraction = context.getResources().getFraction(
123                 R.fraction.battery_button_height_fraction, 1, 1);
124         mSubpixelSmoothingLeft = context.getResources().getFraction(
125                 R.fraction.battery_subpixel_smoothing_left, 1, 1);
126         mSubpixelSmoothingRight = context.getResources().getFraction(
127                 R.fraction.battery_subpixel_smoothing_right, 1, 1);
128 
129         mFramePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
130         mFramePaint.setColor(frameColor);
131         mFramePaint.setDither(true);
132         mFramePaint.setStrokeWidth(0);
133         mFramePaint.setStyle(Paint.Style.FILL_AND_STROKE);
134 
135         mBatteryPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
136         mBatteryPaint.setDither(true);
137         mBatteryPaint.setStrokeWidth(0);
138         mBatteryPaint.setStyle(Paint.Style.FILL_AND_STROKE);
139 
140         mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
141         Typeface font = Typeface.create("sans-serif-condensed", Typeface.BOLD);
142         mTextPaint.setTypeface(font);
143         mTextPaint.setTextAlign(Paint.Align.CENTER);
144 
145         mWarningTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
146         font = Typeface.create("sans-serif", Typeface.BOLD);
147         mWarningTextPaint.setTypeface(font);
148         mWarningTextPaint.setTextAlign(Paint.Align.CENTER);
149         if (mColors.length > 1) {
150             mWarningTextPaint.setColor(mColors[1]);
151         }
152 
153         mChargeColor = Utils.getColorStateListDefaultColor(mContext, R.color.meter_consumed_color);
154 
155         mBoltPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
156         mBoltPaint.setColor(Utils.getColorStateListDefaultColor(mContext,
157                 R.color.batterymeter_bolt_color));
158         mBoltPoints = loadPoints(res, R.array.batterymeter_bolt_points);
159 
160         mPlusPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
161         mPlusPaint.setColor(Utils.getColorStateListDefaultColor(mContext,
162                 R.color.batterymeter_plus_color));
163         mPlusPoints = loadPoints(res, R.array.batterymeter_plus_points);
164 
165         mPowersavePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
166         mPowersavePaint.setColor(mPlusPaint.getColor());
167         mPowersavePaint.setStyle(Style.STROKE);
168         mPowersavePaint.setStrokeWidth(context.getResources()
169                 .getDimensionPixelSize(R.dimen.battery_powersave_outline_thickness));
170 
171         mIntrinsicWidth = context.getResources().getDimensionPixelSize(R.dimen.battery_width);
172         mIntrinsicHeight = context.getResources().getDimensionPixelSize(R.dimen.battery_height);
173     }
174 
175     @Override
getIntrinsicHeight()176     public int getIntrinsicHeight() {
177         return mIntrinsicHeight;
178     }
179 
180     @Override
getIntrinsicWidth()181     public int getIntrinsicWidth() {
182         return mIntrinsicWidth;
183     }
184 
setShowPercent(boolean show)185     public void setShowPercent(boolean show) {
186         mShowPercent = show;
187         postInvalidate();
188     }
189 
setCharging(boolean val)190     public void setCharging(boolean val) {
191         mCharging = val;
192         postInvalidate();
193     }
194 
getCharging()195     public boolean getCharging() {
196         return mCharging;
197     }
198 
setBatteryLevel(int val)199     public void setBatteryLevel(int val) {
200         mLevel = val;
201         postInvalidate();
202     }
203 
getBatteryLevel()204     public int getBatteryLevel() {
205         return mLevel;
206     }
207 
setPowerSave(boolean val)208     public void setPowerSave(boolean val) {
209         mPowerSaveEnabled = val;
210         postInvalidate();
211     }
212 
getPowerSave()213     public boolean getPowerSave() {
214         return mPowerSaveEnabled;
215     }
216 
setPowerSaveAsColorError(boolean asError)217     protected void setPowerSaveAsColorError(boolean asError) {
218         mPowerSaveAsColorError = asError;
219     }
220 
221     // an approximation of View.postInvalidate()
postInvalidate()222     protected void postInvalidate() {
223         unscheduleSelf(this::invalidateSelf);
224         scheduleSelf(this::invalidateSelf, 0);
225     }
226 
loadPoints(Resources res, int pointArrayRes)227     private static float[] loadPoints(Resources res, int pointArrayRes) {
228         final int[] pts = res.getIntArray(pointArrayRes);
229         int maxX = 0, maxY = 0;
230         for (int i = 0; i < pts.length; i += 2) {
231             maxX = Math.max(maxX, pts[i]);
232             maxY = Math.max(maxY, pts[i + 1]);
233         }
234         final float[] ptsF = new float[pts.length];
235         for (int i = 0; i < pts.length; i += 2) {
236             ptsF[i] = (float) pts[i] / maxX;
237             ptsF[i + 1] = (float) pts[i + 1] / maxY;
238         }
239         return ptsF;
240     }
241 
242     @Override
setBounds(int left, int top, int right, int bottom)243     public void setBounds(int left, int top, int right, int bottom) {
244         super.setBounds(left, top, right, bottom);
245         updateSize();
246     }
247 
updateSize()248     private void updateSize() {
249         final Rect bounds = getBounds();
250 
251         mHeight = (bounds.bottom - mPadding.bottom) - (bounds.top + mPadding.top);
252         mWidth = (bounds.right - mPadding.right) - (bounds.left + mPadding.left);
253         mWarningTextPaint.setTextSize(mHeight * 0.75f);
254         mWarningTextHeight = -mWarningTextPaint.getFontMetrics().ascent;
255     }
256 
257     @Override
getPadding(Rect padding)258     public boolean getPadding(Rect padding) {
259         if (mPadding.left == 0
260             && mPadding.top == 0
261             && mPadding.right == 0
262             && mPadding.bottom == 0) {
263             return super.getPadding(padding);
264         }
265 
266         padding.set(mPadding);
267         return true;
268     }
269 
setPadding(int left, int top, int right, int bottom)270     public void setPadding(int left, int top, int right, int bottom) {
271         mPadding.left = left;
272         mPadding.top = top;
273         mPadding.right = right;
274         mPadding.bottom = bottom;
275 
276         updateSize();
277     }
278 
getColorForLevel(int percent)279     private int getColorForLevel(int percent) {
280         int thresh, color = 0;
281         for (int i = 0; i < mColors.length; i += 2) {
282             thresh = mColors[i];
283             color = mColors[i + 1];
284             if (percent <= thresh) {
285 
286                 // Respect tinting for "normal" level
287                 if (i == mColors.length - 2) {
288                     return mIconTint;
289                 } else {
290                     return color;
291                 }
292             }
293         }
294         return color;
295     }
296 
setColors(int fillColor, int backgroundColor)297     public void setColors(int fillColor, int backgroundColor) {
298         mIconTint = fillColor;
299         mFramePaint.setColor(backgroundColor);
300         mBoltPaint.setColor(fillColor);
301         mChargeColor = fillColor;
302         invalidateSelf();
303     }
304 
batteryColorForLevel(int level)305     protected int batteryColorForLevel(int level) {
306         return (mCharging || (mPowerSaveEnabled && mPowerSaveAsColorError))
307                 ? mChargeColor
308                 : getColorForLevel(level);
309     }
310 
311     @Override
draw(Canvas c)312     public void draw(Canvas c) {
313         final int level = mLevel;
314         final Rect bounds = getBounds();
315 
316         if (level == -1) return;
317 
318         float drawFrac = (float) level / 100f;
319         final int height = mHeight;
320         final int width = (int) (getAspectRatio() * mHeight);
321         final int px = (mWidth - width) / 2;
322         final int buttonHeight = Math.round(height * mButtonHeightFraction);
323         final int left = mPadding.left + bounds.left;
324         final int top = bounds.bottom - mPadding.bottom - height;
325 
326         mFrame.set(left, top, width + left, height + top);
327         mFrame.offset(px, 0);
328 
329         // button-frame: area above the battery body
330         mButtonFrame.set(
331                 mFrame.left + Math.round(width * 0.28f),
332                 mFrame.top,
333                 mFrame.right - Math.round(width * 0.28f),
334                 mFrame.top + buttonHeight);
335 
336         // frame: battery body area
337         mFrame.top += buttonHeight;
338 
339         // set the battery charging color
340         mBatteryPaint.setColor(batteryColorForLevel(level));
341 
342         if (level >= FULL) {
343             drawFrac = 1f;
344         } else if (level <= mCriticalLevel) {
345             drawFrac = 0f;
346         }
347 
348         final float levelTop = drawFrac == 1f ? mButtonFrame.top
349                 : (mFrame.top + (mFrame.height() * (1f - drawFrac)));
350 
351         // define the battery shape
352         mShapePath.reset();
353         mOutlinePath.reset();
354         final float radius = getRadiusRatio() * (mFrame.height() + buttonHeight);
355         mShapePath.setFillType(FillType.WINDING);
356         mShapePath.addRoundRect(mFrame, radius, radius, Direction.CW);
357         mShapePath.addRect(mButtonFrame, Direction.CW);
358         mOutlinePath.addRoundRect(mFrame, radius, radius, Direction.CW);
359         Path p = new Path();
360         p.addRect(mButtonFrame, Direction.CW);
361         mOutlinePath.op(p, Op.XOR);
362 
363         if (mCharging) {
364             // define the bolt shape
365             // Shift right by 1px for maximal bolt-goodness
366             final float bl = mFrame.left + mFrame.width() / 4f + 1;
367             final float bt = mFrame.top + mFrame.height() / 6f;
368             final float br = mFrame.right - mFrame.width() / 4f + 1;
369             final float bb = mFrame.bottom - mFrame.height() / 10f;
370             if (mBoltFrame.left != bl || mBoltFrame.top != bt
371                     || mBoltFrame.right != br || mBoltFrame.bottom != bb) {
372                 mBoltFrame.set(bl, bt, br, bb);
373                 mBoltPath.reset();
374                 mBoltPath.moveTo(
375                         mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(),
376                         mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height());
377                 for (int i = 2; i < mBoltPoints.length; i += 2) {
378                     mBoltPath.lineTo(
379                             mBoltFrame.left + mBoltPoints[i] * mBoltFrame.width(),
380                             mBoltFrame.top + mBoltPoints[i + 1] * mBoltFrame.height());
381                 }
382                 mBoltPath.lineTo(
383                         mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(),
384                         mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height());
385             }
386 
387             float boltPct = (mBoltFrame.bottom - levelTop) / (mBoltFrame.bottom - mBoltFrame.top);
388             boltPct = Math.min(Math.max(boltPct, 0), 1);
389             if (boltPct <= BOLT_LEVEL_THRESHOLD) {
390                 // draw the bolt if opaque
391                 c.drawPath(mBoltPath, mBoltPaint);
392             } else {
393                 // otherwise cut the bolt out of the overall shape
394                 mShapePath.op(mBoltPath, Path.Op.DIFFERENCE);
395             }
396         } else if (mPowerSaveEnabled) {
397             // define the plus shape
398             final float pw = mFrame.width() * 2 / 3;
399             final float pl = mFrame.left + (mFrame.width() - pw) / 2;
400             final float pt = mFrame.top + (mFrame.height() - pw) / 2;
401             final float pr = mFrame.right - (mFrame.width() - pw) / 2;
402             final float pb = mFrame.bottom - (mFrame.height() - pw) / 2;
403             if (mPlusFrame.left != pl || mPlusFrame.top != pt
404                     || mPlusFrame.right != pr || mPlusFrame.bottom != pb) {
405                 mPlusFrame.set(pl, pt, pr, pb);
406                 mPlusPath.reset();
407                 mPlusPath.moveTo(
408                         mPlusFrame.left + mPlusPoints[0] * mPlusFrame.width(),
409                         mPlusFrame.top + mPlusPoints[1] * mPlusFrame.height());
410                 for (int i = 2; i < mPlusPoints.length; i += 2) {
411                     mPlusPath.lineTo(
412                             mPlusFrame.left + mPlusPoints[i] * mPlusFrame.width(),
413                             mPlusFrame.top + mPlusPoints[i + 1] * mPlusFrame.height());
414                 }
415                 mPlusPath.lineTo(
416                         mPlusFrame.left + mPlusPoints[0] * mPlusFrame.width(),
417                         mPlusFrame.top + mPlusPoints[1] * mPlusFrame.height());
418             }
419 
420             // Always cut out of the whole shape, and sometimes filled colorError
421             mShapePath.op(mPlusPath, Path.Op.DIFFERENCE);
422             if (mPowerSaveAsColorError) {
423                 c.drawPath(mPlusPath, mPlusPaint);
424             }
425         }
426 
427         // compute percentage text
428         boolean pctOpaque = false;
429         float pctX = 0, pctY = 0;
430         String pctText = null;
431         if (!mCharging && !mPowerSaveEnabled && level > mCriticalLevel && mShowPercent) {
432             mTextPaint.setColor(getColorForLevel(level));
433             mTextPaint.setTextSize(height *
434                     (SINGLE_DIGIT_PERCENT ? 0.75f
435                             : (mLevel == 100 ? 0.38f : 0.5f)));
436             mTextHeight = -mTextPaint.getFontMetrics().ascent;
437             pctText = String.valueOf(SINGLE_DIGIT_PERCENT ? (level / 10) : level);
438             pctX = mWidth * 0.5f + left;
439             pctY = (mHeight + mTextHeight) * 0.47f + top;
440             pctOpaque = levelTop > pctY;
441             if (!pctOpaque) {
442                 mTextPath.reset();
443                 mTextPaint.getTextPath(pctText, 0, pctText.length(), pctX, pctY, mTextPath);
444                 // cut the percentage text out of the overall shape
445                 mShapePath.op(mTextPath, Path.Op.DIFFERENCE);
446             }
447         }
448 
449         // draw the battery shape background
450         c.drawPath(mShapePath, mFramePaint);
451 
452         // draw the battery shape, clipped to charging level
453         mFrame.top = levelTop;
454         c.save();
455         c.clipRect(mFrame);
456         c.drawPath(mShapePath, mBatteryPaint);
457         c.restore();
458 
459         if (!mCharging && !mPowerSaveEnabled) {
460             if (level <= mCriticalLevel) {
461                 // draw the warning text
462                 final float x = mWidth * 0.5f + left;
463                 final float y = (mHeight + mWarningTextHeight) * 0.48f + top;
464                 c.drawText(mWarningString, x, y, mWarningTextPaint);
465             } else if (pctOpaque) {
466                 // draw the percentage text
467                 c.drawText(pctText, pctX, pctY, mTextPaint);
468             }
469         }
470 
471         // Draw the powersave outline last
472         if (!mCharging && mPowerSaveEnabled && mPowerSaveAsColorError) {
473             c.drawPath(mOutlinePath, mPowersavePaint);
474         }
475     }
476 
477     // Some stuff required by Drawable.
478     @Override
setAlpha(int alpha)479     public void setAlpha(int alpha) {
480     }
481 
482     @Override
setColorFilter(@ullable ColorFilter colorFilter)483     public void setColorFilter(@Nullable ColorFilter colorFilter) {
484         mFramePaint.setColorFilter(colorFilter);
485         mBatteryPaint.setColorFilter(colorFilter);
486         mWarningTextPaint.setColorFilter(colorFilter);
487         mBoltPaint.setColorFilter(colorFilter);
488         mPlusPaint.setColorFilter(colorFilter);
489     }
490 
491     @Override
getOpacity()492     public int getOpacity() {
493         return 0;
494     }
495 
getCriticalLevel()496     public int getCriticalLevel() {
497         return mCriticalLevel;
498     }
499 
getAspectRatio()500     protected float getAspectRatio() {
501         return ASPECT_RATIO;
502     }
503 
getRadiusRatio()504     protected float getRadiusRatio() {
505         return RADIUS_RATIO;
506     }
507 }
508