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