1 /* 2 * Copyright (C) 2015 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.systemui; 18 19 import android.animation.ArgbEvaluator; 20 import android.annotation.Nullable; 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.content.res.TypedArray; 24 import android.database.ContentObserver; 25 import android.graphics.Canvas; 26 import android.graphics.Color; 27 import android.graphics.ColorFilter; 28 import android.graphics.Paint; 29 import android.graphics.Path; 30 import android.graphics.RectF; 31 import android.graphics.Typeface; 32 import android.graphics.drawable.Drawable; 33 import android.net.Uri; 34 import android.os.Bundle; 35 import android.os.Handler; 36 import android.provider.Settings; 37 38 import com.android.systemui.statusbar.policy.BatteryController; 39 40 public class BatteryMeterDrawable extends Drawable implements 41 BatteryController.BatteryStateChangeCallback { 42 43 private static final float ASPECT_RATIO = 9.5f / 14.5f; 44 public static final String TAG = BatteryMeterDrawable.class.getSimpleName(); 45 public static final String SHOW_PERCENT_SETTING = "status_bar_show_battery_percent"; 46 47 private static final boolean SINGLE_DIGIT_PERCENT = false; 48 49 private static final int FULL = 96; 50 51 private static final float BOLT_LEVEL_THRESHOLD = 0.3f; // opaque bolt below this fraction 52 53 private final int[] mColors; 54 private final int mIntrinsicWidth; 55 private final int mIntrinsicHeight; 56 57 private boolean mShowPercent; 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 BatteryController mBatteryController; 87 private boolean mPowerSaveEnabled; 88 89 private int mDarkModeBackgroundColor; 90 private int mDarkModeFillColor; 91 92 private int mLightModeBackgroundColor; 93 private int mLightModeFillColor; 94 95 private final SettingObserver mSettingObserver = new SettingObserver(); 96 97 private final Context mContext; 98 private final Handler mHandler; 99 100 private int mLevel = -1; 101 private boolean mPluggedIn; 102 private boolean mListening; 103 104 public BatteryMeterDrawable(Context context, Handler handler, int frameColor) { 105 mContext = context; 106 mHandler = handler; 107 final Resources res = context.getResources(); 108 TypedArray levels = res.obtainTypedArray(R.array.batterymeter_color_levels); 109 TypedArray colors = res.obtainTypedArray(R.array.batterymeter_color_values); 110 111 final int N = levels.length(); 112 mColors = new int[2*N]; 113 for (int i=0; i<N; i++) { 114 mColors[2*i] = levels.getInt(i, 0); 115 mColors[2*i+1] = colors.getColor(i, 0); 116 } 117 levels.recycle(); 118 colors.recycle(); 119 updateShowPercent(); 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 mWarningTextPaint.setColor(mColors[1]); 148 font = Typeface.create("sans-serif", Typeface.BOLD); 149 mWarningTextPaint.setTypeface(font); 150 mWarningTextPaint.setTextAlign(Paint.Align.CENTER); 151 152 mChargeColor = context.getColor(R.color.batterymeter_charge_color); 153 154 mBoltPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 155 mBoltPaint.setColor(context.getColor(R.color.batterymeter_bolt_color)); 156 mBoltPoints = loadBoltPoints(res); 157 158 mPlusPaint = new Paint(mBoltPaint); 159 mPlusPoints = loadPlusPoints(res); 160 161 mDarkModeBackgroundColor = 162 context.getColor(R.color.dark_mode_icon_color_dual_tone_background); 163 mDarkModeFillColor = context.getColor(R.color.dark_mode_icon_color_dual_tone_fill); 164 mLightModeBackgroundColor = 165 context.getColor(R.color.light_mode_icon_color_dual_tone_background); 166 mLightModeFillColor = context.getColor(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 startListening() { 183 mListening = true; 184 mContext.getContentResolver().registerContentObserver( 185 Settings.System.getUriFor(SHOW_PERCENT_SETTING), false, mSettingObserver); 186 updateShowPercent(); 187 mBatteryController.addStateChangedCallback(this); 188 } 189 190 public void stopListening() { 191 mListening = false; 192 mContext.getContentResolver().unregisterContentObserver(mSettingObserver); 193 mBatteryController.removeStateChangedCallback(this); 194 } 195 196 public void disableShowPercent() { 197 mShowPercent = false; 198 postInvalidate(); 199 } 200 201 private void postInvalidate() { 202 mHandler.post(new Runnable() { 203 @Override 204 public void run() { 205 invalidateSelf(); 206 } 207 }); 208 } 209 210 public void setBatteryController(BatteryController batteryController) { 211 mBatteryController = batteryController; 212 mPowerSaveEnabled = mBatteryController.isPowerSave(); 213 } 214 215 @Override 216 public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) { 217 mLevel = level; 218 mPluggedIn = pluggedIn; 219 220 postInvalidate(); 221 } 222 223 @Override 224 public void onPowerSaveChanged(boolean isPowerSave) { 225 mPowerSaveEnabled = isPowerSave; 226 invalidateSelf(); 227 } 228 229 private static float[] loadBoltPoints(Resources res) { 230 final int[] pts = res.getIntArray(R.array.batterymeter_bolt_points); 231 int maxX = 0, maxY = 0; 232 for (int i = 0; i < pts.length; i += 2) { 233 maxX = Math.max(maxX, pts[i]); 234 maxY = Math.max(maxY, pts[i + 1]); 235 } 236 final float[] ptsF = new float[pts.length]; 237 for (int i = 0; i < pts.length; i += 2) { 238 ptsF[i] = (float)pts[i] / maxX; 239 ptsF[i + 1] = (float)pts[i + 1] / maxY; 240 } 241 return ptsF; 242 } 243 244 private static float[] loadPlusPoints(Resources res) { 245 final int[] pts = res.getIntArray(R.array.batterymeter_plus_points); 246 int maxX = 0, maxY = 0; 247 for (int i = 0; i < pts.length; i += 2) { 248 maxX = Math.max(maxX, pts[i]); 249 maxY = Math.max(maxY, pts[i + 1]); 250 } 251 final float[] ptsF = new float[pts.length]; 252 for (int i = 0; i < pts.length; i += 2) { 253 ptsF[i] = (float)pts[i] / maxX; 254 ptsF[i + 1] = (float)pts[i + 1] / maxY; 255 } 256 return ptsF; 257 } 258 259 @Override 260 public void setBounds(int left, int top, int right, int bottom) { 261 super.setBounds(left, top, right, bottom); 262 mHeight = bottom - top; 263 mWidth = right - left; 264 mWarningTextPaint.setTextSize(mHeight * 0.75f); 265 mWarningTextHeight = -mWarningTextPaint.getFontMetrics().ascent; 266 } 267 268 private void updateShowPercent() { 269 mShowPercent = 0 != Settings.System.getInt(mContext.getContentResolver(), 270 SHOW_PERCENT_SETTING, 0); 271 } 272 273 private int getColorForLevel(int percent) { 274 275 // If we are in power save mode, always use the normal color. 276 if (mPowerSaveEnabled) { 277 return mColors[mColors.length-1]; 278 } 279 int thresh, color = 0; 280 for (int i=0; i<mColors.length; i+=2) { 281 thresh = mColors[i]; 282 color = mColors[i+1]; 283 if (percent <= thresh) { 284 285 // Respect tinting for "normal" level 286 if (i == mColors.length-2) { 287 return mIconTint; 288 } else { 289 return color; 290 } 291 } 292 } 293 return color; 294 } 295 296 public void setDarkIntensity(float darkIntensity) { 297 if (darkIntensity == mOldDarkIntensity) { 298 return; 299 } 300 int backgroundColor = getBackgroundColor(darkIntensity); 301 int fillColor = getFillColor(darkIntensity); 302 mIconTint = fillColor; 303 mFramePaint.setColor(backgroundColor); 304 mBoltPaint.setColor(fillColor); 305 mChargeColor = fillColor; 306 invalidateSelf(); 307 mOldDarkIntensity = darkIntensity; 308 } 309 310 private int getBackgroundColor(float darkIntensity) { 311 return getColorForDarkIntensity( 312 darkIntensity, mLightModeBackgroundColor, mDarkModeBackgroundColor); 313 } 314 315 private int getFillColor(float darkIntensity) { 316 return getColorForDarkIntensity( 317 darkIntensity, mLightModeFillColor, mDarkModeFillColor); 318 } 319 320 private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) { 321 return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor); 322 } 323 324 @Override 325 public void draw(Canvas c) { 326 final int level = mLevel; 327 328 if (level == -1) return; 329 330 float drawFrac = (float) level / 100f; 331 final int height = mHeight; 332 final int width = (int) (ASPECT_RATIO * mHeight); 333 int px = (mWidth - width) / 2; 334 335 final int buttonHeight = (int) (height * mButtonHeightFraction); 336 337 mFrame.set(0, 0, width, height); 338 mFrame.offset(px, 0); 339 340 // button-frame: area above the battery body 341 mButtonFrame.set( 342 mFrame.left + Math.round(width * 0.25f), 343 mFrame.top, 344 mFrame.right - Math.round(width * 0.25f), 345 mFrame.top + buttonHeight); 346 347 mButtonFrame.top += mSubpixelSmoothingLeft; 348 mButtonFrame.left += mSubpixelSmoothingLeft; 349 mButtonFrame.right -= mSubpixelSmoothingRight; 350 351 // frame: battery body area 352 mFrame.top += buttonHeight; 353 mFrame.left += mSubpixelSmoothingLeft; 354 mFrame.top += mSubpixelSmoothingLeft; 355 mFrame.right -= mSubpixelSmoothingRight; 356 mFrame.bottom -= mSubpixelSmoothingRight; 357 358 // set the battery charging color 359 mBatteryPaint.setColor(mPluggedIn ? mChargeColor : getColorForLevel(level)); 360 361 if (level >= FULL) { 362 drawFrac = 1f; 363 } else if (level <= mCriticalLevel) { 364 drawFrac = 0f; 365 } 366 367 final float levelTop = drawFrac == 1f ? mButtonFrame.top 368 : (mFrame.top + (mFrame.height() * (1f - drawFrac))); 369 370 // define the battery shape 371 mShapePath.reset(); 372 mShapePath.moveTo(mButtonFrame.left, mButtonFrame.top); 373 mShapePath.lineTo(mButtonFrame.right, mButtonFrame.top); 374 mShapePath.lineTo(mButtonFrame.right, mFrame.top); 375 mShapePath.lineTo(mFrame.right, mFrame.top); 376 mShapePath.lineTo(mFrame.right, mFrame.bottom); 377 mShapePath.lineTo(mFrame.left, mFrame.bottom); 378 mShapePath.lineTo(mFrame.left, mFrame.top); 379 mShapePath.lineTo(mButtonFrame.left, mFrame.top); 380 mShapePath.lineTo(mButtonFrame.left, mButtonFrame.top); 381 382 if (mPluggedIn) { 383 // define the bolt shape 384 final float bl = mFrame.left + mFrame.width() / 4f; 385 final float bt = mFrame.top + mFrame.height() / 6f; 386 final float br = mFrame.right - mFrame.width() / 4f; 387 final float bb = mFrame.bottom - mFrame.height() / 10f; 388 if (mBoltFrame.left != bl || mBoltFrame.top != bt 389 || mBoltFrame.right != br || mBoltFrame.bottom != bb) { 390 mBoltFrame.set(bl, bt, br, bb); 391 mBoltPath.reset(); 392 mBoltPath.moveTo( 393 mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(), 394 mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height()); 395 for (int i = 2; i < mBoltPoints.length; i += 2) { 396 mBoltPath.lineTo( 397 mBoltFrame.left + mBoltPoints[i] * mBoltFrame.width(), 398 mBoltFrame.top + mBoltPoints[i + 1] * mBoltFrame.height()); 399 } 400 mBoltPath.lineTo( 401 mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(), 402 mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height()); 403 } 404 405 float boltPct = (mBoltFrame.bottom - levelTop) / (mBoltFrame.bottom - mBoltFrame.top); 406 boltPct = Math.min(Math.max(boltPct, 0), 1); 407 if (boltPct <= BOLT_LEVEL_THRESHOLD) { 408 // draw the bolt if opaque 409 c.drawPath(mBoltPath, mBoltPaint); 410 } else { 411 // otherwise cut the bolt out of the overall shape 412 mShapePath.op(mBoltPath, Path.Op.DIFFERENCE); 413 } 414 } else if (mPowerSaveEnabled) { 415 // define the plus shape 416 final float pw = mFrame.width() * 2 / 3; 417 final float pl = mFrame.left + (mFrame.width() - pw) / 2; 418 final float pt = mFrame.top + (mFrame.height() - pw) / 2; 419 final float pr = mFrame.right - (mFrame.width() - pw) / 2; 420 final float pb = mFrame.bottom - (mFrame.height() - pw) / 2; 421 if (mPlusFrame.left != pl || mPlusFrame.top != pt 422 || mPlusFrame.right != pr || mPlusFrame.bottom != pb) { 423 mPlusFrame.set(pl, pt, pr, pb); 424 mPlusPath.reset(); 425 mPlusPath.moveTo( 426 mPlusFrame.left + mPlusPoints[0] * mPlusFrame.width(), 427 mPlusFrame.top + mPlusPoints[1] * mPlusFrame.height()); 428 for (int i = 2; i < mPlusPoints.length; i += 2) { 429 mPlusPath.lineTo( 430 mPlusFrame.left + mPlusPoints[i] * mPlusFrame.width(), 431 mPlusFrame.top + mPlusPoints[i + 1] * mPlusFrame.height()); 432 } 433 mPlusPath.lineTo( 434 mPlusFrame.left + mPlusPoints[0] * mPlusFrame.width(), 435 mPlusFrame.top + mPlusPoints[1] * mPlusFrame.height()); 436 } 437 438 float boltPct = (mPlusFrame.bottom - levelTop) / (mPlusFrame.bottom - mPlusFrame.top); 439 boltPct = Math.min(Math.max(boltPct, 0), 1); 440 if (boltPct <= BOLT_LEVEL_THRESHOLD) { 441 // draw the bolt if opaque 442 c.drawPath(mPlusPath, mPlusPaint); 443 } else { 444 // otherwise cut the bolt out of the overall shape 445 mShapePath.op(mPlusPath, Path.Op.DIFFERENCE); 446 } 447 } 448 449 // compute percentage text 450 boolean pctOpaque = false; 451 float pctX = 0, pctY = 0; 452 String pctText = null; 453 if (!mPluggedIn && !mPowerSaveEnabled && level > mCriticalLevel && mShowPercent) { 454 mTextPaint.setColor(getColorForLevel(level)); 455 mTextPaint.setTextSize(height * 456 (SINGLE_DIGIT_PERCENT ? 0.75f 457 : (mLevel == 100 ? 0.38f : 0.5f))); 458 mTextHeight = -mTextPaint.getFontMetrics().ascent; 459 pctText = String.valueOf(SINGLE_DIGIT_PERCENT ? (level/10) : level); 460 pctX = mWidth * 0.5f; 461 pctY = (mHeight + mTextHeight) * 0.47f; 462 pctOpaque = levelTop > pctY; 463 if (!pctOpaque) { 464 mTextPath.reset(); 465 mTextPaint.getTextPath(pctText, 0, pctText.length(), pctX, pctY, mTextPath); 466 // cut the percentage text out of the overall shape 467 mShapePath.op(mTextPath, Path.Op.DIFFERENCE); 468 } 469 } 470 471 // draw the battery shape background 472 c.drawPath(mShapePath, mFramePaint); 473 474 // draw the battery shape, clipped to charging level 475 mFrame.top = levelTop; 476 mClipPath.reset(); 477 mClipPath.addRect(mFrame, Path.Direction.CCW); 478 mShapePath.op(mClipPath, Path.Op.INTERSECT); 479 c.drawPath(mShapePath, mBatteryPaint); 480 481 if (!mPluggedIn && !mPowerSaveEnabled) { 482 if (level <= mCriticalLevel) { 483 // draw the warning text 484 final float x = mWidth * 0.5f; 485 final float y = (mHeight + mWarningTextHeight) * 0.48f; 486 c.drawText(mWarningString, x, y, mWarningTextPaint); 487 } else if (pctOpaque) { 488 // draw the percentage text 489 c.drawText(pctText, pctX, pctY, mTextPaint); 490 } 491 } 492 } 493 494 // Some stuff required by Drawable. 495 @Override 496 public void setAlpha(int alpha) { 497 } 498 499 @Override 500 public void setColorFilter(@Nullable ColorFilter colorFilter) { 501 } 502 503 @Override 504 public int getOpacity() { 505 return 0; 506 } 507 508 private final class SettingObserver extends ContentObserver { 509 public SettingObserver() { 510 super(new Handler()); 511 } 512 513 @Override 514 public void onChange(boolean selfChange, Uri uri) { 515 super.onChange(selfChange, uri); 516 updateShowPercent(); 517 postInvalidate(); 518 } 519 } 520 521 } 522