一.思路

自定义view最简单了,画两个圆环叠加,一个是用于底图,一个用于进度。
view里画圆的方法是canvas.drawArc

二.canvas.drawArc画扇形和圆环

canvas.drawArc是Canvas类提供的一个方法,用于绘制弧形或扇形。

方法的参数如下:

RectF oval:指定了弧形或扇形所在的矩形区域。矩形由左上角和右下角两个点确定。
float startAngle:指定了弧形或扇形的起始角度,以X轴正方向为0度,顺时针方向增加角度。
float sweepAngle:指定了弧形或扇形的角度范围。
boolean useCenter:表示是否连接弧形或扇形的起始点和终点,如果为true,则连接;如果为false,则不连接。

下面是一个示例代码,演示了如何使用canvas.drawArc()方法绘制一个扇形:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

// 创建一个矩形区域
RectF rectF = new RectF(100, 100, 500, 500);

// 设置画笔颜色为红色
Paint paint = new Paint();
paint.setColor(Color.RED);

// 绘制扇形
canvas.drawArc(rectF, 0, 120, true, paint);
}

效果图

在这个示例中,我们首先创建了一个矩形区域rectF,指定了扇形所在的位置和大小。然后,我们创建了一个画笔paint,并设置其颜色。最后,我们调用canvas.drawArc()方法,传入矩形区域、起始角度、角度范围和是否连接起始点和终点的参数,来绘制一个扇形。

以下是一个示例代码,演示了如何使用canvas.drawArc()方法绘制一个圆环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**画一个圆环*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

// 设置圆心的坐标和半径
float cx = getWidth() / 2f;
float cy = getHeight() / 2f;
float radius = Math.min(getWidth(), getHeight()) / 2f-20;

// 设置画笔颜色为红色
Paint paint = new Paint();
paint.setColor(Color.GREEN);
paint.setStyle(Paint.Style.STROKE); // 设置画笔样式为描边
paint.setStrokeWidth(10); // 设置描边宽度

// 绘制圆环
RectF rectF = new RectF(cx - radius, cy - radius, cx + radius, cy + radius);
canvas.drawArc(rectF, 0, 360, false, paint);
}

效果图

在这个示例中,我们首先根据View的宽度和高度计算出圆心的坐标和半径。然后,我们创建了一个画笔paint,并设置其颜色,样式为描边,描边宽度为10。最后,我们调用canvas.drawArc()方法,传入矩形区域、起始角度、角度范围和画笔,来绘制一个圆环。

三.canvas.drawArc画两个重叠的圆环

简单使用后就可以开始画重叠的圆环了
首先设置一点自定义属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 获取自定义属性
* max:最大进度值,默认为100。
* progress:当前进度值,默认为0。
* startAngle:起始角度,默认为270度(即12点钟方向)。
* strokeWidth:进度条宽度,默认为10dp。
* backgroundColor:背景颜色,默认为灰色。
* progressColor:进度条颜色,默认为蓝色。
**/
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CircleSeekBar);
mMax = ta.getInt(R.styleable.CircleSeekBar_max, 100);
mProgress = ta.getInt(R.styleable.CircleSeekBar_progress, 0);
mStartAngle = ta.getInt(R.styleable.CircleSeekBar_startAngle, 270);
mStartAngle = ta.getInt(R.styleable.CircleSeekBar_maxAngle, 360);
mStrokeWidth = ta.getDimensionPixelSize(R.styleable.CircleSeekBar_strokeWidth, 10);
mBackgroundColor = ta.getColor(R.styleable.CircleSeekBar_backgroundColor, Color.GRAY);
mProgressColor = ta.getColor(R.styleable.CircleSeekBar_progressColor, Color.GREEN);

然后开画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

// 绘制背景
mPaint.setColor(mBackgroundColor);
mPaint.setStrokeWidth(mStrokeWidth);
canvas.drawArc(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(),
getHeight() - getPaddingBottom(), mStartAngle, 360, false, mPaint);

// 绘制进度条
mPaint.setColor(mProgressColor);
float sweepAngle = (float) mProgress / mMax * 360;
canvas.drawArc(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(),
getHeight() - getPaddingBottom(), mStartAngle, sweepAngle, false, mPaint);
}

使用时,在布局文件中添加以下代码:

1
2
3
4
5
6
7
8
9
10
<com.example.CircleSeekBar
android:id="@+id/circle_seekbar"
android:layout_width="200dp"
android:layout_height="200dp"
app:backgroundColor="#CCCCCC"
app:max="100"
app:progress="50"
app:progressColor="#FF0000"
app:startAngle="270"
app:strokeWidth="20dp" />

Activity代码中,可以通过以下方式来设置圆形进度条的参数:

1
2
3
4
5
6
7
CircleSeekBar seekBar = findViewById(R.id.circle_seekbar);
seekBar.setMax(100);
seekBar.setProgress(50);
seekBar.setStartAngle(270);
seekBar.setStrokeWidth(20);
seekBar.setBackgroundColor(Color.GRAY);
seekBar.setProgressColor(Color.RED);

效果

四.圆环加渐变

要实现圆环进度的渐变效果,可以使用Android提供的Shader类来创建渐变。
Shader是一个抽象类,它定义了一种颜色渲染的方式,可以用来实现各种颜色效果,包括线性渐变、径向渐变、扫描渐变等。
在这里,我们使用线性渐变来实现圆环进度的渐变效果。

具体实现方法如下:

在CircleSeekBar的构造方法中创建一个Paint对象,用来绘制圆环进度。

1
2
3
4
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(mStrokeWidth);

在onDraw()方法中,使用Shader类创建一个线性渐变对象,并设置给Paint的Shader属性。

1
2
3
4
int[] colors = {Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE, Color.MAGENTA, Color.RED};
float[] positions = {0, 0.15f, 0.3f, 0.45f, 0.6f, 0.75f, 1};
Shader shader = new SweepGradient(getWidth() / 2, getHeight() / 2, colors, positions);
mPaint.setShader(shader);

这里使用了SweepGradient类来创建一个扫描渐变对象,它的构造方法接受4个参数:渐变中心点的x坐标、y坐标,渐变颜色数组和颜色位置数组。这里我们使用了7种颜色,分别在圆环进度的不同位置上显示,形成一个渐变的效果。

最后在onDraw()方法中使用canvas.drawArc()方法来绘制圆环进度。

1
canvas.drawArc(rectF, mStartAngle, sweepAngle, false, mPaint);

效果

五.最终

为方便看交互效果,我加了一个原生的seekbar,用于控制圆环进度。
最终效果图:

CircleSeekBar完整代码

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
package com.tw.customviews.yuanhuan;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Shader;
import android.graphics.SweepGradient;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import com.tw.customviews.R;

public class CircleSeekBar extends View {

private Paint mPaint;
private int mProgress = 0;
private int mMax = 80;
private int mStartAngle = 270;
private int mMaxAngle = 270;//默认360
private int mStrokeWidth = 10;
private int mBackgroundColor = Color.GRAY;
private int mProgressColor = Color.GREEN;


public CircleSeekBar(Context context) {
this(context, null);
}

public CircleSeekBar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public CircleSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);

// 初始化画笔
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeCap(Paint.Cap.ROUND);

/**
* 获取自定义属性
* max:最大进度值,默认为100。
* progress:当前进度值,默认为0。
* startAngle:起始角度,默认为270度(即12点钟方向)。
* strokeWidth:进度条宽度,默认为10dp。
* backgroundColor:背景颜色,默认为灰色。
* progressColor:进度条颜色,默认为蓝色。
**/
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CircleSeekBar);
mMax = ta.getInt(R.styleable.CircleSeekBar_max, 100);
mProgress = ta.getInt(R.styleable.CircleSeekBar_progress, 0);
mStartAngle = ta.getInt(R.styleable.CircleSeekBar_startAngle, 270);
mStartAngle = ta.getInt(R.styleable.CircleSeekBar_maxAngle, 360);
mStrokeWidth = ta.getDimensionPixelSize(R.styleable.CircleSeekBar_strokeWidth, 10);
mBackgroundColor = ta.getColor(R.styleable.CircleSeekBar_backgroundColor, Color.GRAY);
mProgressColor = ta.getColor(R.styleable.CircleSeekBar_progressColor, Color.GREEN);
ta.recycle();
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

// 绘制背景圆环
mPaint.setShader(null);
mPaint.setColor(mBackgroundColor);
mPaint.setStrokeWidth(mStrokeWidth);
canvas.drawArc(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(),
getHeight() - getPaddingBottom(), mStartAngle, 360, false, mPaint);

// 绘制进度条
int[] colors = {Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE, Color.MAGENTA, Color.RED};
float[] positions = {0, 0.15f, 0.3f, 0.45f, 0.6f, 0.75f, 1};
Shader shader = new SweepGradient(getWidth() / 2, getHeight() / 2, colors, positions);
mPaint.setShader(shader);
mPaint.setColor(mProgressColor);
float sweepAngle = (float) mProgress / mMax * mMaxAngle;
canvas.drawArc(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(),
getHeight() - getPaddingBottom(), mStartAngle, sweepAngle, false, mPaint);

}

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
// 计算当前进度值
float x = event.getX() - getWidth() / 2;
float y = event.getY() - getHeight() / 2;
double angle = Math.toDegrees(Math.atan2(y, x));
angle = angle < 0 ? angle + 360 : angle;
mProgress = (int) (angle / 360 * mMax);
invalidate();
return true;
}

return super.onTouchEvent(event);
}

public void setMax(int max) {
mMax = max;
invalidate();
}

public void setProgress(int progress) {
mProgress = progress;
invalidate();
}

public void setStartAngle(int startAngle) {
mStartAngle = startAngle;
invalidate();
}
public void setMaxAngle(int maxAngle) {
mMaxAngle = maxAngle;
invalidate();
}

public void setStrokeWidth(int strokeWidth) {
mStrokeWidth = strokeWidth;
invalidate();
}

public void setBackgroundColor(int backgroundColor) {
mBackgroundColor = backgroundColor;
invalidate();
}

public void setProgressColor(int progressColor) {
mProgressColor = progressColor;
invalidate();
}
}