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.annotation.Nullable;
     20 import android.content.Context;
     21 import android.content.res.Resources;
     22 import android.content.res.TypedArray;
     23 import android.graphics.Canvas;
     24 import android.graphics.Color;
     25 import android.graphics.ColorFilter;
     26 import android.graphics.Paint;
     27 import android.graphics.Paint.Style;
     28 import android.graphics.Path;
     29 import android.graphics.Path.Direction;
     30 import android.graphics.Path.FillType;
     31 import android.graphics.Path.Op;
     32 import android.graphics.Rect;
     33 import android.graphics.RectF;
     34 import android.graphics.Typeface;
     35 import android.graphics.drawable.Drawable;
     36 import android.util.TypedValue;
     37 
     38 import com.android.settingslib.R;
     39 import com.android.settingslib.Utils;
     40 
     41 public class BatteryMeterDrawableBase extends Drawable {
     42 
     43     private static final float ASPECT_RATIO = .58f;
     44     public static final String TAG = BatteryMeterDrawableBase.class.getSimpleName();
     45     private static final float RADIUS_RATIO = 1.0f / 17f;
     46 
     47     protected final Context mContext;
     48     protected final Paint mFramePaint;
     49     protected final Paint mBatteryPaint;
     50     protected final Paint mWarningTextPaint;
     51     protected final Paint mTextPaint;
     52     protected final Paint mBoltPaint;
     53     protected final Paint mPlusPaint;
     54     protected final Paint mPowersavePaint;
     55     protected float mButtonHeightFraction;
     56 
     57     private int mLevel = -1;
     58     private boolean mCharging;
     59     private boolean mPowerSaveEnabled;
     60     protected boolean mPowerSaveAsColorError = true;
     61     private boolean mShowPercent;
     62 
     63     private static final boolean SINGLE_DIGIT_PERCENT = false;
     64 
     65     private static final int FULL = 96;
     66 
     67     private static final float BOLT_LEVEL_THRESHOLD = 0.3f;  // opaque bolt below this fraction
     68 
     69     private final int[] mColors;
     70     private final int mIntrinsicWidth;
     71     private final int mIntrinsicHeight;
     72 
     73     private float mSubpixelSmoothingLeft;
     74     private float mSubpixelSmoothingRight;
     75     private float mTextHeight, mWarningTextHeight;
     76     private int mIconTint = Color.WHITE;
     77     private float mOldDarkIntensity = -1f;
     78 
     79     private int mHeight;
     80     private int mWidth;
     81     private String mWarningString;
     82     private final int mCriticalLevel;
     83     private int mChargeColor;
     84     private final float[] mBoltPoints;
     85     private final Path mBoltPath = new Path();
     86     private final float[] mPlusPoints;
     87     private final Path mPlusPath = new Path();
     88 
     89     private final Rect mPadding = new Rect();
     90     private final RectF mFrame = new RectF();
     91     private final RectF mButtonFrame = new RectF();
     92     private final RectF mBoltFrame = new RectF();
     93     private final RectF mPlusFrame = new RectF();
     94 
     95     private final Path mShapePath = new Path();
     96     private final Path mOutlinePath = new Path();
     97     private final Path mTextPath = new Path();
     98 
     99     public BatteryMeterDrawableBase(Context context, int frameColor) {
    100         mContext = context;
    101         final Resources res = context.getResources();
    102         TypedArray levels = res.obtainTypedArray(R.array.batterymeter_color_levels);
    103         TypedArray colors = res.obtainTypedArray(R.array.batterymeter_color_values);
    104 
    105         final int N = levels.length();
    106         mColors = new int[2 * N];
    107         for (int i = 0; i < N; i++) {
    108             mColors[2 * i] = levels.getInt(i, 0);
    109             if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) {
    110                 mColors[2 * i + 1] = Utils.getColorAttrDefaultColor(context,
    111                         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.getColorStateListDefaultColor(mContext, R.color.meter_consumed_color);
    154 
    155         mBoltPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    156         mBoltPaint.setColor(Utils.getColorStateListDefaultColor(mContext,
    157                 R.color.batterymeter_bolt_color));
    158         mBoltPoints = loadPoints(res, R.array.batterymeter_bolt_points);
    159 
    160         mPlusPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    161         mPlusPaint.setColor(Utils.getColorStateListDefaultColor(mContext,
    162                 R.color.batterymeter_plus_color));
    163         mPlusPoints = loadPoints(res, R.array.batterymeter_plus_points);
    164 
    165         mPowersavePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    166         mPowersavePaint.setColor(mPlusPaint.getColor());
    167         mPowersavePaint.setStyle(Style.STROKE);
    168         mPowersavePaint.setStrokeWidth(context.getResources()
    169                 .getDimensionPixelSize(R.dimen.battery_powersave_outline_thickness));
    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     public boolean getPowerSave() {
    214         return mPowerSaveEnabled;
    215     }
    216 
    217     protected void setPowerSaveAsColorError(boolean asError) {
    218         mPowerSaveAsColorError = asError;
    219     }
    220 
    221     // an approximation of View.postInvalidate()
    222     protected void postInvalidate() {
    223         unscheduleSelf(this::invalidateSelf);
    224         scheduleSelf(this::invalidateSelf, 0);
    225     }
    226 
    227     private static float[] loadPoints(Resources res, int pointArrayRes) {
    228         final int[] pts = res.getIntArray(pointArrayRes);
    229         int maxX = 0, maxY = 0;
    230         for (int i = 0; i < pts.length; i += 2) {
    231             maxX = Math.max(maxX, pts[i]);
    232             maxY = Math.max(maxY, pts[i + 1]);
    233         }
    234         final float[] ptsF = new float[pts.length];
    235         for (int i = 0; i < pts.length; i += 2) {
    236             ptsF[i] = (float) pts[i] / maxX;
    237             ptsF[i + 1] = (float) pts[i + 1] / maxY;
    238         }
    239         return ptsF;
    240     }
    241 
    242     @Override
    243     public void setBounds(int left, int top, int right, int bottom) {
    244         super.setBounds(left, top, right, bottom);
    245         updateSize();
    246     }
    247 
    248     private void updateSize() {
    249         final Rect bounds = getBounds();
    250 
    251         mHeight = (bounds.bottom - mPadding.bottom) - (bounds.top + mPadding.top);
    252         mWidth = (bounds.right - mPadding.right) - (bounds.left + mPadding.left);
    253         mWarningTextPaint.setTextSize(mHeight * 0.75f);
    254         mWarningTextHeight = -mWarningTextPaint.getFontMetrics().ascent;
    255     }
    256 
    257     @Override
    258     public boolean getPadding(Rect padding) {
    259         if (mPadding.left == 0
    260             && mPadding.top == 0
    261             && mPadding.right == 0
    262             && mPadding.bottom == 0) {
    263             return super.getPadding(padding);
    264         }
    265 
    266         padding.set(mPadding);
    267         return true;
    268     }
    269 
    270     public void setPadding(int left, int top, int right, int bottom) {
    271         mPadding.left = left;
    272         mPadding.top = top;
    273         mPadding.right = right;
    274         mPadding.bottom = bottom;
    275 
    276         updateSize();
    277     }
    278 
    279     private int getColorForLevel(int percent) {
    280         int thresh, color = 0;
    281         for (int i = 0; i < mColors.length; i += 2) {
    282             thresh = mColors[i];
    283             color = mColors[i + 1];
    284             if (percent <= thresh) {
    285 
    286                 // Respect tinting for "normal" level
    287                 if (i == mColors.length - 2) {
    288                     return mIconTint;
    289                 } else {
    290                     return color;
    291                 }
    292             }
    293         }
    294         return color;
    295     }
    296 
    297     public void setColors(int fillColor, int backgroundColor) {
    298         mIconTint = fillColor;
    299         mFramePaint.setColor(backgroundColor);
    300         mBoltPaint.setColor(fillColor);
    301         mChargeColor = fillColor;
    302         invalidateSelf();
    303     }
    304 
    305     protected int batteryColorForLevel(int level) {
    306         return (mCharging || (mPowerSaveEnabled && mPowerSaveAsColorError))
    307                 ? mChargeColor
    308                 : getColorForLevel(level);
    309     }
    310 
    311     @Override
    312     public void draw(Canvas c) {
    313         final int level = mLevel;
    314         final Rect bounds = getBounds();
    315 
    316         if (level == -1) return;
    317 
    318         float drawFrac = (float) level / 100f;
    319         final int height = mHeight;
    320         final int width = (int) (getAspectRatio() * mHeight);
    321         final int px = (mWidth - width) / 2;
    322         final int buttonHeight = Math.round(height * mButtonHeightFraction);
    323         final int left = mPadding.left + bounds.left;
    324         final int top = bounds.bottom - mPadding.bottom - height;
    325 
    326         mFrame.set(left, top, width + left, height + top);
    327         mFrame.offset(px, 0);
    328 
    329         // button-frame: area above the battery body
    330         mButtonFrame.set(
    331                 mFrame.left + Math.round(width * 0.28f),
    332                 mFrame.top,
    333                 mFrame.right - Math.round(width * 0.28f),
    334                 mFrame.top + buttonHeight);
    335 
    336         // frame: battery body area
    337         mFrame.top += buttonHeight;
    338 
    339         // set the battery charging color
    340         mBatteryPaint.setColor(batteryColorForLevel(level));
    341 
    342         if (level >= FULL) {
    343             drawFrac = 1f;
    344         } else if (level <= mCriticalLevel) {
    345             drawFrac = 0f;
    346         }
    347 
    348         final float levelTop = drawFrac == 1f ? mButtonFrame.top
    349                 : (mFrame.top + (mFrame.height() * (1f - drawFrac)));
    350 
    351         // define the battery shape
    352         mShapePath.reset();
    353         mOutlinePath.reset();
    354         final float radius = getRadiusRatio() * (mFrame.height() + buttonHeight);
    355         mShapePath.setFillType(FillType.WINDING);
    356         mShapePath.addRoundRect(mFrame, radius, radius, Direction.CW);
    357         mShapePath.addRect(mButtonFrame, Direction.CW);
    358         mOutlinePath.addRoundRect(mFrame, radius, radius, Direction.CW);
    359         Path p = new Path();
    360         p.addRect(mButtonFrame, Direction.CW);
    361         mOutlinePath.op(p, Op.XOR);
    362 
    363         if (mCharging) {
    364             // define the bolt shape
    365             // Shift right by 1px for maximal bolt-goodness
    366             final float bl = mFrame.left + mFrame.width() / 4f + 1;
    367             final float bt = mFrame.top + mFrame.height() / 6f;
    368             final float br = mFrame.right - mFrame.width() / 4f + 1;
    369             final float bb = mFrame.bottom - mFrame.height() / 10f;
    370             if (mBoltFrame.left != bl || mBoltFrame.top != bt
    371                     || mBoltFrame.right != br || mBoltFrame.bottom != bb) {
    372                 mBoltFrame.set(bl, bt, br, bb);
    373                 mBoltPath.reset();
    374                 mBoltPath.moveTo(
    375                         mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(),
    376                         mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height());
    377                 for (int i = 2; i < mBoltPoints.length; i += 2) {
    378                     mBoltPath.lineTo(
    379                             mBoltFrame.left + mBoltPoints[i] * mBoltFrame.width(),
    380                             mBoltFrame.top + mBoltPoints[i + 1] * mBoltFrame.height());
    381                 }
    382                 mBoltPath.lineTo(
    383                         mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(),
    384                         mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height());
    385             }
    386 
    387             float boltPct = (mBoltFrame.bottom - levelTop) / (mBoltFrame.bottom - mBoltFrame.top);
    388             boltPct = Math.min(Math.max(boltPct, 0), 1);
    389             if (boltPct <= BOLT_LEVEL_THRESHOLD) {
    390                 // draw the bolt if opaque
    391                 c.drawPath(mBoltPath, mBoltPaint);
    392             } else {
    393                 // otherwise cut the bolt out of the overall shape
    394                 mShapePath.op(mBoltPath, Path.Op.DIFFERENCE);
    395             }
    396         } else if (mPowerSaveEnabled) {
    397             // define the plus shape
    398             final float pw = mFrame.width() * 2 / 3;
    399             final float pl = mFrame.left + (mFrame.width() - pw) / 2;
    400             final float pt = mFrame.top + (mFrame.height() - pw) / 2;
    401             final float pr = mFrame.right - (mFrame.width() - pw) / 2;
    402             final float pb = mFrame.bottom - (mFrame.height() - pw) / 2;
    403             if (mPlusFrame.left != pl || mPlusFrame.top != pt
    404                     || mPlusFrame.right != pr || mPlusFrame.bottom != pb) {
    405                 mPlusFrame.set(pl, pt, pr, pb);
    406                 mPlusPath.reset();
    407                 mPlusPath.moveTo(
    408                         mPlusFrame.left + mPlusPoints[0] * mPlusFrame.width(),
    409                         mPlusFrame.top + mPlusPoints[1] * mPlusFrame.height());
    410                 for (int i = 2; i < mPlusPoints.length; i += 2) {
    411                     mPlusPath.lineTo(
    412                             mPlusFrame.left + mPlusPoints[i] * mPlusFrame.width(),
    413                             mPlusFrame.top + mPlusPoints[i + 1] * mPlusFrame.height());
    414                 }
    415                 mPlusPath.lineTo(
    416                         mPlusFrame.left + mPlusPoints[0] * mPlusFrame.width(),
    417                         mPlusFrame.top + mPlusPoints[1] * mPlusFrame.height());
    418             }
    419 
    420             // Always cut out of the whole shape, and sometimes filled colorError
    421             mShapePath.op(mPlusPath, Path.Op.DIFFERENCE);
    422             if (mPowerSaveAsColorError) {
    423                 c.drawPath(mPlusPath, mPlusPaint);
    424             }
    425         }
    426 
    427         // compute percentage text
    428         boolean pctOpaque = false;
    429         float pctX = 0, pctY = 0;
    430         String pctText = null;
    431         if (!mCharging && !mPowerSaveEnabled && level > mCriticalLevel && mShowPercent) {
    432             mTextPaint.setColor(getColorForLevel(level));
    433             mTextPaint.setTextSize(height *
    434                     (SINGLE_DIGIT_PERCENT ? 0.75f
    435                             : (mLevel == 100 ? 0.38f : 0.5f)));
    436             mTextHeight = -mTextPaint.getFontMetrics().ascent;
    437             pctText = String.valueOf(SINGLE_DIGIT_PERCENT ? (level / 10) : level);
    438             pctX = mWidth * 0.5f + left;
    439             pctY = (mHeight + mTextHeight) * 0.47f + top;
    440             pctOpaque = levelTop > pctY;
    441             if (!pctOpaque) {
    442                 mTextPath.reset();
    443                 mTextPaint.getTextPath(pctText, 0, pctText.length(), pctX, pctY, mTextPath);
    444                 // cut the percentage text out of the overall shape
    445                 mShapePath.op(mTextPath, Path.Op.DIFFERENCE);
    446             }
    447         }
    448 
    449         // draw the battery shape background
    450         c.drawPath(mShapePath, mFramePaint);
    451 
    452         // draw the battery shape, clipped to charging level
    453         mFrame.top = levelTop;
    454         c.save();
    455         c.clipRect(mFrame);
    456         c.drawPath(mShapePath, mBatteryPaint);
    457         c.restore();
    458 
    459         if (!mCharging && !mPowerSaveEnabled) {
    460             if (level <= mCriticalLevel) {
    461                 // draw the warning text
    462                 final float x = mWidth * 0.5f + left;
    463                 final float y = (mHeight + mWarningTextHeight) * 0.48f + top;
    464                 c.drawText(mWarningString, x, y, mWarningTextPaint);
    465             } else if (pctOpaque) {
    466                 // draw the percentage text
    467                 c.drawText(pctText, pctX, pctY, mTextPaint);
    468             }
    469         }
    470 
    471         // Draw the powersave outline last
    472         if (!mCharging && mPowerSaveEnabled && mPowerSaveAsColorError) {
    473             c.drawPath(mOutlinePath, mPowersavePaint);
    474         }
    475     }
    476 
    477     // Some stuff required by Drawable.
    478     @Override
    479     public void setAlpha(int alpha) {
    480     }
    481 
    482     @Override
    483     public void setColorFilter(@Nullable ColorFilter colorFilter) {
    484         mFramePaint.setColorFilter(colorFilter);
    485         mBatteryPaint.setColorFilter(colorFilter);
    486         mWarningTextPaint.setColorFilter(colorFilter);
    487         mBoltPaint.setColorFilter(colorFilter);
    488         mPlusPaint.setColorFilter(colorFilter);
    489     }
    490 
    491     @Override
    492     public int getOpacity() {
    493         return 0;
    494     }
    495 
    496     public int getCriticalLevel() {
    497         return mCriticalLevel;
    498     }
    499 
    500     protected float getAspectRatio() {
    501         return ASPECT_RATIO;
    502     }
    503 
    504     protected float getRadiusRatio() {
    505         return RADIUS_RATIO;
    506     }
    507 }
    508