/* * Copyright (C) 2013 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. */ // Modified 2014 AChep@xda <artemchep@gmail.com> package com.achep.acdisplay.ui.widgets.status; 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.Paint; import android.graphics.Path; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.graphics.Typeface; import android.os.BatteryManager; import android.support.annotation.NonNull; import android.util.AttributeSet; import android.view.View; import android.widget.TextView; import com.achep.acdisplay.R; import com.achep.base.utils.ResUtils; // TODO: Bring RTL support public class BatteryMeterView extends TextView { public static final String TAG = BatteryMeterView.class.getSimpleName(); public static final int EVENT_LEVEL = 1; public static final int EVENT_CHARGING = 2; public enum BatteryMeterMode { BATTERY_METER_ICON_PORTRAIT, } protected class BatteryTracker extends BroadcastReceiver { public static final int UNKNOWN_LEVEL = -1; // current battery status boolean present = true; boolean plugged; int plugType; int status; int level = UNKNOWN_LEVEL; @Override public void onReceive(Context context, Intent intent) { switch (intent.getAction()) { case Intent.ACTION_BATTERY_CHANGED: final boolean chargingOld = indicateCharging(); final int levelOld = level; // Get battery level level = 100 * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); present = intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true); plugType = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); plugged = plugType != 0; status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN); // Update view setText(String.format(mBatteryFormat, this.level)); setContentDescription(ResUtils.getString(context, R.string.accessibility_battery_level, level)); // Notify listener if (mOnBatteryChangedListener != null) { int event = 0; event |= chargingOld != indicateCharging() ? EVENT_CHARGING : 0; event |= levelOld != level ? EVENT_LEVEL : 0; if (event != 0) { mOnBatteryChangedListener.onBatteryChanged(BatteryMeterView.this, event); } } break; } } /** * @return {@code true} if device is charging, {@code false} otherwise. */ protected boolean indicateCharging() { return status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL && plugged; } } /** * Interface definition for a callback to be invoked * when battery status/level/other changed. * * @see #setOnBatteryChangedListener(OnBatteryChangedListener) */ public interface OnBatteryChangedListener { /** * Invoked when battery status/level/other changed. * * @param event bit-set of events: {@link #EVENT_LEVEL}, {@link #EVENT_CHARGING} or other. */ void onBatteryChanged(BatteryMeterView view, int event); } protected BatteryMeterMode mBatteryMeterMode; final int[] mColors; private String mBatteryFormat; private String mWarningString; private final int mChargeColor; private final int mBatteryHeight; private final int mBatteryWidth; private final int mBatteryPadding; private OnBatteryChangedListener mOnBatteryChangedListener; private boolean mAttached; private Context mContext; protected BatteryTracker mTracker = new BatteryTracker(); private BatteryMeterDrawable mBatteryDrawable; private final Object mLock = new Object(); private int mPaddingLeft; @Override public void onAttachedToWindow() { super.onAttachedToWindow(); mAttached = true; IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); Intent sticky = getContext().registerReceiver(mTracker, filter); // Pre-load the battery level if (sticky != null) { mTracker.onReceive(getContext(), sticky); } } @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); mAttached = false; getContext().unregisterReceiver(mTracker); } 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); mContext = context; TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.BatteryMeterView, defStyle, 0); mBatteryHeight = arr.getDimensionPixelSize(R.styleable.BatteryMeterView_battery_height, 0); mBatteryWidth = arr.getDimensionPixelSize(R.styleable.BatteryMeterView_battery_width, 0); mBatteryPadding = arr.getDimensionPixelSize(R.styleable.BatteryMeterView_battery_padding, 0); setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom()); arr.recycle(); final Resources res = context.getResources(); if (!isInEditMode()) { 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(); } else { mColors = new int[]{ 4, res.getColor(R.color.batterymeter_critical), 15, res.getColor(R.color.batterymeter_low), 100, res.getColor(R.color.batterymeter_full), }; } mChargeColor = getResources().getColor(R.color.batterymeter_charge_color); mBatteryFormat = getResources().getString(R.string.batterymeter_precise); mWarningString = context.getString(R.string.batterymeter_very_low_overlay_symbol); setMode(BatteryMeterMode.BATTERY_METER_ICON_PORTRAIT); mBatteryDrawable.onSizeChanged(mBatteryWidth, mBatteryHeight, 0, 0); setLayerType(View.LAYER_TYPE_SOFTWARE, null); } @Override public void setPadding(int left, int top, int right, int bottom) { // Apply additional padding to leave free space // for battery meter drawing. final int leftExtended = (left += mBatteryPadding - mPaddingLeft) + mBatteryWidth + mBatteryPadding; super.setPadding(leftExtended, top, right, bottom); mPaddingLeft = left; } @Override public int getPaddingLeft() { return mPaddingLeft; } private BatteryMeterDrawable createBatteryMeterDrawable(BatteryMeterMode mode) { Resources res = mContext.getResources(); return new NormalBatteryMeterDrawable(res); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int heightMin = mBatteryHeight + getTotalPaddingTop() + getTotalPaddingBottom(); int height = Math.max(getMeasuredHeight(), heightMin); setMeasuredDimension(getMeasuredWidth(), height); } public void setOnBatteryChangedListener(OnBatteryChangedListener listener) { mOnBatteryChangedListener = listener; } /** * @return battery charge level, from {@code [0...100]} */ public int getBatteryLevel() { return mTracker.level; } /** * @return {@code true} if charging, {@code false} otherwise */ public boolean getBatteryCharging() { return mTracker.indicateCharging(); } public int getColorForLevel(int percent) { int thresh, color; for (int i = 0; i < mColors.length; i += 2) { thresh = mColors[i]; color = mColors[i + 1]; if (percent <= thresh) { return color; } } throw new RuntimeException("Broken color levels!"); } public void setMode(BatteryMeterMode mode) { synchronized (mLock) { if (mBatteryMeterMode == mode) { return; } mBatteryMeterMode = mode; if (mBatteryDrawable != null) mBatteryDrawable.onDispose(); mBatteryDrawable = createBatteryMeterDrawable(mode); if (mBatteryMeterMode == BatteryMeterMode.BATTERY_METER_ICON_PORTRAIT) { NormalBatteryMeterDrawable drawable = (NormalBatteryMeterDrawable) mBatteryDrawable; drawable.loadBoltPoints(mContext.getResources()); } if (mAttached) { postInvalidate(); } } } @Override public void onDraw(@NonNull Canvas c) { super.onDraw(c); synchronized (mLock) { if (mBatteryDrawable != null) { mBatteryDrawable.onDraw(c, mTracker); } } } protected interface BatteryMeterDrawable { void onDraw(Canvas c, BatteryTracker tracker); void onSizeChanged(int w, int h, int oldw, int oldh); void onDispose(); } protected class NormalBatteryMeterDrawable implements BatteryMeterDrawable { public static final int FULL = 96; public static final int EMPTY = 4; public static final float SUBPIXEL = 0.4f; // inset rects for softer edges private boolean mDisposed; private Paint mFramePaint, mBatteryPaint, mWarningTextPaint, mBoltPaint; private int mButtonHeight; private float mWarningTextHeight; private final float[] mBoltPoints; private final Path mBoltPath = new Path(); private final RectF mFrame = new RectF(); private final RectF mButtonFrame = new RectF(); private final RectF mClipFrame = new RectF(); private final RectF mBoltFrame = new RectF(); public NormalBatteryMeterDrawable(Resources res) { super(); mDisposed = false; mFramePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mFramePaint.setColor(res.getColor(R.color.batterymeter_frame_color)); mFramePaint.setDither(true); mFramePaint.setStrokeWidth(0); mFramePaint.setStyle(Paint.Style.FILL_AND_STROKE); mFramePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP)); mBatteryPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBatteryPaint.setDither(true); mBatteryPaint.setStrokeWidth(0); mBatteryPaint.setStyle(Paint.Style.FILL_AND_STROKE); mWarningTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mWarningTextPaint.setColor(mColors[1]); Typeface font = Typeface.create("sans-serif", Typeface.BOLD); mWarningTextPaint.setTypeface(font); mWarningTextPaint.setTextAlign(Paint.Align.CENTER); mBoltPaint = new Paint(); mBoltPaint.setAntiAlias(true); mBoltPaint.setColor(res.getColor(R.color.batterymeter_bolt_color)); mBoltPoints = loadBoltPoints(res); } @Override public void onDraw(Canvas c, BatteryTracker tracker) { if (mDisposed) return; final int level = tracker.level; if (level == BatteryTracker.UNKNOWN_LEVEL) return; float drawFrac = (float) level / 100f; final int pt = getTotalPaddingTop() + (getHeight() - getTotalPaddingTop() - getTotalPaddingBottom() - mBatteryHeight) / 2; final int pl = getPaddingLeft(); int height = mBatteryHeight; int width = mBatteryWidth; mButtonHeight = (int) (height * 0.12f); mFrame.set(0, 0, width, height); mFrame.offset(pl, pt); mButtonFrame.set( mFrame.left + width * 0.25f, mFrame.top, mFrame.right - width * 0.25f, mFrame.top + mButtonHeight + 5 /*cover frame border of intersecting area*/); mButtonFrame.top += SUBPIXEL; mButtonFrame.left += SUBPIXEL; mButtonFrame.right -= SUBPIXEL; mFrame.top += mButtonHeight; mFrame.left += SUBPIXEL; mFrame.top += SUBPIXEL; mFrame.right -= SUBPIXEL; mFrame.bottom -= SUBPIXEL; // first, draw the battery shape c.drawRect(mFrame, mFramePaint); // fill 'er up final int color = tracker.plugged ? mChargeColor : getColorForLevel(level); mBatteryPaint.setColor(color); if (level >= FULL) { drawFrac = 1f; } else if (level <= EMPTY) { drawFrac = 0f; } c.drawRect(mButtonFrame, drawFrac == 1f ? mBatteryPaint : mFramePaint); mClipFrame.set(mFrame); mClipFrame.top += (mFrame.height() * (1f - drawFrac)); c.save(Canvas.CLIP_SAVE_FLAG); c.clipRect(mClipFrame); c.drawRect(mFrame, mBatteryPaint); c.restore(); if (tracker.indicateCharging()) { // draw the bolt final float bl = (int) (mFrame.left + mFrame.width() / 4.5f); final float bt = (int) (mFrame.top + mFrame.height() / 6f); final float br = (int) (mFrame.right - mFrame.width() / 7f); final float bb = (int) (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()); } c.drawPath(mBoltPath, mBoltPaint); } else if (level <= EMPTY) { final float x = pl + mBatteryWidth * 0.5f; final float y = pt + (mBatteryHeight + mWarningTextHeight) * 0.48f; c.drawText(mWarningString, x, y, mWarningTextPaint); } } @Override public void onDispose() { mDisposed = true; } @Override public void onSizeChanged(int w, int h, int oldw, int oldh) { mWarningTextPaint.setTextSize(h * 0.75f); mWarningTextHeight = -mWarningTextPaint.getFontMetrics().ascent; } private float[] loadBoltPoints(Resources res) { if (!isInEditMode()) { final int[] pts = res.getIntArray(getBoltPointsArrayResource()); 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; } else { return new float[]{0, 0, 1, 1}; } } protected int getBoltPointsArrayResource() { return R.array.batterymeter_bolt_points; } } }