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