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