BatteryMeterView.java revision 4b786ff4b519b4537adb5da7113fb7797c67e385
1/* 2 * Copyright (C) 2013 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.systemui; 18 19import android.content.BroadcastReceiver; 20import android.content.Context; 21import android.content.Intent; 22import android.content.IntentFilter; 23import android.content.res.Resources; 24import android.content.res.TypedArray; 25import android.graphics.Canvas; 26import android.graphics.Paint; 27import android.graphics.Path; 28import android.graphics.PorterDuff; 29import android.graphics.PorterDuffXfermode; 30import android.graphics.RectF; 31import android.graphics.Typeface; 32import android.os.BatteryManager; 33import android.os.Bundle; 34import android.provider.Settings; 35import android.util.AttributeSet; 36import android.view.View; 37 38public class BatteryMeterView extends View implements DemoMode { 39 public static final String TAG = BatteryMeterView.class.getSimpleName(); 40 public static final String ACTION_LEVEL_TEST = "com.android.systemui.BATTERY_LEVEL_TEST"; 41 42 private static final boolean ENABLE_PERCENT = true; 43 private static final boolean SINGLE_DIGIT_PERCENT = false; 44 private static final boolean SHOW_100_PERCENT = false; 45 46 private static final int FULL = 96; 47 private static final int EMPTY = 4; 48 49 private static final float SUBPIXEL = 0.4f; // inset rects for softer edges 50 private static final float BOLT_LEVEL_THRESHOLD = 0.3f; // opaque bolt below this fraction 51 52 private final int[] mColors; 53 54 boolean mShowPercent = true; 55 private final Paint mFramePaint, mBatteryPaint, mWarningTextPaint, mTextPaint, mBoltPaint; 56 private float mTextHeight, mWarningTextHeight; 57 58 private int mHeight; 59 private int mWidth; 60 private String mWarningString; 61 private final int mChargeColor; 62 private final float[] mBoltPoints; 63 private final Path mBoltPath = new Path(); 64 65 private final RectF mFrame = new RectF(); 66 private final RectF mButtonFrame = new RectF(); 67 private final RectF mBoltFrame = new RectF(); 68 69 private final Path mShapePath = new Path(); 70 private final Path mClipPath = new Path(); 71 72 private class BatteryTracker extends BroadcastReceiver { 73 public static final int UNKNOWN_LEVEL = -1; 74 75 // current battery status 76 int level = UNKNOWN_LEVEL; 77 String percentStr; 78 int plugType; 79 boolean plugged; 80 int health; 81 int status; 82 String technology; 83 int voltage; 84 int temperature; 85 boolean testmode = false; 86 87 @Override 88 public void onReceive(Context context, Intent intent) { 89 final String action = intent.getAction(); 90 if (action.equals(Intent.ACTION_BATTERY_CHANGED)) { 91 if (testmode && ! intent.getBooleanExtra("testmode", false)) return; 92 93 level = (int)(100f 94 * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0) 95 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100)); 96 97 plugType = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); 98 plugged = plugType != 0; 99 health = intent.getIntExtra(BatteryManager.EXTRA_HEALTH, 100 BatteryManager.BATTERY_HEALTH_UNKNOWN); 101 status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, 102 BatteryManager.BATTERY_STATUS_UNKNOWN); 103 technology = intent.getStringExtra(BatteryManager.EXTRA_TECHNOLOGY); 104 voltage = intent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0); 105 temperature = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0); 106 107 setContentDescription( 108 context.getString(R.string.accessibility_battery_level, level)); 109 postInvalidate(); 110 } else if (action.equals(ACTION_LEVEL_TEST)) { 111 testmode = true; 112 post(new Runnable() { 113 int curLevel = 0; 114 int incr = 1; 115 int saveLevel = level; 116 int savePlugged = plugType; 117 Intent dummy = new Intent(Intent.ACTION_BATTERY_CHANGED); 118 @Override 119 public void run() { 120 if (curLevel < 0) { 121 testmode = false; 122 dummy.putExtra("level", saveLevel); 123 dummy.putExtra("plugged", savePlugged); 124 dummy.putExtra("testmode", false); 125 } else { 126 dummy.putExtra("level", curLevel); 127 dummy.putExtra("plugged", incr > 0 ? BatteryManager.BATTERY_PLUGGED_AC : 0); 128 dummy.putExtra("testmode", true); 129 } 130 getContext().sendBroadcast(dummy); 131 132 if (!testmode) return; 133 134 curLevel += incr; 135 if (curLevel == 100) { 136 incr *= -1; 137 } 138 postDelayed(this, 200); 139 } 140 }); 141 } 142 } 143 } 144 145 BatteryTracker mTracker = new BatteryTracker(); 146 147 @Override 148 public void onAttachedToWindow() { 149 super.onAttachedToWindow(); 150 151 IntentFilter filter = new IntentFilter(); 152 filter.addAction(Intent.ACTION_BATTERY_CHANGED); 153 filter.addAction(ACTION_LEVEL_TEST); 154 final Intent sticky = getContext().registerReceiver(mTracker, filter); 155 if (sticky != null) { 156 // preload the battery level 157 mTracker.onReceive(getContext(), sticky); 158 } 159 } 160 161 @Override 162 public void onDetachedFromWindow() { 163 super.onDetachedFromWindow(); 164 165 getContext().unregisterReceiver(mTracker); 166 } 167 168 public BatteryMeterView(Context context) { 169 this(context, null, 0); 170 } 171 172 public BatteryMeterView(Context context, AttributeSet attrs) { 173 this(context, attrs, 0); 174 } 175 176 public BatteryMeterView(Context context, AttributeSet attrs, int defStyle) { 177 super(context, attrs, defStyle); 178 179 final Resources res = context.getResources(); 180 TypedArray levels = res.obtainTypedArray(R.array.batterymeter_color_levels); 181 TypedArray colors = res.obtainTypedArray(R.array.batterymeter_color_values); 182 183 final int N = levels.length(); 184 mColors = new int[2*N]; 185 for (int i=0; i<N; i++) { 186 mColors[2*i] = levels.getInt(i, 0); 187 mColors[2*i+1] = colors.getColor(i, 0); 188 } 189 levels.recycle(); 190 colors.recycle(); 191 mShowPercent = ENABLE_PERCENT && 0 != Settings.System.getInt( 192 context.getContentResolver(), "status_bar_show_battery_percent", 0); 193 if (mShowPercent) { 194 setLayerType(View.LAYER_TYPE_SOFTWARE, null); 195 } 196 mWarningString = context.getString(R.string.battery_meter_very_low_overlay_symbol); 197 198 mFramePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 199 mFramePaint.setColor(res.getColor(R.color.batterymeter_frame_color)); 200 mFramePaint.setDither(true); 201 mFramePaint.setStrokeWidth(0); 202 mFramePaint.setStyle(Paint.Style.FILL_AND_STROKE); 203 204 mBatteryPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 205 mBatteryPaint.setDither(true); 206 mBatteryPaint.setStrokeWidth(0); 207 mBatteryPaint.setStyle(Paint.Style.FILL_AND_STROKE); 208 209 mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 210 Typeface font = Typeface.create("sans-serif-condensed", Typeface.BOLD); 211 mTextPaint.setTypeface(font); 212 mTextPaint.setTextAlign(Paint.Align.CENTER); 213 214 mWarningTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 215 mWarningTextPaint.setColor(mColors[1]); 216 font = Typeface.create("sans-serif", Typeface.BOLD); 217 mWarningTextPaint.setTypeface(font); 218 mWarningTextPaint.setTextAlign(Paint.Align.CENTER); 219 220 mChargeColor = getResources().getColor(R.color.batterymeter_charge_color); 221 222 mBoltPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 223 mBoltPaint.setColor(res.getColor(R.color.batterymeter_bolt_color)); 224 mBoltPoints = loadBoltPoints(res); 225 } 226 227 private static float[] loadBoltPoints(Resources res) { 228 final int[] pts = res.getIntArray(R.array.batterymeter_bolt_points); 229 int maxX = 0, maxY = 0; 230 for (int i = 0; i < pts.length; i += 2) { 231 maxX = Math.max(maxX, pts[i]); 232 maxY = Math.max(maxY, pts[i + 1]); 233 } 234 final float[] ptsF = new float[pts.length]; 235 for (int i = 0; i < pts.length; i += 2) { 236 ptsF[i] = (float)pts[i] / maxX; 237 ptsF[i + 1] = (float)pts[i + 1] / maxY; 238 } 239 return ptsF; 240 } 241 242 @Override 243 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 244 mHeight = h; 245 mWidth = w; 246 mWarningTextPaint.setTextSize(h * 0.75f); 247 mWarningTextHeight = -mWarningTextPaint.getFontMetrics().ascent; 248 } 249 250 private int getColorForLevel(int percent) { 251 int thresh, color = 0; 252 for (int i=0; i<mColors.length; i+=2) { 253 thresh = mColors[i]; 254 color = mColors[i+1]; 255 if (percent <= thresh) return color; 256 } 257 return color; 258 } 259 260 @Override 261 public void draw(Canvas c) { 262 BatteryTracker tracker = mDemoMode ? mDemoTracker : mTracker; 263 final int level = tracker.level; 264 265 if (level == BatteryTracker.UNKNOWN_LEVEL) return; 266 267 float drawFrac = (float) level / 100f; 268 final int pt = getPaddingTop(); 269 final int pl = getPaddingLeft(); 270 final int pr = getPaddingRight(); 271 final int pb = getPaddingBottom(); 272 final int height = mHeight - pt - pb; 273 final int width = mWidth - pl - pr; 274 275 final int buttonHeight = (int) (height * 0.12f); 276 277 mFrame.set(0, 0, width, height); 278 mFrame.offset(pl, pt); 279 280 // button-frame: area above the battery body 281 mButtonFrame.set( 282 mFrame.left + width * 0.25f, 283 mFrame.top, 284 mFrame.right - width * 0.25f, 285 mFrame.top + buttonHeight); 286 287 mButtonFrame.top += SUBPIXEL; 288 mButtonFrame.left += SUBPIXEL; 289 mButtonFrame.right -= SUBPIXEL; 290 291 // frame: battery body area 292 mFrame.top += buttonHeight; 293 mFrame.left += SUBPIXEL; 294 mFrame.top += SUBPIXEL; 295 mFrame.right -= SUBPIXEL; 296 mFrame.bottom -= SUBPIXEL; 297 298 // set the battery charging color 299 mBatteryPaint.setColor(tracker.plugged ? mChargeColor : getColorForLevel(level)); 300 301 if (level >= FULL) { 302 drawFrac = 1f; 303 } else if (level <= EMPTY) { 304 drawFrac = 0f; 305 } 306 307 final float levelTop = drawFrac == 1f ? mButtonFrame.top 308 : (mFrame.top + (mFrame.height() * (1f - drawFrac))); 309 310 // define the battery shape 311 mShapePath.reset(); 312 mShapePath.moveTo(mButtonFrame.left, mButtonFrame.top); 313 mShapePath.lineTo(mButtonFrame.right, mButtonFrame.top); 314 mShapePath.lineTo(mButtonFrame.right, mFrame.top); 315 mShapePath.lineTo(mFrame.right, mFrame.top); 316 mShapePath.lineTo(mFrame.right, mFrame.bottom); 317 mShapePath.lineTo(mFrame.left, mFrame.bottom); 318 mShapePath.lineTo(mFrame.left, mFrame.top); 319 mShapePath.lineTo(mButtonFrame.left, mFrame.top); 320 mShapePath.lineTo(mButtonFrame.left, mButtonFrame.top); 321 322 if (tracker.plugged) { 323 // define the bolt shape 324 final float bl = mFrame.left + mFrame.width() / 4.5f; 325 final float bt = mFrame.top + mFrame.height() / 6f; 326 final float br = mFrame.right - mFrame.width() / 7f; 327 final float bb = mFrame.bottom - mFrame.height() / 10f; 328 if (mBoltFrame.left != bl || mBoltFrame.top != bt 329 || mBoltFrame.right != br || mBoltFrame.bottom != bb) { 330 mBoltFrame.set(bl, bt, br, bb); 331 mBoltPath.reset(); 332 mBoltPath.moveTo( 333 mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(), 334 mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height()); 335 for (int i = 2; i < mBoltPoints.length; i += 2) { 336 mBoltPath.lineTo( 337 mBoltFrame.left + mBoltPoints[i] * mBoltFrame.width(), 338 mBoltFrame.top + mBoltPoints[i + 1] * mBoltFrame.height()); 339 } 340 mBoltPath.lineTo( 341 mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(), 342 mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height()); 343 } 344 345 float boltPct = (mBoltFrame.bottom - levelTop) / (mBoltFrame.bottom - mBoltFrame.top); 346 boltPct = Math.min(Math.max(boltPct, 0), 1); 347 if (boltPct <= BOLT_LEVEL_THRESHOLD) { 348 // draw the bolt if opaque 349 c.drawPath(mBoltPath, mBoltPaint); 350 } else { 351 // otherwise cut the bolt out of the overall shape 352 mShapePath.op(mBoltPath, Path.Op.DIFFERENCE); 353 } 354 } 355 356 // draw the battery shape background 357 c.drawPath(mShapePath, mFramePaint); 358 359 // draw the battery shape, clipped to charging level 360 mFrame.top = levelTop; 361 mClipPath.reset(); 362 mClipPath.addRect(mFrame, Path.Direction.CCW); 363 mShapePath.op(mClipPath, Path.Op.INTERSECT); 364 c.drawPath(mShapePath, mBatteryPaint); 365 366 if (!tracker.plugged) { 367 if (level <= EMPTY) { 368 // draw the warning text 369 final float x = mWidth * 0.5f; 370 final float y = (mHeight + mWarningTextHeight) * 0.48f; 371 c.drawText(mWarningString, x, y, mWarningTextPaint); 372 } else if (mShowPercent && !(tracker.level == 100 && !SHOW_100_PERCENT)) { 373 // draw the percentage text 374 mTextPaint.setColor(getColorForLevel(level)); 375 mTextPaint.setTextSize(height * 376 (SINGLE_DIGIT_PERCENT ? 0.75f 377 : (tracker.level == 100 ? 0.38f : 0.5f))); 378 mTextHeight = -mTextPaint.getFontMetrics().ascent; 379 380 final String str = String.valueOf(SINGLE_DIGIT_PERCENT ? (level/10) : level); 381 final float x = mWidth * 0.5f; 382 final float y = (mHeight + mTextHeight) * 0.47f; 383 384 boolean opaque = levelTop > y; 385 if (opaque) { 386 mTextPaint.setXfermode(null); 387 } else { 388 mTextPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); 389 } 390 391 c.drawText(str, 392 x, 393 y, 394 mTextPaint); 395 } 396 } 397 } 398 399 private boolean mDemoMode; 400 private BatteryTracker mDemoTracker = new BatteryTracker(); 401 402 @Override 403 public void dispatchDemoCommand(String command, Bundle args) { 404 if (!mDemoMode && command.equals(COMMAND_ENTER)) { 405 mDemoMode = true; 406 mDemoTracker.level = mTracker.level; 407 mDemoTracker.plugged = mTracker.plugged; 408 } else if (mDemoMode && command.equals(COMMAND_EXIT)) { 409 mDemoMode = false; 410 postInvalidate(); 411 } else if (mDemoMode && command.equals(COMMAND_BATTERY)) { 412 String level = args.getString("level"); 413 String plugged = args.getString("plugged"); 414 if (level != null) { 415 mDemoTracker.level = Math.min(Math.max(Integer.parseInt(level), 0), 100); 416 } 417 if (plugged != null) { 418 mDemoTracker.plugged = Boolean.parseBoolean(plugged); 419 } 420 postInvalidate(); 421 } 422 } 423} 424