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