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