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