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