• 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.RectF;
30 import android.graphics.Typeface;
31 import android.graphics.drawable.Drawable;
32 import android.util.TypedValue;
33 
34 import com.android.settingslib.R;
35 import com.android.settingslib.Utils;
36 
37 public 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();
41 
42     protected final Context mContext;
43     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;
49 
50     private int mLevel = -1;
51     private boolean mCharging;
52     private boolean mPowerSaveEnabled;
53     private boolean mShowPercent;
54 
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;
68     private float mTextHeight, mWarningTextHeight;
69     private int mIconTint = Color.WHITE;
70     private float mOldDarkIntensity = -1f;
71 
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 
BatteryMeterDrawableBase(Context context, int frameColor)91     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];
99         for (int i=0; i < N; i++) {
100             mColors[2 * i] = levels.getInt(i, 0);
101             if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) {
102                 mColors[2 * i + 1] = Utils.getColorAttr(context, colors.getThemeAttributeId(i, 0));
103             } else {
104                 mColors[2 * i + 1] = colors.getColor(i, 0);
105             }
106         }
107         levels.recycle();
108         colors.recycle();
109 
110         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.meter_consumed_color);
145 
146         mBoltPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
147         mBoltPaint.setColor(Utils.getDefaultColor(mContext, R.color.batterymeter_bolt_color));
148         mBoltPoints = loadPoints(res, R.array.batterymeter_bolt_points);
149 
150         mPlusPaint = new Paint(mBoltPaint);
151         mPlusPoints = loadPoints(res, R.array.batterymeter_plus_points);
152 
153         mIntrinsicWidth = context.getResources().getDimensionPixelSize(R.dimen.battery_width);
154         mIntrinsicHeight = context.getResources().getDimensionPixelSize(R.dimen.battery_height);
155     }
156 
157     @Override
getIntrinsicHeight()158     public int getIntrinsicHeight() {
159         return mIntrinsicHeight;
160     }
161 
162     @Override
getIntrinsicWidth()163     public int getIntrinsicWidth() {
164         return mIntrinsicWidth;
165     }
166 
setShowPercent(boolean show)167     public void setShowPercent(boolean show) {
168         mShowPercent = show;
169         postInvalidate();
170     }
171 
setCharging(boolean val)172     public void setCharging(boolean val) {
173         mCharging = val;
174         postInvalidate();
175     }
176 
getCharging()177     public boolean getCharging() {
178         return mCharging;
179     }
180 
setBatteryLevel(int val)181     public void setBatteryLevel(int val) {
182         mLevel = val;
183         postInvalidate();
184     }
185 
getBatteryLevel()186     public int getBatteryLevel() {
187         return mLevel;
188     }
189 
setPowerSave(boolean val)190     public void setPowerSave(boolean val) {
191         mPowerSaveEnabled = val;
192         postInvalidate();
193     }
194 
195     // an approximation of View.postInvalidate()
postInvalidate()196     protected void postInvalidate() {
197         unscheduleSelf(this::invalidateSelf);
198         scheduleSelf(this::invalidateSelf, 0);
199     }
200 
loadPoints(Resources res, int pointArrayRes)201     private static float[] loadPoints(Resources res, int pointArrayRes) {
202         final int[] pts = res.getIntArray(pointArrayRes);
203         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
setBounds(int left, int top, int right, int bottom)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 
getColorForLevel(int percent)225     private int getColorForLevel(int percent) {
226         // If we are in power save mode, always use the normal color.
227         if (mPowerSaveEnabled) {
228             return mIconTint;
229         }
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 
setColors(int fillColor, int backgroundColor)247     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 
255     @Override
draw(Canvas c)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
290         mBatteryPaint.setColor(mCharging ? mChargeColor : getColorForLevel(level));
291 
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 
313         if (mCharging) {
314             // 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;
384         if (!mCharging && !mPowerSaveEnabled && level > mCriticalLevel && mShowPercent) {
385             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 
412         if (!mCharging && !mPowerSaveEnabled) {
413             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
setAlpha(int alpha)427     public void setAlpha(int alpha) {
428     }
429 
430     @Override
setColorFilter(@ullable ColorFilter colorFilter)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
getOpacity()440     public int getOpacity() {
441         return 0;
442     }
443 
getCriticalLevel()444     public int getCriticalLevel() {
445         return mCriticalLevel;
446     }
447 }
448