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