Skip to content

Instantly share code, notes, and snippets.

@pythoncat1024
Last active August 6, 2019 13:48
Show Gist options
  • Save pythoncat1024/0c45c007b671ed30f60cbf7ac658a866 to your computer and use it in GitHub Desktop.
Save pythoncat1024/0c45c007b671ed30f60cbf7ac658a866 to your computer and use it in GitHub Desktop.
正多边形进度条,不闪烁
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="RoundProgressBar">
<attr name="max" format="integer" />
<attr name="progress" format="integer" />
<attr name="secondaryProgress" format="integer" />
<attr name="textSize" format="dimension" />
<attr name="textColor" format="color" />
<attr name="useCenter" format="boolean" />
<attr name="showNumbers" format="boolean" />
<attr name="startAngle" format="integer" />
<attr name="progressColor" format="color" />
<attr name="restColor" format="color" />
<attr name="edgeCount" format="integer" />
<attr name="stokePercent" format="fraction" />
</declare-styleable>
</resources>
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import com.python.cat.needwork.R;
import java.util.Locale;
public class RoundProgressBar extends View {
private int DEFAULT_SIZE;
private int mWidth;
private int mHeight;
private int mMax;
private int mProgress;
private int mSecondaryProgress;
private boolean mUseCenter;
private int mProgressColor;
private boolean mShowNumbers;
private int mStartAngle;
private int mRestColor;
private int mEdgeCount;
private float mStokePercent;
private Paint shapePaint;
private Paint progressPaint;
private Paint secondaryProgressPaint;
private Paint textPaint;
private Rect textBounds; // 文字范围,不过坐标是相对基线,而不是坐标原点
private Path textCirclePath; // 文字区域所在路径
private float mTextSize;
private float mStrokeWidth;
private Path outerShapePath;
private Path innerShapePath;
private Path progressPath;
private Path secondaryProgressPath;
private int mTextColor;
// touch 相关----⤵↓
private int touchSlop;
private float eventX;
private float eventY;
private long eventTime;
private int tapTimeout;
private int longPressTimeout;
// touch 相关----⬆↑
public RoundProgressBar(Context context) {
super(context);
init();
}
public RoundProgressBar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RoundProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public RoundProgressBar(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.RoundProgressBar, defStyleAttr, defStyleRes);
mUseCenter = a.getBoolean(R.styleable.RoundProgressBar_useCenter, true);
mShowNumbers = a.getBoolean(R.styleable.RoundProgressBar_showNumbers, false);
mMax = a.getInteger(a.getIndex(R.styleable.RoundProgressBar_max), 100);
mProgress = a.getInteger(a.getIndex(R.styleable.RoundProgressBar_progress), 0);
mSecondaryProgress = a.getInteger(a.getIndex(R.styleable.RoundProgressBar_secondaryProgress), 0);
mTextSize = a.getDimension(R.styleable.RoundProgressBar_textSize, sp2px(12));
mTextColor = a.getColor(R.styleable.RoundProgressBar_textColor, Color.BLACK);
mStartAngle = a.getInt(R.styleable.RoundProgressBar_startAngle, -90);
mProgressColor = a.getColor(R.styleable.RoundProgressBar_progressColor, Color.RED);
mRestColor = a.getColor(R.styleable.RoundProgressBar_restColor, Color.BLACK);
mEdgeCount = a.getInt(R.styleable.RoundProgressBar_edgeCount, 0);
// https://www.jianshu.com/p/8ba0e86adabb
mStokePercent = a.getFraction(R.styleable.RoundProgressBar_stokePercent, 1, 1, 5);
// 不清楚为何自定义属性不设置的话,获取值为负数!
mMax = mMax < 0 ? 100 : mMax;
mProgress = mProgress < 0 ? 0 : mProgress;
mSecondaryProgress = mSecondaryProgress < 0 ? 0 : mSecondaryProgress;
a.recycle();
init();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
showAttrsInfo();
this.mWidth = w;
this.mHeight = h;
// add args
mStrokeWidth = mStokePercent > 0
? mStokePercent * Math.min(getWidth(), getHeight())
: 0.2f * Math.min(mWidth, mHeight);
shapePaint.setStrokeCap(Paint.Cap.BUTT);
shapePaint.setStyle(Paint.Style.FILL);
// LogUtils.e("shape stoke width:" + mStrokeWidth);
shapePaint.setColor(mRestColor);
progressPaint.setColor(mProgressColor);
progressPaint.setStyle(Paint.Style.FILL);
progressPaint.setStrokeCap(mEdgeCount < 3 ? Paint.Cap.ROUND : Paint.Cap.BUTT);
secondaryProgressPaint.set(progressPaint);
secondaryProgressPaint.setAlpha(60);
textPaint.setTextSize(mTextSize);
textPaint.setColor(mTextColor);
textPaint.setStrokeWidth(4); // 这个有用,可以让文字为粗体
textPaint.setStyle(Paint.Style.STROKE); // 跟上面关联使用,可以写出空心字
textPaint.setTextAlign(Paint.Align.CENTER);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth = computeRealSize(widthMeasureSpec);
int measuredHeight = computeRealSize(heightMeasureSpec);
setMeasuredDimension(measuredWidth, measuredHeight);
Log.e(getClass().getSimpleName(), "measuredWidth:" + measuredWidth + ",measuredHeight:" + measuredHeight);
}
private int computeRealSize(int measureSpec) {
int realSize = 0;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.EXACTLY:
realSize = size > 0 ? size : DEFAULT_SIZE;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
realSize = mTextSize > 0 ? 3 * Math.round(mTextSize) : DEFAULT_SIZE;
realSize = realSize > DEFAULT_SIZE ? realSize : DEFAULT_SIZE;
break;
}
return realSize;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
resetAllPathsBeforeDraw();
// canvas.drawRect(0, 0, mWidth, mHeight, textPaint); // for debug
int centerX = (getPaddingLeft() + getWidth() - getPaddingRight()) / 2;
int centerY = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2;
int radius = Math.min(
getWidth() - getPaddingLeft() - getPaddingRight(),
getHeight() - getPaddingTop() - getPaddingBottom()
) / 2;
canvas.rotate(mStartAngle, centerX, centerY); // 适应起始角度
drawShape(canvas, centerX, centerY, radius);
drawProgress(canvas, centerX, centerY, radius);
drawSecondaryProgress(canvas, centerX, centerY, radius);
canvas.rotate(-mStartAngle, centerX, centerY); // 回到正常的方向
if (mShowNumbers) {
drawText(canvas, centerX, centerY, radius);
}
}
/**
* 文字居中参考 https://www.jianshu.com/p/8b97627b21c4
*/
private void drawText(Canvas canvas, int centerX, int centerY, int radius) {
Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
float distance = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;
float baseline = centerY + distance;
long progress = Math.round(mProgress * 1.0 / mMax * 100);
progress = progress > 100 ? 100 : progress;
String text = progress + "%";
// 文字要在最后,不然被前面的背景覆盖住了
canvas.drawText(text, centerX, baseline, textPaint);
}
/**
* 闪烁的根源找到了, path 过度叠加导致。每次先 reset 即可!
*/
private void resetAllPathsBeforeDraw() {
if (textCirclePath != null) {
textCirclePath.reset();
}
if (outerShapePath != null) {
outerShapePath.reset();
}
if (innerShapePath != null) {
innerShapePath.reset();
}
if (progressPath != null) {
progressPath.reset();
}
if (secondaryProgressPath != null) {
secondaryProgressPath.reset();
}
}
/**
* 坐标计算参考
* https://juejin.im/entry/584fa53a8d6d8100545cec7c
*/
private void drawShape(Canvas canvas, int cX, int cY, int radius) {
int innerRadius = (int) (radius - mStrokeWidth);
canvas.save();
if (mEdgeCount < 3) {
// circle
outerShapePath.addCircle(cX, cY, radius, Path.Direction.CCW);
innerShapePath.addCircle(cX, cY, innerRadius, Path.Direction.CW);
} else {
// shape
float eachAngle = 360.0f / mEdgeCount;
double eachRadians = Math.toRadians(eachAngle);
// StringBuffer points = new StringBuffer("points:");
for (int i = 0; i < mEdgeCount; i++) {
float outX = (float) (cX + radius * Math.cos(eachRadians * (1 + i)));
float outY = (float) (cY + radius * Math.sin(eachRadians * (1 + i)));
float intX = (float) (cX + innerRadius * Math.cos(eachRadians * (1 + i)));
float intY = (float) (cY + innerRadius * Math.sin(eachRadians * (1 + i)));
if (i == 0) {
outerShapePath.moveTo(outX, outY);
innerShapePath.moveTo(intX, intY);
} else {
outerShapePath.lineTo(outX, outY);
innerShapePath.lineTo(intX, intY);
}
// points.append("(" + outX + "," + outY + "),");
// canvas.drawPoint(outX, outY, shapePaint);
}
outerShapePath.close();
innerShapePath.close();
}
if (!mUseCenter) {
// 此时需要镂空多边形
outerShapePath.op(innerShapePath, Path.Op.DIFFERENCE);
}
// canvas.drawCircle(cX, cY, radius, textPaint);
canvas.drawPath(outerShapePath, shapePaint);
canvas.restore();
}
private void drawProgress(Canvas canvas, int cX, int cY, int radius) {
float left = cX - radius;
float top = cY - radius;
float right = cX + radius;
float bottom = cY + radius;
float sweepAngle = mProgress * 1.0f / mMax * 360;
// LogUtils.e("sweepAngle: " + sweepAngle + "------" + mProgress + "," + mMax);
progressPath.moveTo(cX, cY);
float startX = (float) (cX + radius);
float startY = (float) (cY);
progressPath.lineTo(startX, startY);
progressPath.addArc(left, top, right, bottom, 0, sweepAngle);
progressPath.lineTo(cX, cY);
progressPath.close();
progressPath.op(outerShapePath, Path.Op.INTERSECT);
if (mUseCenter && mShowNumbers) {
// 进度条不遮挡数字
final String holderText = "100% "; // 占位
textPaint.getTextBounds(holderText, 0, holderText.length(), textBounds);
float length = textPaint.measureText(holderText);
float max = Math.max(textBounds.width(), textBounds.height());
max = length > max ? length : max;
textCirclePath.addCircle(cX, cY, max / 2, Path.Direction.CW);
progressPath.op(textCirclePath, Path.Op.DIFFERENCE);
}
canvas.drawPath(progressPath, progressPaint);
}
private void drawSecondaryProgress(Canvas canvas, int cX, int cY, int radius) {
float left = cX - radius;
float top = cY - radius;
float right = cX + radius;
float bottom = cY + radius;
float sweepAngle = mSecondaryProgress * 1.0f / mMax * 360;
// LogUtils.e("sweepAngle: " + sweepAngle + "---" + mSecondaryProgress + "," + mMax);
secondaryProgressPath.moveTo(cX, cY);
float startX = (float) (cX + radius);
float startY = (float) (cY);
secondaryProgressPath.lineTo(startX, startY);
secondaryProgressPath.addArc(left, top, right, bottom, 0, sweepAngle);
secondaryProgressPath.lineTo(cX, cY);
secondaryProgressPath.close();
secondaryProgressPath.op(outerShapePath, Path.Op.INTERSECT);
if (mUseCenter && mShowNumbers) {
// 进度条不遮挡数字
final String holderText = "100% "; // 占位
textPaint.getTextBounds(holderText, 0, holderText.length(), textBounds);
float length = textPaint.measureText(holderText);
float max = Math.max(textBounds.width(), textBounds.height());
max = length > max ? length : max;
textCirclePath.addCircle(cX, cY, max / 2, Path.Direction.CW);
secondaryProgressPath.op(textCirclePath, Path.Op.DIFFERENCE);
}
canvas.drawPath(secondaryProgressPath, secondaryProgressPaint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
eventX = event.getX();
eventY = event.getY();
eventTime = SystemClock.elapsedRealtime();
break;
case MotionEvent.ACTION_MOVE: {
float y = event.getY();
float x = event.getX();
float diffX = x - eventX;
float diffY = y - eventY;
// LogUtils.d("dx=%.1f,dy=%.1f,ts=%d", diffX, diffY, touchSlop);
if (/*Math.abs(diffX) < touchSlop && */Math.abs(diffY) >= touchSlop) {
// 默认滑太多,搞个 0.25 缓冲一下
float factor = (-1f) * diffY / mHeight * 0.25f; // 预期从中间滑到顶部,为 +50%
// 系统坐标决定下滑时 diffY 为正值,所以前面加 (-1f)
mProgress += factor * mMax;
// LogUtils.i("factor=%s,progress=%s", factor, mProgress);
mProgress = mProgress > mMax ? mMax : mProgress;
mProgress = mProgress < 0 ? 0 : mProgress;
setProgress(mProgress);
}
}
break;
case MotionEvent.ACTION_UP: {
// LogUtils.e("#ACTION_UP#ACTION_UPACTION_UP#ACTION_UP#ACTION_UP");
float y = event.getY();
float x = event.getX();
float diffX = x - eventX;
float diffY = y - eventY;
long elapsedRealtime = SystemClock.elapsedRealtime() - eventTime;
boolean noMove = Math.abs(diffX) < touchSlop && Math.abs(diffY) < touchSlop;
if (noMove && elapsedRealtime > longPressTimeout) {
// 超过此时间就认为是长按,认为是一次长按
performLongClick();
// 长按不进行 进度的累加
} else if (noMove && elapsedRealtime < tapTimeout) {
// 小于此时间,认为是一次点击
if (y < mHeight / 2) {
// 点击上边,让进度 + 5 %
mProgress += 0.05 * mMax;
} else {
// 点击下边,让进度 - 5 %
mProgress -= 0.05 * mMax;
}
mProgress = mProgress > mMax ? mMax : mProgress;
mProgress = mProgress < 0 ? 0 : mProgress;
setProgress(mProgress);
performClick();
}
}
break;
}
return true;
}
@Override
public boolean performClick() {
return super.performClick();
}
private void init() {
DEFAULT_SIZE = Math.round(dp2px(120));
Log.e(getClass().getSimpleName(), "default size=== " + DEFAULT_SIZE);
shapePaint = new Paint();
progressPaint = new Paint();
secondaryProgressPaint = new Paint();
textPaint = new Paint();
textBounds = new Rect();
textCirclePath = new Path();
outerShapePath = new Path();
innerShapePath = new Path();
progressPath = new Path();
secondaryProgressPath = new Path();
// touch 相关
ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext());
// 获取touchSlop (系统 滑动距离的最小值,大于该值可以认为滑动)
touchSlop = viewConfiguration.getScaledTouchSlop();
// 获得允许执行fling (抛)的最小速度值
int minimumVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
// 获得允许执行fling (抛)的最大速度值
int maximumVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
// Report if the device has a permanent menu key available to the user
// (报告设备是否有用户可找到的永久的菜单按键)
// 即判断设备是否有返回、主页、菜单键等实体按键(非虚拟按键)
boolean hasPermanentMenuKey = viewConfiguration.hasPermanentMenuKey();
// 获得敲击超时时间,如果在此时间内没有移动,则认为是一次点击
tapTimeout = ViewConfiguration.getTapTimeout();
// 双击间隔时间,在该时间内被认为是双击
int doubleTapTimeout = ViewConfiguration.getDoubleTapTimeout();
// 长按时间,超过此时间就认为是长按
longPressTimeout = ViewConfiguration.getLongPressTimeout();
// 重复按键间隔时间
int repeatTimeout = ViewConfiguration.getKeyRepeatTimeout();
//LogUtils.e("tapTimeout=%s,longPressTimeout=%s,doubleTapTimeout=%s,repeatTimeout=%s",
// tapTimeout, longPressTimeout, doubleTapTimeout, repeatTimeout);
}
private void showAttrsInfo() {
StringBuffer attrsInfo = new StringBuffer("attrs:\n{\n")
.append(String.format(Locale.ENGLISH, "\t%s:%s,\n", "max", mMax))
.append(String.format(Locale.ENGLISH, "\t%s:%s,\n", "progress", mProgress))
.append(String.format(Locale.ENGLISH, "\t%s:%s,\n", "secondaryProgress", mSecondaryProgress))
.append(String.format(Locale.ENGLISH, "\t%s:%s,\n", "textSize", mTextSize))
.append(String.format(Locale.ENGLISH, "\t%s:%s,\n", "textColor", mTextColor))
.append(String.format(Locale.ENGLISH, "\t%s:%s,\n", "userCenter", mUseCenter))
.append(String.format(Locale.ENGLISH, "\t%s:%s,\n", "showNumbers", mShowNumbers))
.append(String.format(Locale.ENGLISH, "\t%s:%s,\n", "startAngle", mStartAngle))
.append(String.format(Locale.ENGLISH, "\t%s:%s,\n", "progressColor", mProgressColor))
.append(String.format(Locale.ENGLISH, "\t%s:%s,\n", "restColor", mRestColor))
.append(String.format(Locale.ENGLISH, "\t%s:%s,\n", "edgeCount", mEdgeCount))
.append(String.format(Locale.ENGLISH, "\t%s:%s", "stokePercent", mStokePercent));
attrsInfo.append("\n}");
// LogUtils.v(attrsInfo);
}
/**
* convert dp to its equivalent px
*/
protected float dp2px(int dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
dp, getResources().getDisplayMetrics());
}
/**
* convert sp to its equivalent px
*/
protected float sp2px(int sp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
sp, getResources().getDisplayMetrics());
}
protected int px2sp(float pxValue) {
final float fontScale = getResources().getDisplayMetrics().scaledDensity;
return (int) (pxValue / fontScale + 0.5f);
}
public void setProgress(int mProgress) {
this.mProgress = mProgress;
invalidate();
}
public int getProgress() {
return mProgress;
}
public void setSecondaryProgress(int mSecondaryProgress) {
this.mSecondaryProgress = mSecondaryProgress;
invalidate();
}
public int getSecondaryProgress() {
return mSecondaryProgress;
}
public void setMax(int mMax) {
this.mMax = mMax;
invalidate();
}
public int getMax() {
return mMax;
}
public void setEdgeCount(int mEdgeCount) {
this.mEdgeCount = mEdgeCount;
invalidate();
}
public int getEdgeCount() {
return mEdgeCount;
}
public void setShowNumbers(boolean mShowNumbers) {
this.mShowNumbers = mShowNumbers;
invalidate();
}
public boolean isShowNumbers() {
return mShowNumbers;
}
public void setUseCenter(boolean mUseCenter) {
this.mUseCenter = mUseCenter;
invalidate();
}
public boolean isUseCenter() {
return mUseCenter;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment