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