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