package com.v7lin.android.widget;
import java.lang.reflect.Field;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.FontMetrics;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.ClipDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.ScaleDrawable;
import android.text.Layout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.widget.ProgressBar;
import com.v7lin.android.env.widget.CompatProgressBar;
import com.v7lin.android.env.widget.InternalTransfer;
/**
* 采用 Region.Op.XOR 方案会有 BUG,故而变更方案
*
* @author v7lin Email:v7lin@qq.com
*/
public class NumberProgressBar extends CompatProgressBar {
private static final int MAX_LEVEL = generateMaxLevel();
private static int generateMaxLevel() {
try {
Field field = ProgressBar.class.getField("MAX_LEVEL");
if (field != null) {
field.setAccessible(true);
int maxLevel = field.getInt(null);
return maxLevel;
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
return 10000;
}
private NumberHelper mNumberHelper;
public NumberProgressBar(Context context) {
this(context, null);
}
public NumberProgressBar(Context context, AttributeSet attrs) {
// this(context, attrs, com.android.internal.R.attr.progressBarStyle);
this(context, attrs, InternalTransfer.transferAttr(context, "progressBarStyle"));
}
public NumberProgressBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mNumberHelper = new NumberHelper();
}
public void setTextColor(ColorStateList color) {
mNumberHelper.setTextColor(color);
}
public void setTextSize(float textSize) {
mNumberHelper.setTextSize(textSize);
}
public void setTypeface(Typeface tf) {
mNumberHelper.setTypeface(tf);
}
@Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mNumberHelper.measure(widthMeasureSpec, heightMeasureSpec);
}
@Override
public void setProgressDrawable(Drawable d) {
super.setProgressDrawable(tileifyProgressDrawable(d));
}
private Drawable tileifyProgressDrawable(Drawable wrapped) {
if (wrapped instanceof LayerDrawable) {
LayerDrawable drawable = (LayerDrawable) wrapped;
final int N = drawable.getNumberOfLayers();
Drawable[] outDrawables = new Drawable[N];
for (int i = 0; i < N; i++) {
final int id = drawable.getId(i);
Drawable childDrawable = drawable.getDrawable(i);
if (id == android.R.id.background) {
outDrawables[i] = new NumberBGDrawable(childDrawable);
} else if (id == android.R.id.progress) {
if (childDrawable instanceof ScaleDrawable) {
outDrawables[i] = tileifyScaleDrawable((ScaleDrawable) childDrawable);
} else if (childDrawable instanceof ClipDrawable) {
outDrawables[i] = tileifyClipDrawable((ClipDrawable) childDrawable);
} else {
outDrawables[i] = childDrawable;
}
} else {
outDrawables[i] = childDrawable;
}
}
LayerDrawable newDrawable = new NumberLayerDrawable(outDrawables);
return newDrawable;
}
return wrapped;
}
private Drawable tileifyScaleDrawable(ScaleDrawable wrapped) {
try {
Field mScaleStateField = ScaleDrawable.class.getDeclaredField("mScaleState");
if (mScaleStateField != null) {
mScaleStateField.setAccessible(true);
Object mScaleState = mScaleStateField.get(wrapped);
if (mScaleState != null) {
Class<?> clazz = mScaleState.getClass();
if (TextUtils.equals(clazz.getSimpleName(), "ScaleState")) {
Drawable drawable = null;
int gravity = Gravity.LEFT;
float scaleWidth = 0;
float scaleHeight = 0;
Field mDrawableField = clazz.getDeclaredField("mDrawable");
if (mDrawableField != null) {
mDrawableField.setAccessible(true);
drawable = (Drawable) mDrawableField.get(mScaleState);
}
Field mGravityField = clazz.getDeclaredField("mGravity");
if (mGravityField != null) {
mGravityField.setAccessible(true);
gravity = mGravityField.getInt(mScaleState);
}
Field mScaleWidthField = clazz.getDeclaredField("mScaleWidth");
if (mScaleWidthField != null) {
mScaleWidthField.setAccessible(true);
scaleWidth = mScaleWidthField.getFloat(mScaleState);
}
Field mScaleHeightField = clazz.getDeclaredField("mScaleHeight");
if (mScaleHeightField != null) {
mScaleHeightField.setAccessible(true);
scaleHeight = mScaleHeightField.getFloat(mScaleState);
}
return new NumberScaleDrawable(drawable, gravity, scaleWidth, scaleHeight);
}
}
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
return wrapped;
}
private Drawable tileifyClipDrawable(ClipDrawable wrapped) {
try {
Field mClipStateField = ClipDrawable.class.getDeclaredField("mClipState");
if (mClipStateField != null) {
mClipStateField.setAccessible(true);
Object mClipState = mClipStateField.get(wrapped);
if (mClipState != null) {
Class<?> clazz = mClipState.getClass();
if (TextUtils.equals(clazz.getSimpleName(), "ClipState")) {
Drawable drawable = null;
int gravity = Gravity.LEFT;
int orientation = ClipDrawable.HORIZONTAL;
Field mDrawableField = clazz.getDeclaredField("mDrawable");
if (mDrawableField != null) {
mDrawableField.setAccessible(true);
drawable = (Drawable) mDrawableField.get(mClipState);
}
Field mGravityField = clazz.getDeclaredField("mGravity");
if (mGravityField != null) {
mGravityField.setAccessible(true);
gravity = mGravityField.getInt(mClipState);
}
Field mOrientationField = clazz.getDeclaredField("mOrientation");
if (mOrientationField != null) {
mOrientationField.setAccessible(true);
orientation = mOrientationField.getInt(mClipState);
}
return new NumberClipDrawable(drawable, gravity, orientation);
}
}
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
return wrapped;
}
class NumberBGDrawable extends InsetDrawable implements DrawCallback {
public NumberBGDrawable(Drawable drawable) {
super(drawable, 0);
}
@Override
public void draw(Canvas canvas) {
if (mNumberHelper != null) {
mNumberHelper.proxyDrawBG(canvas, this);
} else {
scheduleDraw(canvas);
}
}
@Override
public void scheduleDraw(Canvas canvas) {
super.draw(canvas);
}
}
class NumberClipDrawable extends ClipDrawable implements DrawCallback {
public NumberClipDrawable(Drawable drawable, int gravity, int orientation) {
super(drawable, gravity, orientation);
}
@Override
public void draw(Canvas canvas) {
if (mNumberHelper != null) {
mNumberHelper.proxyDrawPG(canvas, this);
} else {
scheduleDraw(canvas);
}
}
@Override
public void scheduleDraw(Canvas canvas) {
super.draw(canvas);
}
}
class NumberScaleDrawable extends ScaleDrawable implements DrawCallback {
public NumberScaleDrawable(Drawable drawable, int gravity, float scaleWidth, float scaleHeight) {
super(drawable, gravity, scaleWidth, scaleHeight);
}
@Override
public void draw(Canvas canvas) {
if (mNumberHelper != null) {
mNumberHelper.proxyDrawPG(canvas, this);
} else {
scheduleDraw(canvas);
}
}
@Override
public void scheduleDraw(Canvas canvas) {
super.draw(canvas);
}
}
class NumberLayerDrawable extends LayerDrawable implements LevelCallback {
public NumberLayerDrawable(Drawable[] layers) {
super(layers);
}
@Override
protected boolean onLevelChange(int level) {
if (mNumberHelper != null && mNumberHelper.proxyLevelChange(level, this)) {
invalidate();
return true;
}
return super.onLevelChange(level);
}
@Override
public boolean scheduleLevel(int level) {
return super.onLevelChange(level);
}
}
@Override
public void setIndeterminateDrawable(Drawable d) {
super.setIndeterminateDrawable(tileifyIndeterminateDrawable(d));
}
private Drawable tileifyIndeterminateDrawable(Drawable wrapped) {
if (wrapped instanceof AnimationDrawable) {
return tileifyAnimationDrawable((AnimationDrawable) wrapped);
}
return wrapped;
}
private Drawable tileifyAnimationDrawable(AnimationDrawable wrapped) {
NumberAnimationDrawable drawable = new NumberAnimationDrawable();
drawable.setOneShot(wrapped.isOneShot());
final int N = wrapped.getNumberOfFrames();
for (int i = 0; i < N; i++) {
Drawable frame = wrapped.getFrame(i);
int duration = wrapped.getDuration(i);
drawable.addFrame(frame, duration);
}
return drawable;
}
class NumberAnimationDrawable extends AnimationDrawable implements DrawCallback {
public NumberAnimationDrawable() {
super();
}
@Override
public void draw(Canvas canvas) {
if (mNumberHelper != null) {
mNumberHelper.proxyDrawIM(canvas, this);
} else {
scheduleDraw(canvas);
}
}
@Override
public void scheduleDraw(Canvas canvas) {
super.draw(canvas);
}
}
class NumberHelper {
private static final String FORMAT_PROGRESS = "%1$d%%";
private TextPaint mTextPaint;
private int mCurTextColor;
private ColorStateList mTextColor;
private float mTextLRPad;
private AtomicBoolean mIsMeasure = new AtomicBoolean(false);
public NumberHelper() {
super();
setup();
}
private void setup() {
mTextPaint = new TextPaint();
mTextPaint.setAntiAlias(true);
mTextPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12, getContext().getResources().getDisplayMetrics()));
mTextPaint.setTextAlign(Paint.Align.CENTER);
mTextColor = ColorStateList.valueOf(Color.BLACK);
mTextLRPad = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getContext().getResources().getDisplayMetrics());
}
public void setTextColor(ColorStateList color) {
if (color != null) {
mTextColor = color;
} else {
mTextColor = ColorStateList.valueOf(Color.BLACK);
}
updateTextColors();
}
public void setTextSize(float textSize) {
if (mTextPaint.getTextSize() != textSize) {
mTextPaint.setTextSize(textSize);
rInvalidate();
}
}
public void setTypeface(Typeface tf) {
if (mTextPaint.getTypeface() != tf) {
mTextPaint.setTypeface(tf);
rInvalidate();
}
}
private void updateTextColors() {
boolean inval = false;
int color = mTextColor.getColorForState(getDrawableState(), 0);
if (color != mCurTextColor) {
mCurTextColor = color;
inval = true;
}
if (inval) {
rInvalidate();
}
}
private void rInvalidate() {
}
private boolean shouldDrawText() {
return getProgress() > 0 && mIsMeasure.get();
}
private float generateProgress() {
return getMax() > 0 ? getProgress() * 1.0f / getMax() : 0;
}
private String generateText() {
final int progressInt = Math.round(generateProgress() * 100);
return String.format(Locale.getDefault(), FORMAT_PROGRESS, progressInt);
}
private RectF generateTextRectF() {
FontMetrics fontMetrics = mTextPaint.getFontMetrics();
final float height = fontMetrics.bottom - fontMetrics.top;
String text = generateText();
final float width = Layout.getDesiredWidth(text, mTextPaint) + 2 * mTextLRPad;
return new RectF(0, 0, width, height);
}
public void measure(int widthMeasureSpec, int heightMeasureSpec) {
mIsMeasure.compareAndSet(false, true);
if (shouldDrawText()) {
final int measuredWidth = getMeasuredWidth();
final int measuredHeight = getMeasuredHeight();
RectF targetRectF = generateTextRectF();
setMeasuredDimension(Math.max(measuredWidth, resolveSize(Math.round(targetRectF.width()) + getPaddingLeft() + getPaddingRight(), widthMeasureSpec)),
Math.max(measuredHeight, resolveSize(Math.round(targetRectF.height()) + getPaddingTop() + getPaddingBottom(), heightMeasureSpec)));
}
}
public void proxyDrawBG(Canvas canvas, DrawCallback callback) {
if (shouldDrawText()) {
final float progress = generateProgress();
final RectF targetRectF = generateTextRectF();
final float textWidth = targetRectF.width();
final float width = getWidth();
final float height = getHeight();
// Drawable
{
final float left = width * progress + textWidth > width ? width : width * progress + textWidth;
final float top = 0;
final float right = width;
final float bottom = height;
canvas.save();
canvas.clipRect(left, top, right, bottom);
callback.scheduleDraw(canvas);
canvas.restore();
}
} else {
callback.scheduleDraw(canvas);
}
}
public boolean proxyLevelChange(int level, LevelCallback callback) {
if (shouldDrawText()) {
final float progress = generateProgress();
final RectF targetRectF = generateTextRectF();
final float textWidth = targetRectF.width();
final float width = getWidth();
if (width * progress + textWidth > width) {
int proxyLevel = Math.round((width - textWidth) * MAX_LEVEL / width);
callback.scheduleLevel(proxyLevel);
return true;
}
}
return false;
}
public void proxyDrawPG(Canvas canvas, DrawCallback callback) {
if (shouldDrawText()) {
final float progress = generateProgress();
final String text = generateText();
final RectF targetRectF = generateTextRectF();
final float textWidth = targetRectF.width();
final float width = getWidth();
final float height = getHeight();
// Drawable
{
final float left = 0;
final float top = 0;
final float right = width * progress + textWidth > width ? width - textWidth : width * progress;
final float bottom = height;
canvas.save();
canvas.clipRect(left, top, right, bottom);
callback.scheduleDraw(canvas);
canvas.restore();
}
// Text
{
final float left = width * progress + textWidth > width ? width - textWidth : width * progress;
final float top = 0;
final float right = left + textWidth;
final float bottom = height;
FontMetrics fontMetrics = mTextPaint.getFontMetrics();
final float baseline = top + (bottom - top - fontMetrics.descent + fontMetrics.ascent) / 2 - fontMetrics.ascent;
canvas.save();
mCurTextColor = mTextColor.getColorForState(getDrawableState(), 0);
mTextPaint.setColor(mCurTextColor);
canvas.drawText(text, (left + right) * 0.5f, baseline, mTextPaint);
canvas.restore();
}
} else {
callback.scheduleDraw(canvas);
}
}
public void proxyDrawIM(Canvas canvas, DrawCallback callback) {
if (shouldDrawText()) {
final float progress = generateProgress();
final String text = generateText();
final RectF targetRectF = generateTextRectF();
final float textWidth = targetRectF.width();
final float width = getWidth();
final float height = getHeight();
// Drawable
{
callback.scheduleDraw(canvas);
}
// Text
{
final float left = width * progress + textWidth > width ? width - textWidth : width * progress;
final float top = 0;
final float right = left + textWidth;
final float bottom = height;
FontMetrics fontMetrics = mTextPaint.getFontMetrics();
final float baseline = top + (bottom - top - fontMetrics.descent + fontMetrics.ascent) / 2 - fontMetrics.ascent;
canvas.save();
canvas.drawText(text, (left + right) * 0.5f, baseline, mTextPaint);
canvas.restore();
}
} else {
callback.scheduleDraw(canvas);
}
}
}
interface DrawCallback {
public void scheduleDraw(Canvas canvas);
}
interface LevelCallback {
public boolean scheduleLevel(int level);
}
}