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