blob: 076b5bcd0861c6eb9dc674e3e4dae29cfdc6776f [file] [log] [blame]
/*
* Copyright (C) 2015 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 android.animation.ArgbEvaluator;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.database.ContentObserver;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.Settings;
import com.android.systemui.statusbar.policy.BatteryController;
public class BatteryMeterDrawable extends Drawable implements
BatteryController.BatteryStateChangeCallback {
private static final float ASPECT_RATIO = 9.5f / 14.5f;
public static final String TAG = BatteryMeterDrawable.class.getSimpleName();
public static final String SHOW_PERCENT_SETTING = "status_bar_show_battery_percent";
private static final boolean SINGLE_DIGIT_PERCENT = false;
private static final int FULL = 96;
private static final float BOLT_LEVEL_THRESHOLD = 0.3f; // opaque bolt below this fraction
private final int[] mColors;
private final int mIntrinsicWidth;
private final int mIntrinsicHeight;
private boolean mShowPercent;
private float mButtonHeightFraction;
private float mSubpixelSmoothingLeft;
private float mSubpixelSmoothingRight;
private final Paint mFramePaint, mBatteryPaint, mWarningTextPaint, mTextPaint, mBoltPaint,
mPlusPaint;
private float mTextHeight, mWarningTextHeight;
private int mIconTint = Color.WHITE;
private float mOldDarkIntensity = 0f;
private int mHeight;
private int mWidth;
private String mWarningString;
private final int mCriticalLevel;
private int mChargeColor;
private final float[] mBoltPoints;
private final Path mBoltPath = new Path();
private final float[] mPlusPoints;
private final Path mPlusPath = new Path();
private final RectF mFrame = new RectF();
private final RectF mButtonFrame = new RectF();
private final RectF mBoltFrame = new RectF();
private final RectF mPlusFrame = new RectF();
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;
private final SettingObserver mSettingObserver = new SettingObserver();
private final Context mContext;
private final Handler mHandler;
private int mLevel = -1;
private boolean mPluggedIn;
private boolean mListening;
public BatteryMeterDrawable(Context context, Handler handler, int frameColor) {
mContext = context;
mHandler = handler;
final Resources res = context.getResources();
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<N; i++) {
mColors[2*i] = levels.getInt(i, 0);
mColors[2*i+1] = colors.getColor(i, 0);
}
levels.recycle();
colors.recycle();
updateShowPercent();
mWarningString = context.getString(R.string.battery_meter_very_low_overlay_symbol);
mCriticalLevel = mContext.getResources().getInteger(
com.android.internal.R.integer.config_criticalBatteryWarningLevel);
mButtonHeightFraction = context.getResources().getFraction(
R.fraction.battery_button_height_fraction, 1, 1);
mSubpixelSmoothingLeft = context.getResources().getFraction(
R.fraction.battery_subpixel_smoothing_left, 1, 1);
mSubpixelSmoothingRight = context.getResources().getFraction(
R.fraction.battery_subpixel_smoothing_right, 1, 1);
mFramePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mFramePaint.setColor(frameColor);
mFramePaint.setDither(true);
mFramePaint.setStrokeWidth(0);
mFramePaint.setStyle(Paint.Style.FILL_AND_STROKE);
mBatteryPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBatteryPaint.setDither(true);
mBatteryPaint.setStrokeWidth(0);
mBatteryPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
Typeface font = Typeface.create("sans-serif-condensed", Typeface.BOLD);
mTextPaint.setTypeface(font);
mTextPaint.setTextAlign(Paint.Align.CENTER);
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 = context.getColor(R.color.batterymeter_charge_color);
mBoltPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBoltPaint.setColor(context.getColor(R.color.batterymeter_bolt_color));
mBoltPoints = loadBoltPoints(res);
mPlusPaint = new Paint(mBoltPaint);
mPlusPoints = loadPlusPoints(res);
mDarkModeBackgroundColor =
context.getColor(R.color.dark_mode_icon_color_dual_tone_background);
mDarkModeFillColor = context.getColor(R.color.dark_mode_icon_color_dual_tone_fill);
mLightModeBackgroundColor =
context.getColor(R.color.light_mode_icon_color_dual_tone_background);
mLightModeFillColor = context.getColor(R.color.light_mode_icon_color_dual_tone_fill);
mIntrinsicWidth = context.getResources().getDimensionPixelSize(R.dimen.battery_width);
mIntrinsicHeight = context.getResources().getDimensionPixelSize(R.dimen.battery_height);
}
@Override
public int getIntrinsicHeight() {
return mIntrinsicHeight;
}
@Override
public int getIntrinsicWidth() {
return mIntrinsicWidth;
}
public void startListening() {
mListening = true;
mContext.getContentResolver().registerContentObserver(
Settings.System.getUriFor(SHOW_PERCENT_SETTING), false, mSettingObserver);
updateShowPercent();
mBatteryController.addStateChangedCallback(this);
}
public void stopListening() {
mListening = false;
mContext.getContentResolver().unregisterContentObserver(mSettingObserver);
mBatteryController.removeStateChangedCallback(this);
}
public void disableShowPercent() {
mShowPercent = false;
postInvalidate();
}
private void postInvalidate() {
mHandler.post(new Runnable() {
@Override
public void run() {
invalidateSelf();
}
});
}
public void setBatteryController(BatteryController batteryController) {
mBatteryController = batteryController;
mPowerSaveEnabled = mBatteryController.isPowerSave();
}
@Override
public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
mLevel = level;
mPluggedIn = pluggedIn;
postInvalidate();
}
@Override
public void onPowerSaveChanged(boolean isPowerSave) {
mPowerSaveEnabled = isPowerSave;
invalidateSelf();
}
private static 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 static float[] loadPlusPoints(Resources res) {
final int[] pts = res.getIntArray(R.array.batterymeter_plus_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;
}
@Override
public void setBounds(int left, int top, int right, int bottom) {
super.setBounds(left, top, right, bottom);
mHeight = bottom - top;
mWidth = right - left;
mWarningTextPaint.setTextSize(mHeight * 0.75f);
mWarningTextHeight = -mWarningTextPaint.getFontMetrics().ascent;
}
private void updateShowPercent() {
mShowPercent = 0 != Settings.System.getInt(mContext.getContentResolver(),
SHOW_PERCENT_SETTING, 0);
}
private int getColorForLevel(int percent) {
// If we are in power save mode, always use the normal color.
if (mPowerSaveEnabled) {
return mColors[mColors.length-1];
}
int thresh, color = 0;
for (int i=0; i<mColors.length; i+=2) {
thresh = mColors[i];
color = mColors[i+1];
if (percent <= thresh) {
// Respect tinting for "normal" level
if (i == mColors.length-2) {
return mIconTint;
} else {
return color;
}
}
}
return color;
}
public void setDarkIntensity(float darkIntensity) {
if (darkIntensity == mOldDarkIntensity) {
return;
}
int backgroundColor = getBackgroundColor(darkIntensity);
int fillColor = getFillColor(darkIntensity);
mIconTint = fillColor;
mFramePaint.setColor(backgroundColor);
mBoltPaint.setColor(fillColor);
mChargeColor = fillColor;
invalidateSelf();
mOldDarkIntensity = darkIntensity;
}
private int getBackgroundColor(float darkIntensity) {
return getColorForDarkIntensity(
darkIntensity, mLightModeBackgroundColor, mDarkModeBackgroundColor);
}
private int getFillColor(float darkIntensity) {
return getColorForDarkIntensity(
darkIntensity, mLightModeFillColor, mDarkModeFillColor);
}
private int getColorForDarkIntensity(float darkIntensity, int lightColor, int darkColor) {
return (int) ArgbEvaluator.getInstance().evaluate(darkIntensity, lightColor, darkColor);
}
@Override
public void draw(Canvas c) {
final int level = mLevel;
if (level == -1) return;
float drawFrac = (float) level / 100f;
final int height = mHeight;
final int width = (int) (ASPECT_RATIO * mHeight);
int px = (mWidth - width) / 2;
final int buttonHeight = (int) (height * mButtonHeightFraction);
mFrame.set(0, 0, width, height);
mFrame.offset(px, 0);
// button-frame: area above the battery body
mButtonFrame.set(
mFrame.left + Math.round(width * 0.25f),
mFrame.top,
mFrame.right - Math.round(width * 0.25f),
mFrame.top + buttonHeight);
mButtonFrame.top += mSubpixelSmoothingLeft;
mButtonFrame.left += mSubpixelSmoothingLeft;
mButtonFrame.right -= mSubpixelSmoothingRight;
// frame: battery body area
mFrame.top += buttonHeight;
mFrame.left += mSubpixelSmoothingLeft;
mFrame.top += mSubpixelSmoothingLeft;
mFrame.right -= mSubpixelSmoothingRight;
mFrame.bottom -= mSubpixelSmoothingRight;
// set the battery charging color
mBatteryPaint.setColor(mPluggedIn ? mChargeColor : getColorForLevel(level));
if (level >= FULL) {
drawFrac = 1f;
} else if (level <= mCriticalLevel) {
drawFrac = 0f;
}
final float levelTop = drawFrac == 1f ? mButtonFrame.top
: (mFrame.top + (mFrame.height() * (1f - drawFrac)));
// define the battery shape
mShapePath.reset();
mShapePath.moveTo(mButtonFrame.left, mButtonFrame.top);
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 (mPluggedIn) {
// define the bolt shape
final float bl = mFrame.left + mFrame.width() / 4f;
final float bt = mFrame.top + mFrame.height() / 6f;
final float br = mFrame.right - mFrame.width() / 4f;
final float bb = mFrame.bottom - mFrame.height() / 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 = (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);
}
} else if (mPowerSaveEnabled) {
// define the plus shape
final float pw = mFrame.width() * 2 / 3;
final float pl = mFrame.left + (mFrame.width() - pw) / 2;
final float pt = mFrame.top + (mFrame.height() - pw) / 2;
final float pr = mFrame.right - (mFrame.width() - pw) / 2;
final float pb = mFrame.bottom - (mFrame.height() - pw) / 2;
if (mPlusFrame.left != pl || mPlusFrame.top != pt
|| mPlusFrame.right != pr || mPlusFrame.bottom != pb) {
mPlusFrame.set(pl, pt, pr, pb);
mPlusPath.reset();
mPlusPath.moveTo(
mPlusFrame.left + mPlusPoints[0] * mPlusFrame.width(),
mPlusFrame.top + mPlusPoints[1] * mPlusFrame.height());
for (int i = 2; i < mPlusPoints.length; i += 2) {
mPlusPath.lineTo(
mPlusFrame.left + mPlusPoints[i] * mPlusFrame.width(),
mPlusFrame.top + mPlusPoints[i + 1] * mPlusFrame.height());
}
mPlusPath.lineTo(
mPlusFrame.left + mPlusPoints[0] * mPlusFrame.width(),
mPlusFrame.top + mPlusPoints[1] * mPlusFrame.height());
}
float boltPct = (mPlusFrame.bottom - levelTop) / (mPlusFrame.bottom - mPlusFrame.top);
boltPct = Math.min(Math.max(boltPct, 0), 1);
if (boltPct <= BOLT_LEVEL_THRESHOLD) {
// draw the bolt if opaque
c.drawPath(mPlusPath, mPlusPaint);
} else {
// otherwise cut the bolt out of the overall shape
mShapePath.op(mPlusPath, Path.Op.DIFFERENCE);
}
}
// compute percentage text
boolean pctOpaque = false;
float pctX = 0, pctY = 0;
String pctText = null;
if (!mPluggedIn && !mPowerSaveEnabled && level > mCriticalLevel && mShowPercent) {
mTextPaint.setColor(getColorForLevel(level));
mTextPaint.setTextSize(height *
(SINGLE_DIGIT_PERCENT ? 0.75f
: (mLevel == 100 ? 0.38f : 0.5f)));
mTextHeight = -mTextPaint.getFontMetrics().ascent;
pctText = String.valueOf(SINGLE_DIGIT_PERCENT ? (level/10) : level);
pctX = mWidth * 0.5f;
pctY = (mHeight + mTextHeight) * 0.47f;
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
mFrame.top = levelTop;
mClipPath.reset();
mClipPath.addRect(mFrame, Path.Direction.CCW);
mShapePath.op(mClipPath, Path.Op.INTERSECT);
c.drawPath(mShapePath, mBatteryPaint);
if (!mPluggedIn && !mPowerSaveEnabled) {
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);
}
}
}
// Some stuff required by Drawable.
@Override
public void setAlpha(int alpha) {
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
}
@Override
public int getOpacity() {
return 0;
}
private final class SettingObserver extends ContentObserver {
public SettingObserver() {
super(new Handler());
}
@Override
public void onChange(boolean selfChange, Uri uri) {
super.onChange(selfChange, uri);
updateShowPercent();
postInvalidate();
}
}
}