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