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