-
Notifications
You must be signed in to change notification settings - Fork 5
/
MultiCircleProgressView.java
475 lines (433 loc) · 18 KB
/
MultiCircleProgressView.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
package com.androidanimation.progressanim.sample1;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.SweepGradient;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.util.ArrayList;
import java.util.List;
/**
* 多状态圆形进度条控件,进度1~100可控,完成分为成功失败两种
* 在SurfaceView中绘制,不阻塞UI线程,可以自己控制帧率
*/
public class MultiCircleProgressView extends SurfaceView implements SurfaceHolder.Callback {
// ================ 公共数据 ============= //
/** 顶部作为计数起点, 右边是0,左边是-180或者180,底部是90 */
private static final int START_POINT_TOP = -90;
/** 圆环进度的颜色 */
private int mRoundProgressColor = 0xff04d3ff;
/** 圆心的x坐标 */
int mCenterX = 0;
/** 圆心的y坐标 */
int mCenterY = 0;
/** 定义画笔 */
private Paint mProgressPaint;
/** 定义监听事件列表 */
private List<IProgressStateChangeListener> mProgressStateChangeListeners;
/** SurfaceView的绘制线程 */
private DrawThread mDrawThread;
private SurfaceHolder mSurfaceHolder;
/** 最大帧数 (1000 / 20) */
private static final int DRAW_INTERVAL = 20;
private static final int CLEAR_COLOR = 0xff0583f7;
// ================ 外环进度数据 ============= //
/** 顶部作为计数起点 270度, 计算圆上的任意点坐标时顺时针为正,右边是0 */
private static final float HEAD_CIRCLE_START_ANGLE = 270f;
/** 进度条每次移动的角度 */
private static int mOuterProgressStep = 6;
/** 修改这个颜色数组就会出现不一样的渐变圆弧 */
private int[] mColors = {
0x0004d3ff, 0x0004d3ff, 0x4004d3ff, 0x8004d3ff, 0xff04d3ff
};
/** 外环渐变处理器 */
private SweepGradient mOuterSweepGradient;
/** 外环用于旋转的矩阵 Matrix */
private Matrix mOuterMatrix = new Matrix();
/** 外圆环的宽度 */
private float mOuterRoundWidth;
/** 外圆环头部的圆圈半径 */
private float mOuterHeadCircleWidth;
/** 外环的半径 */
private float mOuterRadius = 0;
/** 外环背景的半径 */
private float mOuterBgRadius = 0;
/** 外环角度旋转总进度*/
private float mOuterAngleProgressTotal = 0;
/** 外环头部圆选择角度 */
private float mOuterHeadCircleAngleTotal = 0;
private double mOuterHeadCircleAngleTotalMath = 0;
// ================ 内环进度数据 ============= //
/** 内环的半径 */
private float mInnerRadius = 0;
/** 内圆环的宽度 */
private float mInnerRoundWidth;
/** 用于定义的圆弧的形状和大小的界限 */
private RectF mInnerArcLimitRect = new RectF();
/** 内环总弧长 */
private float mInnerArcAngle = 0;
// ================ 中间百分比数据 ============= //
private static final int MAX_PROGRESS_DEFAULT = 100;
private static final int MIN_PROGRESS_DEFAULT = 1;
private static final int PERCENT_BASE = 100;
private static final float TOTAL_ANGLE = 360f;
/** 当前进度 */
private int mCurProgress = 0;
private String mCurProgressStr = "";
private String mPercentSignStr = "%";
/** 中间进度百分比的字符串的颜色 */
private int mPercentTextColor = 0xffffffff;
/** 中间进度百分比的字符串的字体大小 */
private float mPercentTextSize;
/** 中间进度百分号字体大小 */
private float mPercentSignSize;
/** 百分比文本与%符号之间的间距 */
private float mSpaceTextAndSign;
/** 百分比数字文本的边界 */
private Rect mPercentTextBounds = new Rect();
/** %符号的边界 */
private Rect mPercentSignBounds = new Rect();
/** 字体绘制的基线 */
private float mFontBaseline;
public MultiCircleProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mProgressStateChangeListeners = new ArrayList<IProgressStateChangeListener>();
mProgressPaint = new Paint();
DisplayMetrics metrics = getResources().getDisplayMetrics();
mOuterRoundWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, metrics);
mInnerRoundWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, metrics);
mOuterRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 70, metrics);
mOuterBgRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 75, metrics);
mInnerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 60, metrics);
mSpaceTextAndSign = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, metrics);
mOuterHeadCircleWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2.5f, metrics);
mPercentTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 45, metrics);
mPercentSignSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 15, metrics);
mSurfaceHolder = getHolder();
mSurfaceHolder.addCallback(this);
}
/** 剩下需要快速完成的角度 */
private float mNeedQuickCompleteAngle = 0;
/** 开始快速完成时的角度起始值 */
private float mQuickStartAngle = 0;
/** 是否需要快速完成 */
private boolean isNeedCompleteQuickly = false;
public void completeQuickly() {
smoothScrollToAngle(TOTAL_ANGLE);
}
private float mSmoothScrollTotalAngle = 0;
/**
* 平滑旋转到指定的角度(0~360度)
* @param angle 0~360度
*/
private void smoothScrollToAngle(float angle) {
isNeedCompleteQuickly = true;
mQuickStartAngle = 0;
mSmoothScrollTotalAngle = angle;
}
private int mSmoothScrollToProgress = 0;
/**
* 平滑旋转到指定的进度(0~100)
* @param progress
*/
public void smoothScrollToProgress(int progress) {
if (progress <= MIN_PROGRESS_DEFAULT || progress >= MAX_PROGRESS_DEFAULT) {
return;
}
mSmoothScrollToProgress = progress;
smoothScrollToAngle(progress * TOTAL_ANGLE / PERCENT_BASE);
}
public void start() {
reset();
if (mDrawThread == null) {
mDrawThread = new DrawThread(mSurfaceHolder, getContext());
}
try {
if (!mDrawThread.isRunning) {
mDrawThread.isRunning = true;
mDrawThread.start();
}
} catch (Exception ignore) {
ignore.printStackTrace();
}
}
public void stop() {
if (mDrawThread == null) {
return;
}
mDrawThread.isRunning = false;
try {
mDrawThread.join();
mDrawThread = null;
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
start();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { }
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
stop();
}
class DrawThread extends Thread {
SurfaceHolder surfaceHolder;
Context context;
boolean isRunning;
public DrawThread(SurfaceHolder surfaceHolder, Context context) {
this.surfaceHolder = surfaceHolder;
this.context = context;
}
@Override
public void run() {
long deltaTime;
long timeStartPerDraw;
while (isRunning) {
Canvas canvas = null;
timeStartPerDraw = System.currentTimeMillis();
try {
synchronized (surfaceHolder) {
Surface surface = surfaceHolder.getSurface();
if (surface != null && surface.isValid()) {
canvas = surfaceHolder.lockCanvas(null);
}
if (canvas != null) {
doDraw(canvas);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (surfaceHolder != null && canvas != null) {
try {
surfaceHolder.unlockCanvasAndPost(canvas);
} catch (Exception ignore) {}
}
}
deltaTime = System.currentTimeMillis() - timeStartPerDraw;
if (deltaTime < DRAW_INTERVAL) {
try {
// 控制帧数
Thread.sleep(DRAW_INTERVAL - deltaTime);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
private Path mBgCirclePath = new Path();
@Override
public void draw(Canvas canvas) {
// clipCanvas(canvas);
super.draw(canvas);
}
private void clipCanvas(Canvas canvas) {
// 裁剪画布为一个圆,但是有锯齿啊。。。
// 设置裁剪的圆心,半径
if (mCenterX != 0) {
mBgCirclePath.addCircle(mCenterX, mCenterY, mOuterBgRadius, Path.Direction.CCW);
} else {
mBgCirclePath.addCircle(getHeight() / 2, getHeight() / 2, getHeight() / 2, Path.Direction.CCW);
}
// 裁剪画布,并设置其填充方式
canvas.clipPath(mBgCirclePath, Region.Op.REPLACE);
}
public void reset() {
mTimerStartMillis = 0;
isNeedCompleteQuickly = false;
setAngle(0.0f);
}
private long mTimerStartMillis = 0;
private long mDeltaTime = 0;
private float mStep1 = 180f / 10000f; // 前10秒每秒走5% 即18度
private float mStep2 = 175f / 20000f; // 10秒到30秒走175度
private void calculateAngle() {
if (mTimerStartMillis == 0) {
mTimerStartMillis = System.currentTimeMillis();
}
if (isNeedCompleteQuickly) {
if (mQuickStartAngle == 0) {
mTimerStartMillis = System.currentTimeMillis();
mQuickStartAngle = getAngle();
mNeedQuickCompleteAngle = mSmoothScrollTotalAngle - mQuickStartAngle;
if (mNeedQuickCompleteAngle < 0) {
mNeedQuickCompleteAngle = 0;
}
}
mDeltaTime = System.currentTimeMillis() - mTimerStartMillis;
if (mDeltaTime <= 2000) { // 快速结束时2秒走完剩下的
setAngle(mQuickStartAngle + mNeedQuickCompleteAngle / 2000f * mDeltaTime);
} else {
setAngle(mSmoothScrollTotalAngle);
}
} else {
mDeltaTime = System.currentTimeMillis() - mTimerStartMillis;
if (mDeltaTime <= 10000) { // 前10秒每秒走5% 即18度,总共走掉180度
setAngle(mStep1 * mDeltaTime);
} else if (mDeltaTime <= 30000f) { // 10秒到30秒走175度
setAngle(180f + (mStep2 * (mDeltaTime - 10000)));
}
}
}
private void doDraw(Canvas canvas) {
calculatePreValue();
canvas.drawColor(CLEAR_COLOR);
calculateAngle();
drawOuterGradientProgress(canvas);
drawInnerProgress(canvas);
drawPercentText(canvas);
}
private void calculatePreValue() {
if (mCenterX == 0) {
mCenterX = getWidth() / 2; // 获取圆心的x坐标
}
if (mCenterY == 0) {
mCenterY = getHeight() / 2;
}
if (mInnerArcLimitRect.isEmpty()) {
mInnerArcLimitRect.set(mCenterX - mInnerRadius, mCenterX - mInnerRadius, mCenterX + mInnerRadius, mCenterX + mInnerRadius);
}
}
private void drawOuterGradientProgress(final Canvas canvas) {
mProgressPaint.setStrokeWidth(mOuterRoundWidth); // 设置圆环的宽度
mProgressPaint.setColor(mRoundProgressColor); // 设置进度的颜色
mProgressPaint.setAntiAlias(true);
mProgressPaint.setStyle(Paint.Style.STROKE);
// 定义一个梯度渲染,由于梯度渲染是从三点钟方向开始,所以再让他逆时针旋转90°,从0点开始
if (mOuterSweepGradient == null) {
mOuterSweepGradient = new SweepGradient(mCenterX, mCenterY, mColors, null);
}
mOuterMatrix.setRotate((START_POINT_TOP + mOuterAngleProgressTotal), mCenterX, mCenterY);
mOuterSweepGradient.setLocalMatrix(mOuterMatrix);
mProgressPaint.setShader(mOuterSweepGradient);
canvas.drawCircle(mCenterX, mCenterY, mOuterRadius, mProgressPaint); // 画出圆环
drawOuterArcHeadCircle(canvas);
mOuterAngleProgressTotal += mOuterProgressStep;
if (mOuterAngleProgressTotal > TOTAL_ANGLE) {
mOuterAngleProgressTotal -= TOTAL_ANGLE;
}
}
private void drawOuterArcHeadCircle(final Canvas canvas) {
mProgressPaint.setShader(null);
mProgressPaint.setStrokeWidth(0);
mProgressPaint.setStyle(Paint.Style.FILL);
// 一开始从顶部开始旋转
mOuterHeadCircleAngleTotal = (HEAD_CIRCLE_START_ANGLE + mOuterAngleProgressTotal);
if (mOuterHeadCircleAngleTotal - TOTAL_ANGLE > 0) {
mOuterHeadCircleAngleTotal -= TOTAL_ANGLE;
}
// 根据旋转角度计算圆上当前位置点坐标,再以当前位置左边点位圆心画一个圆
mOuterHeadCircleAngleTotalMath = mOuterHeadCircleAngleTotal * Math.PI / 180f;
canvas.drawCircle((float) (mCenterX + mOuterRadius * Math.cos(mOuterHeadCircleAngleTotalMath)),
(float) (mCenterY + mOuterRadius * Math.sin(mOuterHeadCircleAngleTotalMath)),
mOuterHeadCircleWidth, mProgressPaint);
}
private void drawInnerProgress(final Canvas canvas) {
mProgressPaint.setStrokeWidth(mInnerRoundWidth); // 设置圆环的宽度
mProgressPaint.setColor(mRoundProgressColor); // 设置进度的颜色
mProgressPaint.setAntiAlias(true);
mProgressPaint.setStyle(Paint.Style.STROKE);
mProgressPaint.setShader(null);
canvas.drawArc(mInnerArcLimitRect, START_POINT_TOP, mInnerArcAngle, false, mProgressPaint);
}
private void drawPercentText(final Canvas canvas) {
mProgressPaint.setStrokeWidth(0);
mProgressPaint.setStyle(Paint.Style.FILL);
mProgressPaint.setColor(mPercentTextColor);
mProgressPaint.setTextSize(mPercentTextSize);
mProgressPaint.setTextAlign(Paint.Align.CENTER);
mProgressPaint.setTypeface(Typeface.DEFAULT_BOLD); // 设置字体
mProgressPaint.getTextBounds(mCurProgressStr, 0, mCurProgressStr.length(), mPercentTextBounds);
Paint.FontMetrics fontMetrics = mProgressPaint.getFontMetrics();
mFontBaseline = mCenterY - (fontMetrics.top + fontMetrics.bottom) / 2;
mProgressPaint.setTextSize(mPercentSignSize);
mProgressPaint.getTextBounds(mPercentSignStr, 0, mPercentSignStr.length(), mPercentSignBounds);
if (mCurProgress != 0) {
mProgressPaint.setTextSize(mPercentTextSize);
canvas.drawText(mCurProgressStr, mCenterX - (mSpaceTextAndSign + mPercentSignBounds.width()) / 2 ,
mFontBaseline, mProgressPaint); // 画出进度百分比
mProgressPaint.setTextSize(mPercentSignSize);
canvas.drawText(mPercentSignStr, mCenterX + (mSpaceTextAndSign + mPercentTextBounds.width()) / 2,
mFontBaseline, mProgressPaint); // 画出进度百分f符号
}
}
/**
* progress的范围 0~360
* @param angle
*/
public void setAngle(float angle) {
if (angle - TOTAL_ANGLE > 0) {
angle = TOTAL_ANGLE;
} else if (angle < 0) {
angle = 0;
}
mInnerArcAngle = angle;
int progress = (int) (mInnerArcAngle / TOTAL_ANGLE * PERCENT_BASE);
if (progress == mCurProgress) { // 相同进度不重复设置,避免notify重复通知
return;
}
if (progress < MIN_PROGRESS_DEFAULT) {
progress = MIN_PROGRESS_DEFAULT;
} else if (progress > MAX_PROGRESS_DEFAULT) {
progress = MAX_PROGRESS_DEFAULT;
}
mCurProgress = progress;
mCurProgressStr = "" + mCurProgress;
notifyProgressStateChangeListeners();
}
public float getAngle() {
return mInnerArcAngle;
}
public interface IProgressStateChangeListener {
/** 进度执行到100%时回调 */
void onFinished();
/** 执行到外部指定的进度回调 */
void onSmoothScrollFinish();
}
private void notifyProgressStateChangeListeners() {
if (mProgressStateChangeListeners == null) {
return;
}
if (mCurProgress == MAX_PROGRESS_DEFAULT) {
for (IProgressStateChangeListener listener : mProgressStateChangeListeners) {
if (listener != null) {
listener.onFinished();
}
}
return;
}
if (mCurProgress == mSmoothScrollToProgress) {
for (IProgressStateChangeListener listener : mProgressStateChangeListeners) {
if (listener != null) {
listener.onSmoothScrollFinish();
}
}
}
}
public void addProgressStateListener(IProgressStateChangeListener listener) {
mProgressStateChangeListeners.add(listener);
}
public void onDestroy() {
if (mProgressStateChangeListeners != null) {
mProgressStateChangeListeners.clear();
mProgressStateChangeListeners = null;
}
}
}