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