Last active
August 6, 2019 13:48
-
-
Save pythoncat1024/0c45c007b671ed30f60cbf7ac658a866 to your computer and use it in GitHub Desktop.
正多边形进度条,不闪烁
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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