/* * Copyright (C) 2013-14 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui; import com.android.systemui.statusbar.policy.BatteryController; import android.animation.ArgbEvaluator; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Typeface; import android.os.BatteryManager; import android.os.Bundle; import android.util.AttributeSet; import android.view.View; public class BatteryMeterView extends View implements DemoMode, BatteryController.BatteryStateChangeCallback { public static final String TAG = BatteryMeterView.class.getSimpleName(); public static final String ACTION_LEVEL_TEST = "com.android.systemui.BATTERY_LEVEL_TEST"; private static final int FULL = 96; private static final float BOLT_LEVEL_THRESHOLD = 0.3f; // opaque bolt below this fraction private final int[] mColors; protected boolean mShowPercent = true; private float mButtonHeightFraction; private float mSubpixelSmoothingLeft; private float mSubpixelSmoothingRight; public enum BatteryMeterMode { BATTERY_METER_GONE, BATTERY_METER_ICON_PORTRAIT, BATTERY_METER_ICON_LANDSCAPE, BATTERY_METER_CIRCLE, BATTERY_METER_TEXT } private int mHeight; private int mWidth; private String mWarningString; private final int mCriticalLevel; private final int mFrameColor; private boolean mAnimationsEnabled; private final Path mShapePath = new Path(); private final Path mClipPath = new Path(); private final Path mTextPath = new Path(); private BatteryController mBatteryController; private boolean mPowerSaveEnabled; private int mDarkModeBackgroundColor; private int mDarkModeFillColor; private int mLightModeBackgroundColor; private int mLightModeFillColor; protected BatteryMeterMode mMeterMode = null; protected boolean mAttached; private boolean mDemoMode; protected BatteryTracker mDemoTracker = new BatteryTracker(); protected BatteryTracker mTracker = new BatteryTracker(); private BatteryMeterDrawable mBatteryMeterDrawable; private int mIconTint = Color.WHITE; private class BatteryTracker extends BroadcastReceiver { public static final int UNKNOWN_LEVEL = -1; // current battery status boolean present = true; int level = UNKNOWN_LEVEL; String percentStr; int plugType; boolean plugged; int health; int status; String technology; int voltage; int temperature; boolean testmode = false; @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); if (action.equals(Intent.ACTION_BATTERY_CHANGED)) { if (testmode && ! intent.getBooleanExtra("testmode", false)) return; level = (int)(100f * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0) / intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100)); present = intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true); plugType = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); plugged = plugType != 0; health = intent.getIntExtra(BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_UNKNOWN); status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN); technology = intent.getStringExtra(BatteryManager.EXTRA_TECHNOLOGY); voltage = intent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0); temperature = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0); setContentDescription( context.getString(R.string.accessibility_battery_level, level)); if (mBatteryMeterDrawable != null) { setVisibility(View.VISIBLE); invalidate(); } } else if (action.equals(ACTION_LEVEL_TEST)) { testmode = true; post(new Runnable() { int curLevel = 0; int incr = 1; int saveLevel = level; int savePlugged = plugType; Intent dummy = new Intent(Intent.ACTION_BATTERY_CHANGED); @Override public void run() { if (curLevel < 0) { testmode = false; dummy.putExtra("level", saveLevel); dummy.putExtra("plugged", savePlugged); dummy.putExtra("testmode", false); } else { dummy.putExtra("level", curLevel); dummy.putExtra("plugged", incr > 0 ? BatteryManager.BATTERY_PLUGGED_AC : 0); dummy.putExtra("testmode", true); } getContext().sendBroadcast(dummy); if (!testmode) return; curLevel += incr; if (curLevel == 100) { incr *= -1; } postDelayed(this, 200); } }); } } protected boolean shouldIndicateCharging() { if (status == BatteryManager.BATTERY_STATUS_CHARGING) { return true; } if (plugged) { return status == BatteryManager.BATTERY_STATUS_FULL; } return false; } } @Override public void onAttachedToWindow() { super.onAttachedToWindow(); IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_BATTERY_CHANGED); filter.addAction(ACTION_LEVEL_TEST); final Intent sticky = getContext().registerReceiver(mTracker, filter); if (sticky != null) { // preload the battery level mTracker.onReceive(getContext(), sticky); } mBatteryController.addStateChangedCallback(this); mAttached = true; } @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); mAttached = false; getContext().unregisterReceiver(mTracker); mBatteryController.removeStateChangedCallback(this); } public BatteryMeterView(Context context) { this(context, null, 0); } public BatteryMeterView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public BatteryMeterView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); final Resources res = context.getResources(); TypedArray atts = context.obtainStyledAttributes(attrs, R.styleable.BatteryMeterView, defStyle, 0); mFrameColor = atts.getColor(R.styleable.BatteryMeterView_frameColor, res.getColor(R.color.batterymeter_frame_color)); TypedArray levels = res.obtainTypedArray(R.array.batterymeter_color_levels); TypedArray colors = res.obtainTypedArray(R.array.batterymeter_color_values); final int N = levels.length(); mColors = new int[2*N]; for (int i=0; i= FULL) { drawFrac = 1f; } else if (level <= mCriticalLevel) { drawFrac = 0f; } final float levelTop; if (drawFrac == 1f) { if (mHorizontal) { levelTop = mButtonFrame.right; } else { levelTop = mButtonFrame.top; } } else { if (mHorizontal) { levelTop = (mFrame.right - (mFrame.width() * (1f - drawFrac))); } else { levelTop = (mFrame.top + (mFrame.height() * (1f - drawFrac))); } } // define the battery shape mShapePath.reset(); mShapePath.moveTo(mButtonFrame.left, mButtonFrame.top); if (mHorizontal) { mShapePath.lineTo(mButtonFrame.right, mButtonFrame.top); mShapePath.lineTo(mButtonFrame.right, mButtonFrame.bottom); mShapePath.lineTo(mButtonFrame.left, mButtonFrame.bottom); mShapePath.lineTo(mFrame.right, mFrame.bottom); mShapePath.lineTo(mFrame.left, mFrame.bottom); mShapePath.lineTo(mFrame.left, mFrame.top); mShapePath.lineTo(mButtonFrame.left, mFrame.top); mShapePath.lineTo(mButtonFrame.left, mButtonFrame.top); } else { mShapePath.lineTo(mButtonFrame.right, mButtonFrame.top); mShapePath.lineTo(mButtonFrame.right, mFrame.top); mShapePath.lineTo(mFrame.right, mFrame.top); mShapePath.lineTo(mFrame.right, mFrame.bottom); mShapePath.lineTo(mFrame.left, mFrame.bottom); mShapePath.lineTo(mFrame.left, mFrame.top); mShapePath.lineTo(mButtonFrame.left, mFrame.top); mShapePath.lineTo(mButtonFrame.left, mButtonFrame.top); } if (tracker.plugged) { // define the bolt shape final float bl = mFrame.left + mFrame.width() / (mHorizontal ? 9f : 4.5f); final float bt = mFrame.top + mFrame.height() / (mHorizontal ? 4.5f : 6f); final float br = mFrame.right - mFrame.width() / (mHorizontal ? 6f : 7f); final float bb = mFrame.bottom - mFrame.height() / (mHorizontal ? 7f : 10f); if (mBoltFrame.left != bl || mBoltFrame.top != bt || mBoltFrame.right != br || mBoltFrame.bottom != bb) { mBoltFrame.set(bl, bt, br, bb); mBoltPath.reset(); mBoltPath.moveTo( mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(), mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height()); for (int i = 2; i < mBoltPoints.length; i += 2) { mBoltPath.lineTo( mBoltFrame.left + mBoltPoints[i] * mBoltFrame.width(), mBoltFrame.top + mBoltPoints[i + 1] * mBoltFrame.height()); } mBoltPath.lineTo( mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(), mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height()); } float boltPct = mHorizontal ? (mBoltFrame.left - levelTop) / (mBoltFrame.left - mBoltFrame.right) : (mBoltFrame.bottom - levelTop) / (mBoltFrame.bottom - mBoltFrame.top); boltPct = Math.min(Math.max(boltPct, 0), 1); if (boltPct <= BOLT_LEVEL_THRESHOLD) { // draw the bolt if opaque c.drawPath(mBoltPath, mBoltPaint); } else { // otherwise cut the bolt out of the overall shape mShapePath.op(mBoltPath, Path.Op.DIFFERENCE); } } // compute percentage text boolean pctOpaque = false; float pctX = 0, pctY = 0; String pctText = null; if (!tracker.plugged && level > mCriticalLevel && mShowPercent) { mTextPaint.setColor(getColorForLevel(level)); final float full = mHorizontal ? 0.60f : 0.45f; final float nofull = mHorizontal ? 0.75f : 0.6f; final float single = mHorizontal ? 0.86f : 0.75f; mTextPaint.setTextSize(height * (SINGLE_DIGIT_PERCENT ? single : (tracker.level == 100 ? full : nofull))); mTextHeight = -mTextPaint.getFontMetrics().ascent; pctText = String.valueOf(SINGLE_DIGIT_PERCENT ? (level/10) : level); pctX = mWidth * 0.5f; pctY = (mHeight + mTextHeight) * 0.47f; if (mHorizontal) { pctOpaque = pctX > levelTop; } else { pctOpaque = levelTop > pctY; } if (!pctOpaque) { mTextPath.reset(); mTextPaint.getTextPath(pctText, 0, pctText.length(), pctX, pctY, mTextPath); // cut the percentage text out of the overall shape mShapePath.op(mTextPath, Path.Op.DIFFERENCE); } } // draw the battery shape background c.drawPath(mShapePath, mFramePaint); // draw the battery shape, clipped to charging level if (mHorizontal) { mFrame.right = levelTop; } else { mFrame.top = levelTop; } mClipPath.reset(); mClipPath.addRect(mFrame, Path.Direction.CCW); mShapePath.op(mClipPath, Path.Op.INTERSECT); c.drawPath(mShapePath, mBatteryPaint); if (!tracker.plugged) { if (level <= mCriticalLevel) { // draw the warning text final float x = mWidth * 0.5f; final float y = (mHeight + mWarningTextHeight) * 0.48f; c.drawText(mWarningString, x, y, mWarningTextPaint); } else if (pctOpaque) { // draw the percentage text c.drawText(pctText, pctX, pctY, mTextPaint); } } } @Override public void onDispose() { mDisposed = true; } @Override public void setDarkIntensity(int backgroundColor, int fillColor) { mIconTint = fillColor; mFramePaint.setColor(backgroundColor); mBoltPaint.setColor(fillColor); mChargeColor = fillColor; invalidate(); } @Override public void onSizeChanged(int w, int h, int oldw, int oldh) { mHeight = h; mWidth = w; mWarningTextPaint.setTextSize(h * 0.75f); mWarningTextHeight = -mWarningTextPaint.getFontMetrics().ascent; } private float[] loadBoltPoints(Resources res) { final int[] pts = res.getIntArray((mHorizontal ? R.array.batterymeter_inverted_bolt_points : R.array.batterymeter_bolt_points)); int maxX = 0, maxY = 0; for (int i = 0; i < pts.length; i += 2) { maxX = Math.max(maxX, pts[i]); maxY = Math.max(maxY, pts[i + 1]); } final float[] ptsF = new float[pts.length]; for (int i = 0; i < pts.length; i += 2) { ptsF[i] = (float)pts[i] / maxX; ptsF[i + 1] = (float)pts[i + 1] / maxY; } return ptsF; } } protected class CircleBatteryMeterDrawable implements BatteryMeterDrawable { private static final boolean SINGLE_DIGIT_PERCENT = false; private static final boolean SHOW_100_PERCENT = false; private static final int FULL = 96; public static final float STROKE_WITH = 6.5f; private boolean mDisposed; private int mAnimOffset; private boolean mIsAnimating; // stores charge-animation status to reliably //remove callbacks private int mCircleSize; // draw size of circle private RectF mRectLeft; // contains the precalculated rect used in drawArc(), // derived from mCircleSize private float mTextX, mTextY; // precalculated position for drawText() to appear centered private Paint mTextPaint; private Paint mFrontPaint; private Paint mBackPaint; private Paint mBoltPaint; private Paint mWarningTextPaint; private final RectF mBoltFrame = new RectF(); private int mChargeColor; private final float[] mBoltPoints; private final Path mBoltPath = new Path(); public CircleBatteryMeterDrawable(Resources res) { super(); mDisposed = false; mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); Typeface font = Typeface.create("sans-serif-condensed", Typeface.BOLD); mTextPaint.setTypeface(font); mTextPaint.setTextAlign(Paint.Align.CENTER); mFrontPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mFrontPaint.setStrokeCap(Paint.Cap.BUTT); mFrontPaint.setDither(true); mFrontPaint.setStrokeWidth(0); mFrontPaint.setStyle(Paint.Style.STROKE); mBackPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBackPaint.setColor(res.getColor(R.color.batterymeter_frame_color)); mBackPaint.setStrokeCap(Paint.Cap.BUTT); mBackPaint.setDither(true); mBackPaint.setStrokeWidth(0); mBackPaint.setStyle(Paint.Style.STROKE); mWarningTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mWarningTextPaint.setColor(mColors[1]); font = Typeface.create("sans-serif", Typeface.BOLD); mWarningTextPaint.setTypeface(font); mWarningTextPaint.setTextAlign(Paint.Align.CENTER); mChargeColor = getResources().getColor(R.color.batterymeter_charge_color); mBoltPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBoltPaint.setColor(res.getColor(R.color.batterymeter_bolt_color)); mBoltPoints = loadBoltPoints(res); } @Override public void onDraw(Canvas c, BatteryTracker tracker) { if (mDisposed) return; if (mRectLeft == null) { initSizeBasedStuff(); } drawCircle(c, tracker, mTextX, mRectLeft); if (mAnimationsEnabled) { updateChargeAnim(tracker); } } @Override public void onDispose() { mDisposed = true; } @Override public void setDarkIntensity(int backgroundColor, int fillColor) { mIconTint = fillColor; mBoltPaint.setColor(fillColor); mChargeColor = fillColor; invalidate(); } @Override public void onSizeChanged(int w, int h, int oldw, int oldh) { initSizeBasedStuff(); } private float[] loadBoltPoints(Resources res) { final int[] pts = res.getIntArray(R.array.batterymeter_bolt_points); int maxX = 0, maxY = 0; for (int i = 0; i < pts.length; i += 2) { maxX = Math.max(maxX, pts[i]); maxY = Math.max(maxY, pts[i + 1]); } final float[] ptsF = new float[pts.length]; for (int i = 0; i < pts.length; i += 2) { ptsF[i] = (float)pts[i] / maxX; ptsF[i + 1] = (float)pts[i + 1] / maxY; } return ptsF; } private void drawCircle(Canvas canvas, BatteryTracker tracker, float textX, RectF drawRect) { boolean unknownStatus = tracker.status == BatteryManager.BATTERY_STATUS_UNKNOWN; int level = tracker.level; Paint paint; if (unknownStatus) { paint = mBackPaint; level = 100; // Draw all the circle; } else { paint = mFrontPaint; paint.setColor(getColorForLevel(level)); if (tracker.status == BatteryManager.BATTERY_STATUS_FULL) { level = 100; } } // draw thin gray ring first canvas.drawArc(drawRect, 270, 360, false, mBackPaint); if (level != 0) { // draw colored arc representing charge level canvas.drawArc(drawRect, 270 + mAnimOffset, 3.6f * level, false, paint); } // if chosen by options, draw percentage text in the middle // always skip percentage when 100, so layout doesnt break if (unknownStatus) { mTextPaint.setColor(paint.getColor()); canvas.drawText("?", textX, mTextY, mTextPaint); } else if (tracker.plugged) { canvas.drawPath(mBoltPath, mBoltPaint); } else { if (level > mCriticalLevel && (mShowPercent && !(tracker.level == 100 && !SHOW_100_PERCENT))) { // draw the percentage text String pctText = String.valueOf(SINGLE_DIGIT_PERCENT ? (level/10) : level); mTextPaint.setColor(paint.getColor()); canvas.drawText(pctText, textX, mTextY, mTextPaint); } else if (level <= mCriticalLevel) { // draw the warning text canvas.drawText(mWarningString, textX, mTextY, mWarningTextPaint); } } } /** * updates the animation counter * cares for timed callbacks to continue animation cycles * uses mInvalidate for delayed invalidate() callbacks */ private void updateChargeAnim(BatteryTracker tracker) { // Stop animation when battery is full or after the meter // rotated back to 0 after unplugging. if (!tracker.shouldIndicateCharging() || tracker.status == BatteryManager.BATTERY_STATUS_FULL || tracker.level == 0) { mIsAnimating = false; } else { mIsAnimating = true; } if (mAnimOffset > 360) { mAnimOffset = 0; } boolean continueAnimation = mIsAnimating || mAnimOffset != 0; if (continueAnimation) { mAnimOffset += 3; } if (continueAnimation) { postInvalidateDelayed(50); } } /** * initializes all size dependent variables * sets stroke width and text size of all involved paints * YES! i think the method name is appropriate */ private void initSizeBasedStuff() { mCircleSize = Math.min(getMeasuredWidth(), getMeasuredHeight()); mTextPaint.setTextSize(mCircleSize / 2f); mWarningTextPaint.setTextSize(mCircleSize / 2f); float strokeWidth = mCircleSize / STROKE_WITH; mFrontPaint.setStrokeWidth(strokeWidth); mBackPaint.setStrokeWidth(strokeWidth); // calculate rectangle for drawArc calls int pLeft = getPaddingLeft(); mRectLeft = new RectF(pLeft + strokeWidth / 2.0f, 0 + strokeWidth / 2.0f, mCircleSize - strokeWidth / 2.0f + pLeft, mCircleSize - strokeWidth / 2.0f); // calculate Y position for text Rect bounds = new Rect(); mTextPaint.getTextBounds("99", 0, "99".length(), bounds); mTextX = mCircleSize / 2.0f + getPaddingLeft(); // the +1dp at end of formula balances out rounding issues.works out on all resolutions mTextY = mCircleSize / 2.0f + (bounds.bottom - bounds.top) / 2.0f - strokeWidth / 2.0f + getResources().getDisplayMetrics().density; // draw the bolt final float bl = (int) (mRectLeft.left + mRectLeft.width() / 3.2f); final float bt = (int) (mRectLeft.top + mRectLeft.height() / 4f); final float br = (int) (mRectLeft.right - mRectLeft.width() / 5.2f); final float bb = (int) (mRectLeft.bottom - mRectLeft.height() / 8f); if (mBoltFrame.left != bl || mBoltFrame.top != bt || mBoltFrame.right != br || mBoltFrame.bottom != bb) { mBoltFrame.set(bl, bt, br, bb); mBoltPath.reset(); mBoltPath.moveTo( mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(), mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height()); for (int i = 2; i < mBoltPoints.length; i += 2) { mBoltPath.lineTo( mBoltFrame.left + mBoltPoints[i] * mBoltFrame.width(), mBoltFrame.top + mBoltPoints[i + 1] * mBoltFrame.height()); } mBoltPath.lineTo( mBoltFrame.left + mBoltPoints[0] * mBoltFrame.width(), mBoltFrame.top + mBoltPoints[1] * mBoltFrame.height()); } } } }