fix: 首次提交
This commit is contained in:
@@ -0,0 +1,508 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.settingslib.graph;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Style;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Path.Direction;
|
||||
import android.graphics.Path.FillType;
|
||||
import android.graphics.Path.Op;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.settingslib.R;
|
||||
import com.android.settingslib.Utils;
|
||||
|
||||
public class BatteryMeterDrawableBase extends Drawable {
|
||||
|
||||
private static final float ASPECT_RATIO = .58f;
|
||||
public static final String TAG = BatteryMeterDrawableBase.class.getSimpleName();
|
||||
private static final float RADIUS_RATIO = 1.0f / 17f;
|
||||
|
||||
protected final Context mContext;
|
||||
protected final Paint mFramePaint;
|
||||
protected final Paint mBatteryPaint;
|
||||
protected final Paint mWarningTextPaint;
|
||||
protected final Paint mTextPaint;
|
||||
protected final Paint mBoltPaint;
|
||||
protected final Paint mPlusPaint;
|
||||
protected final Paint mPowersavePaint;
|
||||
protected float mButtonHeightFraction;
|
||||
|
||||
private int mLevel = -1;
|
||||
private boolean mCharging;
|
||||
private boolean mPowerSaveEnabled;
|
||||
protected boolean mPowerSaveAsColorError = true;
|
||||
private boolean mShowPercent;
|
||||
|
||||
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 float mSubpixelSmoothingLeft;
|
||||
private float mSubpixelSmoothingRight;
|
||||
private float mTextHeight, mWarningTextHeight;
|
||||
private int mIconTint = Color.WHITE;
|
||||
private float mOldDarkIntensity = -1f;
|
||||
|
||||
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 Rect mPadding = new Rect();
|
||||
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 mOutlinePath = new Path();
|
||||
private final Path mTextPath = new Path();
|
||||
|
||||
public BatteryMeterDrawableBase(Context context, int frameColor) {
|
||||
mContext = context;
|
||||
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);
|
||||
if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) {
|
||||
mColors[2 * i + 1] = Utils.getColorAttrDefaultColor(context,
|
||||
colors.getThemeAttributeId(i, 0));
|
||||
} else {
|
||||
mColors[2 * i + 1] = colors.getColor(i, 0);
|
||||
}
|
||||
}
|
||||
levels.recycle();
|
||||
colors.recycle();
|
||||
|
||||
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);
|
||||
font = Typeface.create("sans-serif", Typeface.BOLD);
|
||||
mWarningTextPaint.setTypeface(font);
|
||||
mWarningTextPaint.setTextAlign(Paint.Align.CENTER);
|
||||
if (mColors.length > 1) {
|
||||
mWarningTextPaint.setColor(mColors[1]);
|
||||
}
|
||||
|
||||
mChargeColor = Utils.getColorStateListDefaultColor(mContext, R.color.meter_consumed_color);
|
||||
|
||||
mBoltPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
mBoltPaint.setColor(Utils.getColorStateListDefaultColor(mContext,
|
||||
R.color.batterymeter_bolt_color));
|
||||
mBoltPoints = loadPoints(res, R.array.batterymeter_bolt_points);
|
||||
|
||||
mPlusPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
mPlusPaint.setColor(Utils.getColorStateListDefaultColor(mContext,
|
||||
R.color.batterymeter_plus_color));
|
||||
mPlusPoints = loadPoints(res, R.array.batterymeter_plus_points);
|
||||
|
||||
mPowersavePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
mPowersavePaint.setColor(mPlusPaint.getColor());
|
||||
mPowersavePaint.setStyle(Style.STROKE);
|
||||
mPowersavePaint.setStrokeWidth(context.getResources()
|
||||
.getDimensionPixelSize(R.dimen.battery_powersave_outline_thickness));
|
||||
|
||||
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 setShowPercent(boolean show) {
|
||||
mShowPercent = show;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setCharging(boolean val) {
|
||||
mCharging = val;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public boolean getCharging() {
|
||||
return mCharging;
|
||||
}
|
||||
|
||||
public void setBatteryLevel(int val) {
|
||||
mLevel = val;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public int getBatteryLevel() {
|
||||
return mLevel;
|
||||
}
|
||||
|
||||
public void setPowerSave(boolean val) {
|
||||
mPowerSaveEnabled = val;
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public boolean getPowerSave() {
|
||||
return mPowerSaveEnabled;
|
||||
}
|
||||
|
||||
protected void setPowerSaveAsColorError(boolean asError) {
|
||||
mPowerSaveAsColorError = asError;
|
||||
}
|
||||
|
||||
// an approximation of View.postInvalidate()
|
||||
protected void postInvalidate() {
|
||||
unscheduleSelf(this::invalidateSelf);
|
||||
scheduleSelf(this::invalidateSelf, 0);
|
||||
}
|
||||
|
||||
private static float[] loadPoints(Resources res, int pointArrayRes) {
|
||||
final int[] pts = res.getIntArray(pointArrayRes);
|
||||
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);
|
||||
updateSize();
|
||||
}
|
||||
|
||||
private void updateSize() {
|
||||
final Rect bounds = getBounds();
|
||||
|
||||
mHeight = (bounds.bottom - mPadding.bottom) - (bounds.top + mPadding.top);
|
||||
mWidth = (bounds.right - mPadding.right) - (bounds.left + mPadding.left);
|
||||
mWarningTextPaint.setTextSize(mHeight * 0.75f);
|
||||
mWarningTextHeight = -mWarningTextPaint.getFontMetrics().ascent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getPadding(Rect padding) {
|
||||
if (mPadding.left == 0
|
||||
&& mPadding.top == 0
|
||||
&& mPadding.right == 0
|
||||
&& mPadding.bottom == 0) {
|
||||
return super.getPadding(padding);
|
||||
}
|
||||
|
||||
padding.set(mPadding);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setPadding(int left, int top, int right, int bottom) {
|
||||
mPadding.left = left;
|
||||
mPadding.top = top;
|
||||
mPadding.right = right;
|
||||
mPadding.bottom = bottom;
|
||||
|
||||
updateSize();
|
||||
}
|
||||
|
||||
private int getColorForLevel(int percent) {
|
||||
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 setColors(int fillColor, int backgroundColor) {
|
||||
mIconTint = fillColor;
|
||||
mFramePaint.setColor(backgroundColor);
|
||||
mBoltPaint.setColor(fillColor);
|
||||
mChargeColor = fillColor;
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
protected int batteryColorForLevel(int level) {
|
||||
return (mCharging || (mPowerSaveEnabled && mPowerSaveAsColorError))
|
||||
? mChargeColor
|
||||
: getColorForLevel(level);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas c) {
|
||||
final int level = mLevel;
|
||||
final Rect bounds = getBounds();
|
||||
|
||||
if (level == -1) return;
|
||||
|
||||
float drawFrac = (float) level / 100f;
|
||||
final int height = mHeight;
|
||||
final int width = (int) (getAspectRatio() * mHeight);
|
||||
final int px = (mWidth - width) / 2;
|
||||
final int buttonHeight = Math.round(height * mButtonHeightFraction);
|
||||
final int left = mPadding.left + bounds.left;
|
||||
final int top = bounds.bottom - mPadding.bottom - height;
|
||||
|
||||
mFrame.set(left, top, width + left, height + top);
|
||||
mFrame.offset(px, 0);
|
||||
|
||||
// button-frame: area above the battery body
|
||||
mButtonFrame.set(
|
||||
mFrame.left + Math.round(width * 0.28f),
|
||||
mFrame.top,
|
||||
mFrame.right - Math.round(width * 0.28f),
|
||||
mFrame.top + buttonHeight);
|
||||
|
||||
// frame: battery body area
|
||||
mFrame.top += buttonHeight;
|
||||
|
||||
// set the battery charging color
|
||||
mBatteryPaint.setColor(batteryColorForLevel(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();
|
||||
mOutlinePath.reset();
|
||||
final float radius = getRadiusRatio() * (mFrame.height() + buttonHeight);
|
||||
mShapePath.setFillType(FillType.WINDING);
|
||||
mShapePath.addRoundRect(mFrame, radius, radius, Direction.CW);
|
||||
mShapePath.addRect(mButtonFrame, Direction.CW);
|
||||
mOutlinePath.addRoundRect(mFrame, radius, radius, Direction.CW);
|
||||
Path p = new Path();
|
||||
p.addRect(mButtonFrame, Direction.CW);
|
||||
mOutlinePath.op(p, Op.XOR);
|
||||
|
||||
if (mCharging) {
|
||||
// define the bolt shape
|
||||
// Shift right by 1px for maximal bolt-goodness
|
||||
final float bl = mFrame.left + mFrame.width() / 4f + 1;
|
||||
final float bt = mFrame.top + mFrame.height() / 6f;
|
||||
final float br = mFrame.right - mFrame.width() / 4f + 1;
|
||||
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());
|
||||
}
|
||||
|
||||
// Always cut out of the whole shape, and sometimes filled colorError
|
||||
mShapePath.op(mPlusPath, Path.Op.DIFFERENCE);
|
||||
if (mPowerSaveAsColorError) {
|
||||
c.drawPath(mPlusPath, mPlusPaint);
|
||||
}
|
||||
}
|
||||
|
||||
// compute percentage text
|
||||
boolean pctOpaque = false;
|
||||
float pctX = 0, pctY = 0;
|
||||
String pctText = null;
|
||||
if (!mCharging && !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 + left;
|
||||
pctY = (mHeight + mTextHeight) * 0.47f + top;
|
||||
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;
|
||||
c.save();
|
||||
c.clipRect(mFrame);
|
||||
c.drawPath(mShapePath, mBatteryPaint);
|
||||
c.restore();
|
||||
|
||||
if (!mCharging && !mPowerSaveEnabled) {
|
||||
if (level <= mCriticalLevel) {
|
||||
// draw the warning text
|
||||
final float x = mWidth * 0.5f + left;
|
||||
final float y = (mHeight + mWarningTextHeight) * 0.48f + top;
|
||||
c.drawText(mWarningString, x, y, mWarningTextPaint);
|
||||
} else if (pctOpaque) {
|
||||
// draw the percentage text
|
||||
c.drawText(pctText, pctX, pctY, mTextPaint);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the powersave outline last
|
||||
if (!mCharging && mPowerSaveEnabled && mPowerSaveAsColorError) {
|
||||
c.drawPath(mOutlinePath, mPowersavePaint);
|
||||
}
|
||||
}
|
||||
|
||||
// Some stuff required by Drawable.
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter) {
|
||||
mFramePaint.setColorFilter(colorFilter);
|
||||
mBatteryPaint.setColorFilter(colorFilter);
|
||||
mWarningTextPaint.setColorFilter(colorFilter);
|
||||
mBoltPaint.setColorFilter(colorFilter);
|
||||
mPlusPaint.setColorFilter(colorFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int getCriticalLevel() {
|
||||
return mCriticalLevel;
|
||||
}
|
||||
|
||||
protected float getAspectRatio() {
|
||||
return ASPECT_RATIO;
|
||||
}
|
||||
|
||||
protected float getRadiusRatio() {
|
||||
return RADIUS_RATIO;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.settingslib.graph;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.view.Gravity;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.android.settingslib.R;
|
||||
import com.android.settingslib.Utils;
|
||||
|
||||
/**
|
||||
* LayerDrawable contains the bluetooth device icon and battery gauge icon
|
||||
*/
|
||||
public class BluetoothDeviceLayerDrawable extends LayerDrawable {
|
||||
|
||||
private BluetoothDeviceLayerDrawableState mState;
|
||||
|
||||
private BluetoothDeviceLayerDrawable(@NonNull Drawable[] layers) {
|
||||
super(layers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the {@link LayerDrawable} that contains bluetooth device icon and battery icon.
|
||||
* This is a horizontal layout drawable while bluetooth icon at start and battery icon at end.
|
||||
*
|
||||
* @param context used to get the spec for icon
|
||||
* @param resId represents the bluetooth device drawable
|
||||
* @param batteryLevel the battery level for bluetooth device
|
||||
*/
|
||||
public static BluetoothDeviceLayerDrawable createLayerDrawable(Context context, int resId,
|
||||
int batteryLevel) {
|
||||
return createLayerDrawable(context, resId, batteryLevel, 1 /*iconScale*/);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the {@link LayerDrawable} that contains bluetooth device icon and battery icon.
|
||||
* This is a horizontal layout drawable while bluetooth icon at start and battery icon at end.
|
||||
*
|
||||
* @param context used to get the spec for icon
|
||||
* @param resId represents the bluetooth device drawable
|
||||
* @param batteryLevel the battery level for bluetooth device
|
||||
* @param iconScale the ratio of height between battery icon and bluetooth icon
|
||||
*/
|
||||
public static BluetoothDeviceLayerDrawable createLayerDrawable(Context context, int resId,
|
||||
int batteryLevel, float iconScale) {
|
||||
final Drawable deviceDrawable = context.getDrawable(resId);
|
||||
|
||||
final BatteryMeterDrawable batteryDrawable = new BatteryMeterDrawable(context,
|
||||
context.getColor(R.color.meter_background_color), batteryLevel);
|
||||
final int pad = context.getResources().getDimensionPixelSize(R.dimen.bt_battery_padding);
|
||||
batteryDrawable.setPadding(pad, pad, pad, pad);
|
||||
|
||||
final BluetoothDeviceLayerDrawable drawable = new BluetoothDeviceLayerDrawable(
|
||||
new Drawable[]{deviceDrawable, batteryDrawable});
|
||||
// Set the bluetooth icon at the left
|
||||
drawable.setLayerGravity(0 /* index of deviceDrawable */, Gravity.START);
|
||||
// Set battery icon to the right of the bluetooth icon
|
||||
drawable.setLayerInsetStart(1 /* index of batteryDrawable */,
|
||||
deviceDrawable.getIntrinsicWidth());
|
||||
drawable.setLayerInsetTop(1 /* index of batteryDrawable */,
|
||||
(int) (deviceDrawable.getIntrinsicHeight() * (1 - iconScale)));
|
||||
|
||||
drawable.setConstantState(context, resId, batteryLevel, iconScale);
|
||||
|
||||
return drawable;
|
||||
}
|
||||
|
||||
public void setConstantState(Context context, int resId, int batteryLevel, float iconScale) {
|
||||
mState = new BluetoothDeviceLayerDrawableState(context, resId, batteryLevel, iconScale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConstantState getConstantState() {
|
||||
return mState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Battery gauge icon with new spec.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static class BatteryMeterDrawable extends BatteryMeterDrawableBase {
|
||||
private final float mAspectRatio;
|
||||
@VisibleForTesting
|
||||
int mFrameColor;
|
||||
|
||||
public BatteryMeterDrawable(Context context, int frameColor, int batteryLevel) {
|
||||
super(context, frameColor);
|
||||
final Resources resources = context.getResources();
|
||||
mButtonHeightFraction = resources.getFraction(
|
||||
R.fraction.bt_battery_button_height_fraction, 1, 1);
|
||||
mAspectRatio = resources.getFraction(R.fraction.bt_battery_ratio_fraction, 1, 1);
|
||||
|
||||
final int tintColor = Utils.getColorAttrDefaultColor(context,
|
||||
android.R.attr.colorControlNormal);
|
||||
setColorFilter(new PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN));
|
||||
setBatteryLevel(batteryLevel);
|
||||
mFrameColor = frameColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float getAspectRatio() {
|
||||
return mAspectRatio;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float getRadiusRatio() {
|
||||
// Remove the round edge
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ConstantState} to restore the {@link BluetoothDeviceLayerDrawable}
|
||||
*/
|
||||
private static class BluetoothDeviceLayerDrawableState extends ConstantState {
|
||||
Context context;
|
||||
int resId;
|
||||
int batteryLevel;
|
||||
float iconScale;
|
||||
|
||||
public BluetoothDeviceLayerDrawableState(Context context, int resId,
|
||||
int batteryLevel, float iconScale) {
|
||||
this.context = context;
|
||||
this.resId = resId;
|
||||
this.batteryLevel = batteryLevel;
|
||||
this.iconScale = iconScale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable newDrawable() {
|
||||
return createLayerDrawable(context, resId, batteryLevel, iconScale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChangingConfigurations() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.settingslib.graph;
|
||||
|
||||
import static com.android.settingslib.flags.Flags.newStatusBarIcons;
|
||||
|
||||
import android.animation.ArgbEvaluator;
|
||||
import android.annotation.IntRange;
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Path.Direction;
|
||||
import android.graphics.Path.FillType;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.DrawableWrapper;
|
||||
import android.os.Handler;
|
||||
import android.telephony.CellSignalStrength;
|
||||
import android.util.LayoutDirection;
|
||||
import android.util.PathParser;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.settingslib.R;
|
||||
import com.android.settingslib.Utils;
|
||||
|
||||
/**
|
||||
* Drawable displaying a mobile cell signal indicator.
|
||||
*/
|
||||
public class SignalDrawable extends DrawableWrapper {
|
||||
|
||||
private static final String TAG = "SignalDrawable";
|
||||
|
||||
private static final int NUM_DOTS = 3;
|
||||
|
||||
private static final float VIEWPORT = 24f;
|
||||
private static final float PAD = 2f / VIEWPORT;
|
||||
|
||||
private static final float DOT_SIZE = 3f / VIEWPORT;
|
||||
private static final float DOT_PADDING = 1.5f / VIEWPORT;
|
||||
|
||||
// All of these are masks to push all of the drawable state into one int for easy callbacks
|
||||
// and flow through sysui.
|
||||
private static final int LEVEL_MASK = 0xff;
|
||||
private static final int NUM_LEVEL_SHIFT = 8;
|
||||
private static final int NUM_LEVEL_MASK = 0xff << NUM_LEVEL_SHIFT;
|
||||
private static final int STATE_SHIFT = 16;
|
||||
private static final int STATE_MASK = 0xff << STATE_SHIFT;
|
||||
private static final int STATE_CUT = 2;
|
||||
private static final int STATE_CARRIER_CHANGE = 3;
|
||||
|
||||
private static final long DOT_DELAY = 1000;
|
||||
|
||||
// Check the config for which icon we want to use
|
||||
private static final int ICON_RES = SignalDrawable.getIconRes();
|
||||
|
||||
private final Paint mForegroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint mTransparentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final int mDarkModeFillColor;
|
||||
private final int mLightModeFillColor;
|
||||
private final Path mCutoutPath = new Path();
|
||||
private final Path mForegroundPath = new Path();
|
||||
private final Path mAttributionPath = new Path();
|
||||
private final Matrix mAttributionScaleMatrix = new Matrix();
|
||||
private final Path mScaledAttributionPath = new Path();
|
||||
private final Handler mHandler;
|
||||
private final float mCutoutWidthFraction;
|
||||
private final float mCutoutHeightFraction;
|
||||
private float mDarkIntensity = -1;
|
||||
private final int mIntrinsicSize;
|
||||
private boolean mAnimating;
|
||||
private int mCurrentDot;
|
||||
|
||||
public SignalDrawable(Context context) {
|
||||
super(context.getDrawable(ICON_RES));
|
||||
final String attributionPathString = context.getString(
|
||||
com.android.internal.R.string.config_signalAttributionPath);
|
||||
mAttributionPath.set(PathParser.createPathFromPathData(attributionPathString));
|
||||
updateScaledAttributionPath();
|
||||
mCutoutWidthFraction = context.getResources().getFloat(
|
||||
com.android.internal.R.dimen.config_signalCutoutWidthFraction);
|
||||
mCutoutHeightFraction = context.getResources().getFloat(
|
||||
com.android.internal.R.dimen.config_signalCutoutHeightFraction);
|
||||
mDarkModeFillColor = Utils.getColorStateListDefaultColor(context,
|
||||
R.color.dark_mode_icon_color_single_tone);
|
||||
mLightModeFillColor = Utils.getColorStateListDefaultColor(context,
|
||||
R.color.light_mode_icon_color_single_tone);
|
||||
mIntrinsicSize = context.getResources().getDimensionPixelSize(R.dimen.signal_icon_size);
|
||||
mTransparentPaint.setColor(context.getColor(android.R.color.transparent));
|
||||
mTransparentPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
|
||||
mHandler = new Handler();
|
||||
setDarkIntensity(0);
|
||||
}
|
||||
|
||||
private void updateScaledAttributionPath() {
|
||||
if (getBounds().isEmpty()) {
|
||||
mAttributionScaleMatrix.setScale(1f, 1f);
|
||||
} else {
|
||||
mAttributionScaleMatrix.setScale(
|
||||
getBounds().width() / VIEWPORT, getBounds().height() / VIEWPORT);
|
||||
}
|
||||
mAttributionPath.transform(mAttributionScaleMatrix, mScaledAttributionPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth() {
|
||||
return mIntrinsicSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight() {
|
||||
return mIntrinsicSize;
|
||||
}
|
||||
|
||||
private void updateAnimation() {
|
||||
boolean shouldAnimate = isInState(STATE_CARRIER_CHANGE) && isVisible();
|
||||
if (shouldAnimate == mAnimating) return;
|
||||
mAnimating = shouldAnimate;
|
||||
if (shouldAnimate) {
|
||||
mChangeDot.run();
|
||||
} else {
|
||||
mHandler.removeCallbacks(mChangeDot);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onLevelChange(int packedState) {
|
||||
super.onLevelChange(unpackLevel(packedState));
|
||||
updateAnimation();
|
||||
setTintList(ColorStateList.valueOf(mForegroundPaint.getColor()));
|
||||
invalidateSelf();
|
||||
return true;
|
||||
}
|
||||
|
||||
private int unpackLevel(int packedState) {
|
||||
int numBins = (packedState & NUM_LEVEL_MASK) >> NUM_LEVEL_SHIFT;
|
||||
int cutOutOffset = 0;
|
||||
int levelOffset = numBins == (CellSignalStrength.getNumSignalStrengthLevels() + 1) ? 10 : 0;
|
||||
int level = (packedState & LEVEL_MASK);
|
||||
|
||||
if (newStatusBarIcons()) {
|
||||
if (isInState(STATE_CUT)) {
|
||||
cutOutOffset = 20;
|
||||
}
|
||||
}
|
||||
|
||||
return level + levelOffset + cutOutOffset;
|
||||
}
|
||||
|
||||
public void setDarkIntensity(float darkIntensity) {
|
||||
if (darkIntensity == mDarkIntensity) {
|
||||
return;
|
||||
}
|
||||
setTintList(ColorStateList.valueOf(getFillColor(darkIntensity)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTintList(ColorStateList tint) {
|
||||
super.setTintList(tint);
|
||||
int colorForeground = mForegroundPaint.getColor();
|
||||
mForegroundPaint.setColor(tint.getDefaultColor());
|
||||
if (colorForeground != mForegroundPaint.getColor()) invalidateSelf();
|
||||
}
|
||||
|
||||
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
|
||||
protected void onBoundsChange(Rect bounds) {
|
||||
super.onBoundsChange(bounds);
|
||||
updateScaledAttributionPath();
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
canvas.saveLayer(null, null);
|
||||
final float width = getBounds().width();
|
||||
final float height = getBounds().height();
|
||||
|
||||
boolean isRtl = getLayoutDirection() == LayoutDirection.RTL;
|
||||
if (isRtl) {
|
||||
canvas.save();
|
||||
// Mirror the drawable
|
||||
canvas.translate(width, 0);
|
||||
canvas.scale(-1.0f, 1.0f);
|
||||
}
|
||||
super.draw(canvas);
|
||||
mCutoutPath.reset();
|
||||
mCutoutPath.setFillType(FillType.WINDING);
|
||||
|
||||
final float padding = Math.round(PAD * width);
|
||||
|
||||
if (isInState(STATE_CARRIER_CHANGE)) {
|
||||
float dotSize = (DOT_SIZE * height);
|
||||
float dotPadding = (DOT_PADDING * height);
|
||||
float dotSpacing = dotPadding + dotSize;
|
||||
float x = width - padding - dotSize;
|
||||
float y = height - padding - dotSize;
|
||||
mForegroundPath.reset();
|
||||
drawDotAndPadding(x, y, dotPadding, dotSize, 2);
|
||||
drawDotAndPadding(x - dotSpacing, y, dotPadding, dotSize, 1);
|
||||
drawDotAndPadding(x - dotSpacing * 2, y, dotPadding, dotSize, 0);
|
||||
canvas.drawPath(mCutoutPath, mTransparentPaint);
|
||||
canvas.drawPath(mForegroundPath, mForegroundPaint);
|
||||
} else if (!newStatusBarIcons() && isInState(STATE_CUT)) {
|
||||
float cutX = (mCutoutWidthFraction * width / VIEWPORT);
|
||||
float cutY = (mCutoutHeightFraction * height / VIEWPORT);
|
||||
mCutoutPath.moveTo(width, height);
|
||||
mCutoutPath.rLineTo(-cutX, 0);
|
||||
mCutoutPath.rLineTo(0, -cutY);
|
||||
mCutoutPath.rLineTo(cutX, 0);
|
||||
mCutoutPath.rLineTo(0, cutY);
|
||||
canvas.drawPath(mCutoutPath, mTransparentPaint);
|
||||
canvas.drawPath(mScaledAttributionPath, mForegroundPaint);
|
||||
}
|
||||
if (isRtl) {
|
||||
canvas.restore();
|
||||
}
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
private void drawDotAndPadding(float x, float y,
|
||||
float dotPadding, float dotSize, int i) {
|
||||
if (i == mCurrentDot) {
|
||||
// Draw dot
|
||||
mForegroundPath.addRect(x, y, x + dotSize, y + dotSize, Direction.CW);
|
||||
// Draw dot padding
|
||||
mCutoutPath.addRect(x - dotPadding, y - dotPadding, x + dotSize + dotPadding,
|
||||
y + dotSize + dotPadding, Direction.CW);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
|
||||
super.setAlpha(alpha);
|
||||
mForegroundPaint.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter) {
|
||||
super.setColorFilter(colorFilter);
|
||||
mForegroundPaint.setColorFilter(colorFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setVisible(boolean visible, boolean restart) {
|
||||
boolean changed = super.setVisible(visible, restart);
|
||||
updateAnimation();
|
||||
return changed;
|
||||
}
|
||||
|
||||
private final Runnable mChangeDot = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (++mCurrentDot == NUM_DOTS) {
|
||||
mCurrentDot = 0;
|
||||
}
|
||||
invalidateSelf();
|
||||
mHandler.postDelayed(mChangeDot, DOT_DELAY);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether this drawable is in the specified state.
|
||||
*
|
||||
* @param state must be one of {@link #STATE_CARRIER_CHANGE} or {@link #STATE_CUT}
|
||||
*/
|
||||
private boolean isInState(int state) {
|
||||
return getState(getLevel()) == state;
|
||||
}
|
||||
|
||||
public static int getState(int fullState) {
|
||||
return (fullState & STATE_MASK) >> STATE_SHIFT;
|
||||
}
|
||||
|
||||
public static int getState(int level, int numLevels, boolean cutOut) {
|
||||
return ((cutOut ? STATE_CUT : 0) << STATE_SHIFT)
|
||||
| (numLevels << NUM_LEVEL_SHIFT)
|
||||
| level;
|
||||
}
|
||||
|
||||
/** Returns the state representing empty mobile signal with the given number of levels. */
|
||||
public static int getEmptyState(int numLevels) {
|
||||
return getState(0, numLevels, true);
|
||||
}
|
||||
|
||||
/** Returns the state representing carrier change with the given number of levels. */
|
||||
public static int getCarrierChangeState(int numLevels) {
|
||||
return (STATE_CARRIER_CHANGE << STATE_SHIFT) | (numLevels << NUM_LEVEL_SHIFT);
|
||||
}
|
||||
|
||||
private static int getIconRes() {
|
||||
if (newStatusBarIcons()) {
|
||||
return R.drawable.ic_mobile_level_list;
|
||||
} else {
|
||||
return com.android.internal.R.drawable.ic_signal_cellular;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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.settingslib.graph
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.BlendMode
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.PathParser
|
||||
import android.util.TypedValue
|
||||
|
||||
import com.android.settingslib.R
|
||||
import com.android.settingslib.Utils
|
||||
|
||||
/**
|
||||
* A battery meter drawable that respects paths configured in
|
||||
* frameworks/base/core/res/res/values/config.xml to allow for an easily overrideable battery icon
|
||||
*/
|
||||
open class ThemedBatteryDrawable(private val context: Context, frameColor: Int) : Drawable() {
|
||||
|
||||
// Need to load:
|
||||
// 1. perimeter shape
|
||||
// 2. fill mask (if smaller than perimeter, this would create a fill that
|
||||
// doesn't touch the walls
|
||||
private val perimeterPath = Path()
|
||||
private val scaledPerimeter = Path()
|
||||
private val errorPerimeterPath = Path()
|
||||
private val scaledErrorPerimeter = Path()
|
||||
// Fill will cover the whole bounding rect of the fillMask, and be masked by the path
|
||||
private val fillMask = Path()
|
||||
private val scaledFill = Path()
|
||||
// Based off of the mask, the fill will interpolate across this space
|
||||
private val fillRect = RectF()
|
||||
// Top of this rect changes based on level, 100% == fillRect
|
||||
private val levelRect = RectF()
|
||||
private val levelPath = Path()
|
||||
// Updates the transform of the paths when our bounds change
|
||||
private val scaleMatrix = Matrix()
|
||||
private val padding = Rect()
|
||||
// The net result of fill + perimeter paths
|
||||
private val unifiedPath = Path()
|
||||
|
||||
// Bolt path (used while charging)
|
||||
private val boltPath = Path()
|
||||
private val scaledBolt = Path()
|
||||
|
||||
// Plus sign (used for power save mode)
|
||||
private val plusPath = Path()
|
||||
private val scaledPlus = Path()
|
||||
|
||||
private var intrinsicHeight: Int
|
||||
private var intrinsicWidth: Int
|
||||
|
||||
// To implement hysteresis, keep track of the need to invert the interior icon of the battery
|
||||
private var invertFillIcon = false
|
||||
|
||||
// Colors can be configured based on battery level (see res/values/arrays.xml)
|
||||
private var colorLevels: IntArray
|
||||
|
||||
private var fillColor: Int = Color.MAGENTA
|
||||
private var backgroundColor: Int = Color.MAGENTA
|
||||
// updated whenever level changes
|
||||
private var levelColor: Int = Color.MAGENTA
|
||||
|
||||
// Dual tone implies that battery level is a clipped overlay over top of the whole shape
|
||||
private var dualTone = false
|
||||
|
||||
private var batteryLevel = 0
|
||||
|
||||
private val invalidateRunnable: () -> Unit = {
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
open var criticalLevel: Int = context.resources.getInteger(
|
||||
com.android.internal.R.integer.config_criticalBatteryWarningLevel)
|
||||
|
||||
var charging = false
|
||||
set(value) {
|
||||
field = value
|
||||
postInvalidate()
|
||||
}
|
||||
|
||||
var powerSaveEnabled = false
|
||||
set(value) {
|
||||
field = value
|
||||
postInvalidate()
|
||||
}
|
||||
|
||||
private val fillColorStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
|
||||
p.color = frameColor
|
||||
p.alpha = 255
|
||||
p.isDither = true
|
||||
p.strokeWidth = 5f
|
||||
p.style = Paint.Style.STROKE
|
||||
p.blendMode = BlendMode.SRC
|
||||
p.strokeMiter = 5f
|
||||
p.strokeJoin = Paint.Join.ROUND
|
||||
}
|
||||
|
||||
private val fillColorStrokeProtection = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
|
||||
p.isDither = true
|
||||
p.strokeWidth = 5f
|
||||
p.style = Paint.Style.STROKE
|
||||
p.blendMode = BlendMode.CLEAR
|
||||
p.strokeMiter = 5f
|
||||
p.strokeJoin = Paint.Join.ROUND
|
||||
}
|
||||
|
||||
private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
|
||||
p.color = frameColor
|
||||
p.alpha = 255
|
||||
p.isDither = true
|
||||
p.strokeWidth = 0f
|
||||
p.style = Paint.Style.FILL_AND_STROKE
|
||||
}
|
||||
|
||||
private val errorPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
|
||||
p.color = Utils.getColorStateListDefaultColor(context, R.color.batterymeter_saver_color)
|
||||
p.alpha = 255
|
||||
p.isDither = true
|
||||
p.strokeWidth = 0f
|
||||
p.style = Paint.Style.FILL_AND_STROKE
|
||||
p.blendMode = BlendMode.SRC
|
||||
}
|
||||
|
||||
// Only used if dualTone is set to true
|
||||
private val dualToneBackgroundFill = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
|
||||
p.color = frameColor
|
||||
p.alpha = 85 // ~0.3 alpha by default
|
||||
p.isDither = true
|
||||
p.strokeWidth = 0f
|
||||
p.style = Paint.Style.FILL_AND_STROKE
|
||||
}
|
||||
|
||||
init {
|
||||
val density = context.resources.displayMetrics.density
|
||||
intrinsicHeight = (Companion.HEIGHT * density).toInt()
|
||||
intrinsicWidth = (Companion.WIDTH * density).toInt()
|
||||
|
||||
val res = context.resources
|
||||
val levels = res.obtainTypedArray(R.array.batterymeter_color_levels)
|
||||
val colors = res.obtainTypedArray(R.array.batterymeter_color_values)
|
||||
val N = levels.length()
|
||||
colorLevels = IntArray(2 * N)
|
||||
for (i in 0 until N) {
|
||||
colorLevels[2 * i] = levels.getInt(i, 0)
|
||||
if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) {
|
||||
colorLevels[2 * i + 1] = Utils.getColorAttrDefaultColor(context,
|
||||
colors.getThemeAttributeId(i, 0))
|
||||
} else {
|
||||
colorLevels[2 * i + 1] = colors.getColor(i, 0)
|
||||
}
|
||||
}
|
||||
levels.recycle()
|
||||
colors.recycle()
|
||||
|
||||
loadPaths()
|
||||
}
|
||||
|
||||
override fun draw(c: Canvas) {
|
||||
c.saveLayer(null, null)
|
||||
unifiedPath.reset()
|
||||
levelPath.reset()
|
||||
levelRect.set(fillRect)
|
||||
val fillFraction = batteryLevel / 100f
|
||||
val fillTop =
|
||||
if (batteryLevel >= 95)
|
||||
fillRect.top
|
||||
else
|
||||
fillRect.top + (fillRect.height() * (1 - fillFraction))
|
||||
|
||||
levelRect.top = Math.floor(fillTop.toDouble()).toFloat()
|
||||
levelPath.addRect(levelRect, Path.Direction.CCW)
|
||||
|
||||
// The perimeter should never change
|
||||
unifiedPath.addPath(scaledPerimeter)
|
||||
// If drawing dual tone, the level is used only to clip the whole drawable path
|
||||
if (!dualTone) {
|
||||
unifiedPath.op(levelPath, Path.Op.UNION)
|
||||
}
|
||||
|
||||
fillPaint.color = levelColor
|
||||
|
||||
// Deal with unifiedPath clipping before it draws
|
||||
if (charging) {
|
||||
// Clip out the bolt shape
|
||||
unifiedPath.op(scaledBolt, Path.Op.DIFFERENCE)
|
||||
if (!invertFillIcon) {
|
||||
c.drawPath(scaledBolt, fillPaint)
|
||||
}
|
||||
}
|
||||
|
||||
if (dualTone) {
|
||||
// Dual tone means we draw the shape again, clipped to the charge level
|
||||
c.drawPath(unifiedPath, dualToneBackgroundFill)
|
||||
c.save()
|
||||
c.clipRect(0f,
|
||||
bounds.bottom - bounds.height() * fillFraction,
|
||||
bounds.right.toFloat(),
|
||||
bounds.bottom.toFloat())
|
||||
c.drawPath(unifiedPath, fillPaint)
|
||||
c.restore()
|
||||
} else {
|
||||
// Non dual-tone means we draw the perimeter (with the level fill), and potentially
|
||||
// draw the fill again with a critical color
|
||||
fillPaint.color = fillColor
|
||||
c.drawPath(unifiedPath, fillPaint)
|
||||
fillPaint.color = levelColor
|
||||
|
||||
// Show colorError below this level
|
||||
if (batteryLevel <= Companion.CRITICAL_LEVEL && !charging) {
|
||||
c.save()
|
||||
c.clipPath(scaledFill)
|
||||
c.drawPath(levelPath, fillPaint)
|
||||
c.restore()
|
||||
}
|
||||
}
|
||||
|
||||
if (charging) {
|
||||
c.clipOutPath(scaledBolt)
|
||||
if (invertFillIcon) {
|
||||
c.drawPath(scaledBolt, fillColorStrokePaint)
|
||||
} else {
|
||||
c.drawPath(scaledBolt, fillColorStrokeProtection)
|
||||
}
|
||||
} else if (powerSaveEnabled) {
|
||||
// If power save is enabled draw the level path with colorError
|
||||
c.drawPath(levelPath, errorPaint)
|
||||
// And draw the plus sign on top of the fill
|
||||
fillPaint.color = fillColor
|
||||
c.drawPath(scaledPlus, fillPaint)
|
||||
}
|
||||
c.restore()
|
||||
}
|
||||
|
||||
private fun batteryColorForLevel(level: Int): Int {
|
||||
return when {
|
||||
charging || powerSaveEnabled -> fillColor
|
||||
else -> getColorForLevel(level)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getColorForLevel(level: Int): Int {
|
||||
var thresh: Int
|
||||
var color = 0
|
||||
var i = 0
|
||||
while (i < colorLevels.size) {
|
||||
thresh = colorLevels[i]
|
||||
color = colorLevels[i + 1]
|
||||
if (level <= thresh) {
|
||||
|
||||
// Respect tinting for "normal" level
|
||||
return if (i == colorLevels.size - 2) {
|
||||
fillColor
|
||||
} else {
|
||||
color
|
||||
}
|
||||
}
|
||||
i += 2
|
||||
}
|
||||
return color
|
||||
}
|
||||
|
||||
/**
|
||||
* Alpha is unused internally, and should be defined in the colors passed to {@link setColors}.
|
||||
* Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds
|
||||
* defining the minimum background fill alpha. This is because fill + background must be equal
|
||||
* to the net alpha passed in here.
|
||||
*/
|
||||
override fun setAlpha(alpha: Int) {
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
fillPaint.colorFilter = colorFilter
|
||||
fillColorStrokePaint.colorFilter = colorFilter
|
||||
dualToneBackgroundFill.colorFilter = colorFilter
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated, but required by Drawable
|
||||
*/
|
||||
override fun getOpacity(): Int {
|
||||
return PixelFormat.OPAQUE
|
||||
}
|
||||
|
||||
override fun getIntrinsicHeight(): Int {
|
||||
return intrinsicHeight
|
||||
}
|
||||
|
||||
override fun getIntrinsicWidth(): Int {
|
||||
return intrinsicWidth
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the fill level
|
||||
*/
|
||||
public open fun setBatteryLevel(l: Int) {
|
||||
invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon
|
||||
batteryLevel = l
|
||||
levelColor = batteryColorForLevel(batteryLevel)
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
public fun getBatteryLevel(): Int {
|
||||
return batteryLevel
|
||||
}
|
||||
|
||||
override fun onBoundsChange(bounds: Rect) {
|
||||
super.onBoundsChange(bounds)
|
||||
updateSize()
|
||||
}
|
||||
|
||||
fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
|
||||
padding.left = left
|
||||
padding.top = top
|
||||
padding.right = right
|
||||
padding.bottom = bottom
|
||||
|
||||
updateSize()
|
||||
}
|
||||
|
||||
fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) {
|
||||
fillColor = if (dualTone) fgColor else singleToneColor
|
||||
|
||||
fillPaint.color = fillColor
|
||||
fillColorStrokePaint.color = fillColor
|
||||
|
||||
backgroundColor = bgColor
|
||||
dualToneBackgroundFill.color = bgColor
|
||||
|
||||
// Also update the level color, since fillColor may have changed
|
||||
levelColor = batteryColorForLevel(batteryLevel)
|
||||
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
private fun postInvalidate() {
|
||||
unscheduleSelf(invalidateRunnable)
|
||||
scheduleSelf(invalidateRunnable, 0)
|
||||
}
|
||||
|
||||
private fun updateSize() {
|
||||
val b = bounds
|
||||
if (b.isEmpty) {
|
||||
scaleMatrix.setScale(1f, 1f)
|
||||
} else {
|
||||
scaleMatrix.setScale((b.right / WIDTH), (b.bottom / HEIGHT))
|
||||
}
|
||||
|
||||
perimeterPath.transform(scaleMatrix, scaledPerimeter)
|
||||
errorPerimeterPath.transform(scaleMatrix, scaledErrorPerimeter)
|
||||
fillMask.transform(scaleMatrix, scaledFill)
|
||||
scaledFill.computeBounds(fillRect, true)
|
||||
boltPath.transform(scaleMatrix, scaledBolt)
|
||||
plusPath.transform(scaleMatrix, scaledPlus)
|
||||
|
||||
// It is expected that this view only ever scale by the same factor in each dimension, so
|
||||
// just pick one to scale the strokeWidths
|
||||
val scaledStrokeWidth =
|
||||
Math.max(b.right / WIDTH * PROTECTION_STROKE_WIDTH, PROTECTION_MIN_STROKE_WIDTH)
|
||||
|
||||
fillColorStrokePaint.strokeWidth = scaledStrokeWidth
|
||||
fillColorStrokeProtection.strokeWidth = scaledStrokeWidth
|
||||
}
|
||||
|
||||
private fun loadPaths() {
|
||||
val pathString = context.resources.getString(
|
||||
com.android.internal.R.string.config_batterymeterPerimeterPath)
|
||||
perimeterPath.set(PathParser.createPathFromPathData(pathString))
|
||||
perimeterPath.computeBounds(RectF(), true)
|
||||
|
||||
val errorPathString = context.resources.getString(
|
||||
com.android.internal.R.string.config_batterymeterErrorPerimeterPath)
|
||||
errorPerimeterPath.set(PathParser.createPathFromPathData(errorPathString))
|
||||
errorPerimeterPath.computeBounds(RectF(), true)
|
||||
|
||||
val fillMaskString = context.resources.getString(
|
||||
com.android.internal.R.string.config_batterymeterFillMask)
|
||||
fillMask.set(PathParser.createPathFromPathData(fillMaskString))
|
||||
// Set the fill rect so we can calculate the fill properly
|
||||
fillMask.computeBounds(fillRect, true)
|
||||
|
||||
val boltPathString = context.resources.getString(
|
||||
com.android.internal.R.string.config_batterymeterBoltPath)
|
||||
boltPath.set(PathParser.createPathFromPathData(boltPathString))
|
||||
|
||||
val plusPathString = context.resources.getString(
|
||||
com.android.internal.R.string.config_batterymeterPowersavePath)
|
||||
plusPath.set(PathParser.createPathFromPathData(plusPathString))
|
||||
|
||||
dualTone = context.resources.getBoolean(
|
||||
com.android.internal.R.bool.config_batterymeterDualTone)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val WIDTH = 12f
|
||||
const val HEIGHT = 20f
|
||||
private const val CRITICAL_LEVEL = 20
|
||||
// On a 12x20 grid, how wide to make the fill protection stroke.
|
||||
// Scales when our size changes
|
||||
private const val PROTECTION_STROKE_WIDTH = 3f
|
||||
// Arbitrarily chosen for visibility at small sizes
|
||||
const val PROTECTION_MIN_STROKE_WIDTH = 6f
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user