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 BatteryMeterDrawableBase(Context context, int frameColor)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 getIntrinsicHeight()176 public int getIntrinsicHeight() { 177 return mIntrinsicHeight; 178 } 179 180 @Override getIntrinsicWidth()181 public int getIntrinsicWidth() { 182 return mIntrinsicWidth; 183 } 184 setShowPercent(boolean show)185 public void setShowPercent(boolean show) { 186 mShowPercent = show; 187 postInvalidate(); 188 } 189 setCharging(boolean val)190 public void setCharging(boolean val) { 191 mCharging = val; 192 postInvalidate(); 193 } 194 getCharging()195 public boolean getCharging() { 196 return mCharging; 197 } 198 setBatteryLevel(int val)199 public void setBatteryLevel(int val) { 200 mLevel = val; 201 postInvalidate(); 202 } 203 getBatteryLevel()204 public int getBatteryLevel() { 205 return mLevel; 206 } 207 setPowerSave(boolean val)208 public void setPowerSave(boolean val) { 209 mPowerSaveEnabled = val; 210 postInvalidate(); 211 } 212 getPowerSave()213 public boolean getPowerSave() { 214 return mPowerSaveEnabled; 215 } 216 setPowerSaveAsColorError(boolean asError)217 protected void setPowerSaveAsColorError(boolean asError) { 218 mPowerSaveAsColorError = asError; 219 } 220 221 // an approximation of View.postInvalidate() postInvalidate()222 protected void postInvalidate() { 223 unscheduleSelf(this::invalidateSelf); 224 scheduleSelf(this::invalidateSelf, 0); 225 } 226 loadPoints(Resources res, int pointArrayRes)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 setBounds(int left, int top, int right, int bottom)243 public void setBounds(int left, int top, int right, int bottom) { 244 super.setBounds(left, top, right, bottom); 245 updateSize(); 246 } 247 updateSize()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 getPadding(Rect padding)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 setPadding(int left, int top, int right, int bottom)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 getColorForLevel(int percent)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 setColors(int fillColor, int backgroundColor)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 batteryColorForLevel(int level)305 protected int batteryColorForLevel(int level) { 306 return (mCharging || (mPowerSaveEnabled && mPowerSaveAsColorError)) 307 ? mChargeColor 308 : getColorForLevel(level); 309 } 310 311 @Override draw(Canvas c)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 setAlpha(int alpha)479 public void setAlpha(int alpha) { 480 } 481 482 @Override setColorFilter(@ullable ColorFilter colorFilter)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 getOpacity()492 public int getOpacity() { 493 return 0; 494 } 495 getCriticalLevel()496 public int getCriticalLevel() { 497 return mCriticalLevel; 498 } 499 getAspectRatio()500 protected float getAspectRatio() { 501 return ASPECT_RATIO; 502 } 503 getRadiusRatio()504 protected float getRadiusRatio() { 505 return RADIUS_RATIO; 506 } 507 } 508