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 BatteryMeterDrawableBase(Context context, int frameColor)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 getIntrinsicHeight()163 public int getIntrinsicHeight() { 164 return mIntrinsicHeight; 165 } 166 167 @Override getIntrinsicWidth()168 public int getIntrinsicWidth() { 169 return mIntrinsicWidth; 170 } 171 setShowPercent(boolean show)172 public void setShowPercent(boolean show) { 173 mShowPercent = show; 174 postInvalidate(); 175 } 176 setCharging(boolean val)177 public void setCharging(boolean val) { 178 mCharging = val; 179 postInvalidate(); 180 } 181 getCharging()182 public boolean getCharging() { 183 return mCharging; 184 } 185 setBatteryLevel(int val)186 public void setBatteryLevel(int val) { 187 mLevel = val; 188 postInvalidate(); 189 } 190 getBatteryLevel()191 public int getBatteryLevel() { 192 return mLevel; 193 } 194 setPowerSave(boolean val)195 public void setPowerSave(boolean val) { 196 mPowerSaveEnabled = val; 197 postInvalidate(); 198 } 199 200 // an approximation of View.postInvalidate() postInvalidate()201 protected void postInvalidate() { 202 unscheduleSelf(this::invalidateSelf); 203 scheduleSelf(this::invalidateSelf, 0); 204 } 205 loadPoints(Resources res, int pointArrayRes)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 setBounds(int left, int top, int right, int bottom)222 public void setBounds(int left, int top, int right, int bottom) { 223 super.setBounds(left, top, right, bottom); 224 updateSize(); 225 } 226 updateSize()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 getPadding(Rect padding)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 setPadding(int left, int top, int right, int bottom)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 getColorForLevel(int percent)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 setColors(int fillColor, int backgroundColor)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 batteryColorForLevel(int level)289 protected int batteryColorForLevel(int level) { 290 return mCharging ? mChargeColor : getColorForLevel(level); 291 } 292 293 @Override draw(Canvas c)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 setAlpha(int alpha)455 public void setAlpha(int alpha) { 456 } 457 458 @Override setColorFilter(@ullable ColorFilter colorFilter)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 getOpacity()468 public int getOpacity() { 469 return 0; 470 } 471 getCriticalLevel()472 public int getCriticalLevel() { 473 return mCriticalLevel; 474 } 475 getAspectRatio()476 protected float getAspectRatio() { 477 return ASPECT_RATIO; 478 } 479 getRadiusRatio()480 protected float getRadiusRatio() { 481 return RADIUS_RATIO; 482 } 483 } 484