概述:在日常开发者自定义绘图是很常见的一个技能,有时候我们需要自己实现一个不是特别复杂的视图。则需要通过Android提供的API进行计算绘制等。本篇的目的就是通过简单的例子对Android绘图进行简单的认识。注意,本例中没有对其做任何封装,知识简单示范。

对于一般自定义View,我们都是新建一个类,然后继承自View。例如本例中ProgressView.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ProgressView extends View {
public ProgressView(Context context) {
super(context);
}
public ProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
}
}

上述代码是平时开发中再熟悉不过的了,然后我们在xml布局中使用它。这里我添加了一个背景色方便绘制的时候进行调试

1
2
3
4
5
<com.jinlin.progressview.ProgressView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ed7612"
android:layout_centerInParent="true"/>

那么我们接下来就在onDraw方法中进行绘制操作。首先要知道,绘制任何东西都是需要画笔对象的也就是我们的Paint,初始化工作代码如下

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
public class ProgressView extends View {
/**
* 圆形画笔宽度
*/
private float mProgressStrokeWidth = 5;
/**
* 文字画笔欢度
*/
private float mTextStrokeWidth = 3;
/**
* 最大进度
*/
private int mMaxProgress;
/**
* 当前进度
*/
private int mCurrentProgress;
/**
* 绘制区域
*/
private RectF mRectF;
/**
* 画笔对象
*/
private Paint mPaint;
/**
* 上下文对象
*/
private Context mContext;
/**
* 提示信息1
*/
private String mTxtHint1;
/**
* 提示信息2
*/
private String mTxtHint2;
public ProgressView(Context context) {
super(context);
init(context);
}
public ProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
mContext = context;
mPaint = new Paint();
mRectF = new RectF();
}
@Override
protected void onDraw(Canvas canvas) {
int width = this.getWidth();
int height = this.getHeight();
if (width != height) {
int min = Math.min(width, height);
width = min;
height = min;
}
// 画最底层
mPaint.setAntiAlias(true); // 设置抗锯齿
mPaint.setColor(Color.rgb(0xe9, 0xe9, 0xe9)); // 设置全圆弧颜色
mPaint.setStrokeWidth(mProgressStrokeWidth);
mPaint.setStyle(Style.STROKE);
canvas.drawColor(Color.TRANSPARENT);
mRectF.left = mRectF.top = mProgressStrokeWidth / 2;
mRectF.right = width - mProgressStrokeWidth / 2;
mRectF.bottom = height - mProgressStrokeWidth / 2;
canvas.drawArc(mRectF, 0, 360, false, mPaint);
// 画第二层
mPaint.setColor(getResources().getColor(R.color.colorPrimary));
canvas.drawArc(mRectF, 0 , 112, false, mPaint);
}
}

此时我们可以看到效果为

截图

我们看到此时起点是有右边开始,此时查看代码得知我们的起点是由0开始绘制的

1
canvas.drawArc(mRectF, 0 , 112, false, mPaint);

那么如果我们需要有顶部开始怎么做呢?我们可以知道顺时针为正,那么顶部为右边逆时针90°,如果换种思维,顶点也是顺时针270°。那我们试一试,代码说话

1
2
canvas.drawArc(mRectF, -90 , 112, false, mPaint);
canvas.drawArc(mRectF, 270 , 112, false, mPaint);

上面两端代码实现效果其实是一致的

截图

接下来就是绘制文字的部分,我们需要将进度文字绘制到视图的正中央,那么我们可以先把辅助中心线绘制出来。后期进入生产阶段可以去除调试代码部分或者将代码放入到if (isInEditMode())代码块里

1
2
3
4
5
6
7
if (isInEditMode()) {
// 绘制辅助线
mPaint.setStrokeWidth(1);
mPaint.setColor(Color.LTGRAY);
canvas.drawLine(0, width / 2, height , width /2 , mPaint);
canvas.drawLine(width / 2, 0, width / 2, height, mPaint);
}

由于Android中对文字的绘制时一个复杂的处理过程,所以这里不做详细说明,将单独哪一篇博文来说明文字应该如何绘制。

1
2
3
4
5
6
7
8
9
10
// 画进度文字
mPaint.setStrokeWidth(mTextStrokeWidth);
String text = 88 + "%";
int textHeight = height / 4;
mPaint.setTextSize(textHeight);
FontMetrics fontMetrics = mPaint.getFontMetrics();
float baseline = (mRectF.bottom + mRectF.top - fontMetrics.bottom - fontMetrics.top) / 2;
int textWidth = (int) mPaint.measureText(text, 0, text.length());
mPaint.setStyle(Style.FILL);
canvas.drawText(text, mRectF.centerX() - textWidth / 2, baseline, mPaint);

由效果图我们可以看见,文字已经居中了,接下来分别在视图的上三分之一和下三分之一处绘制提示信息,原理同样与上述代码类似

截图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (!TextUtils.isEmpty(mTxtHint1)) {
mPaint.setStrokeWidth(mTextStrokeWidth);
text = mTxtHint1;
textHeight = height / 8;
mPaint.setTextSize(textHeight);
fontMetrics = mPaint.getFontMetrics();
mPaint.setColor(Color.rgb(0x99, 0x99, 0x99));
textWidth = (int) mPaint.measureText(text, 0, text.length());
mPaint.setStyle(Style.FILL);
baseline = height / 4 - (fontMetrics.bottom + fontMetrics.top) / 2;
canvas.drawText(text, width / 2 - textWidth / 2, baseline, mPaint);
}
if (!TextUtils.isEmpty(mTxtHint2)) {
mPaint.setStrokeWidth(mTextStrokeWidth);
text = mTxtHint2;
textHeight = height / 8;
mPaint.setTextSize(textHeight);
fontMetrics = mPaint.getFontMetrics();
textWidth = (int) mPaint.measureText(text, 0, text.length());
mPaint.setStyle(Style.FILL);
baseline = 3 * height / 4 - (fontMetrics.bottom + fontMetrics.top) / 2;
canvas.drawText(text, width / 2 - textWidth / 2, baseline, mPaint);
}

最后我们来看效果图

截图

然后我们对部分属性设置访问方法,或者再新增一些自定义属性就可以完成一个简单的自定义View。例如

1
2
3
4
5
6
7
<declare-styleable name="ProgressView">
<attr name="progress" format="integer"/>
<attr name="textHint" format="string"/>
<attr name="hint" format="string"/>
<attr name="strokeColor" format="color" />
<attr name="progressColor" format="color" />
</declare-styleable>

然后在View中获取自定义属性,进行配置,完整代码如下

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
public class ProgressView extends View {
/**
* 圆形画笔宽度
*/
private float mProgressStrokeWidth = 5;
/**
* 文字画笔欢度
*/
private float mTextStrokeWidth = 3;
/**
* 最大进度
*/
private int mMaxProgress = 100;
/**
* 当前进度
*/
private int mCurrentProgress;
/**
* 绘制区域
*/
private RectF mRectF;
/**
* 画笔对象
*/
private Paint mPaint;
/**
* 进度条画笔
*/
private Paint mProgressPaint;
private TextPaint mTextPaint;
/**
* 提示信息1
*/
private String mHint;
/**
* 提示信息2
*/
private String mTextHint;
/**
* 默认线条颜色
*/
private int mStrokeColor;
/**
* 进度线条颜色
*/
private int mProgressColor;
public ProgressView(Context context) {
this(context, null);
}
public ProgressView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ProgressView);
try {
mCurrentProgress = ta.getInt(R.styleable.ProgressView_progress, 0);
if (mCurrentProgress > mMaxProgress) {
throw new RuntimeException("progress only define less than 100");
}
mHint = ta.getString(R.styleable.ProgressView_hint);
mTextHint = ta.getString(R.styleable.ProgressView_textHint);
mStrokeColor = ta.getColor(R.styleable.ProgressView_strokeColor, Color.rgb(0xe9, 0xe9, 0xe9));
mProgressColor = ta.getColor(R.styleable.ProgressView_progressColor, getResources().getColor(R.color.colorPrimary));
} finally {
ta.recycle();
}
mPaint = new Paint();
mRectF = new RectF();
}
@Override
protected void onDraw(Canvas canvas) {
int width = this.getWidth();
int height = this.getHeight();
Log.d("ProgressView", "width = " + width + " height = " + height);
if (width != height) {
int min = Math.min(width, height);
width = min;
height = min;
}
// 画最底层
mPaint.setAntiAlias(true); // 设置抗锯齿
mPaint.setColor(mStrokeColor); // 设置全圆弧颜色
mPaint.setStrokeWidth(mProgressStrokeWidth);
mPaint.setStyle(Style.STROKE);
mRectF.left = mRectF.top = mProgressStrokeWidth / 2;
mRectF.right = width - mProgressStrokeWidth / 2;
mRectF.bottom = height - mProgressStrokeWidth / 2;
canvas.drawArc(mRectF, 0, 360, false, mPaint);
// 画第二层
mPaint.setColor(mProgressColor);
canvas.drawArc(mRectF, 270, ((float) mCurrentProgress / 100) * 360, false, mPaint);
// 画进度文字
mPaint.setStrokeWidth(mTextStrokeWidth);
String text = mCurrentProgress + "%";
int textHeight = height / 5;
mPaint.setTextSize(textHeight);
FontMetrics fontMetrics = mPaint.getFontMetrics();
float baseline = (mRectF.bottom + mRectF.top - fontMetrics.bottom - fontMetrics.top) / 2;
int textWidth = (int) mPaint.measureText(text, 0, text.length());
mPaint.setStyle(Style.FILL);
canvas.drawText(text, mRectF.centerX() - textWidth / 2, baseline, mPaint);
if (!TextUtils.isEmpty(mHint)) {
mPaint.setStrokeWidth(mTextStrokeWidth);
textHeight = height / 8;
mPaint.setTextSize(textHeight);
fontMetrics = mPaint.getFontMetrics();
mPaint.setColor(Color.rgb(0x99, 0x99, 0x99));
textWidth = (int) mPaint.measureText(mHint, 0, mHint.length());
mPaint.setStyle(Style.FILL);
baseline = height / 4 - (fontMetrics.bottom + fontMetrics.top) / 2;
canvas.drawText(mHint, width / 2 - textWidth / 2, baseline, mPaint);
}
if (!TextUtils.isEmpty(mTextHint)) {
mPaint.setStrokeWidth(mTextStrokeWidth);
textHeight = height / 8;
mPaint.setTextSize(textHeight);
fontMetrics = mPaint.getFontMetrics();
textWidth = (int) mPaint.measureText(mTextHint, 0, mTextHint.length());
mPaint.setStyle(Style.FILL);
baseline = 3 * height / 4 - (fontMetrics.bottom + fontMetrics.top) / 2;
canvas.drawText(mTextHint, width / 2 - textWidth / 2, baseline, mPaint);
}
if (isInEditMode()) {
// 绘制辅助线
mPaint.setStrokeWidth(1);
mPaint.setColor(Color.LTGRAY);
canvas.drawLine(0, width / 2, width, width / 2, mPaint); // 横线 1/2
canvas.drawLine(0, height / 4, width, height / 4, mPaint); // 横线 1/4
canvas.drawLine(0, height / 4 * 3, width, height / 4 * 3, mPaint); // 横线 3/4
canvas.drawLine(width / 2, 0, width / 2, height, mPaint); // 竖线
}
}
public int getMaxProgress() {
return mMaxProgress;
}
public void setMaxProgress(int maxProgress) {
this.mMaxProgress = maxProgress;
}
public void setProgress(int progress) {
this.mCurrentProgress = progress;
this.invalidate();
}
public void setProgressNotInUIThread(int progress) {
this.mCurrentProgress = progress;
this.postInvalidate();
}
public int getProgress() {
return mCurrentProgress;
}
public String getHint() {
return mHint;
}
public void setHint(String hint) {
mHint = hint;
}
public String getTextHint() {
return mTextHint;
}
public void setTextHint(String textHint) {
mTextHint = textHint;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int desiredWidth = 250;
int desiredHeight = 250;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
//Measure Width
if (widthMode == MeasureSpec.EXACTLY) {
//Must be this size
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
//Can't be bigger than...
width = Math.min(desiredWidth, widthSize);
} else {
//Be whatever you want
width = desiredWidth;
}
//Measure Height
if (heightMode == MeasureSpec.EXACTLY) {
//Must be this size
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
//Can't be bigger than...
height = Math.min(desiredHeight, heightSize);
} else {
//Be whatever you want
height = desiredHeight;
}
//MUST CALL THIS
setMeasuredDimension(width, height);
}
}

看看我们最终的实现的效果图

截图