首先貼出實現的效果圖:
gif的效果可能有點過快,在真機上運行的效果會更好一些。我們主要的思路就是利用屬性動畫來動態地畫出選中狀態以及對勾的繪製過程。看到上面的效果圖,相信大家都迫不及待地要躍躍欲試了,那就讓我們開始吧。
自定義View的第一步:自定義屬性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<?xml version=
"1.0"
encoding=
"utf-8"
?>
<resources>
<declare-styleable name=
"SmoothCheckBox"
>
<!-- 動畫持續時間 -->
<attr name=
"duration"
format=
"integer"
></attr>
<!-- 邊框寬度 -->
<attr name=
"strikeWidth"
format=
"dimension|reference"
></attr>
<!-- 邊框顏色 -->
<attr name=
"borderColor"
format=
"color|reference"
></attr>
<!-- 選中狀態的顏色 -->
<attr name=
"trimColor"
format=
"color|reference"
></attr>
<!-- 對勾顏色 -->
<attr name=
"tickColor"
format=
"color|reference"
></attr>
<!-- 對勾寬度 -->
<attr name=
"tickWidth"
format=
"dimension|reference"
></attr>
</declare-styleable>
</resources>
|
我們把CheckBox取名爲SmoothCheckBox,定義了幾個等等要用到的屬性。這一步很簡單,相信大家都熟練了。
接下來看一看onMeasure(int widthMeasureSpec, int heightMeasureSpec):
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
|
@Override
protected
void
onMeasure(
int
widthMeasureSpec,
int
heightMeasureSpec) {
super
.onMeasure(widthMeasureSpec, heightMeasureSpec);
int
widthSize = MeasureSpec.getSize(widthMeasureSpec);
int
widthMode = MeasureSpec.getMode(widthMeasureSpec);
if
(widthMode == MeasureSpec.EXACTLY) {
mWidth = widthSize;
}
else
{
mWidth =
40
;
}
int
heightSize = MeasureSpec.getSize(heightMeasureSpec);
int
heightMode = MeasureSpec.getMode(heightMeasureSpec);
if
(heightMode == MeasureSpec.EXACTLY) {
mHeight = heightSize;
}
else
{
mHeight =
40
;
}
setMeasuredDimension(mWidth, mHeight);
int
size = Math.min(mWidth, mHeight);
center = size /
2
;
mRadius = (
int
) ((size - mStrokeWidth) /
2
/
1
.2f);
startPoint.set(center *
14
/
30
, center *
28
/
30
);
breakPoint.set(center *
26
/
30
, center *
40
/
30
);
endPoint.set(center *
44
/
30
, center *
20
/
30
);
downLength = (
float
) Math.sqrt(Math.pow(startPoint.x - breakPoint.x, 2f) + Math.pow(startPoint.y - breakPoint.y, 2f));
upLength = (
float
) Math.sqrt(Math.pow(endPoint.x - breakPoint.x, 2f) + Math.pow(endPoint.y - breakPoint.y, 2f));
totalLength = downLength + upLength;
}
|
一開始是測量了SmoothCheckBox的寬、高度,默認的寬高度隨便定義了一個,當然你們可以自己去修改和完善它。然後就是設置半徑之類的,最後的startPoint、breakPoint、endPoint分別對應着選中時對勾的三個點(至於爲何是這幾個數字,那完全是經驗值);downLength就是startPoint和breakPoint的距離,而相對應的upLength就是breakPoint和endPoint的距離。即以下圖示:
在看onDraw(Canvas canvas)之前我們先來看兩組動畫,分別是選中狀態時的動畫以及未選中狀態的動畫:
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
|
// 由未選中到選中的動畫
private
void
checkedAnimation() {
animatedValue = 0f;
tickValue = 0f;
// 選中時底色的動畫
mValueAnimator = ValueAnimator.ofFloat(0f,
1
.2f, 1f).setDuration(
2
* duration /
5
);
mValueAnimator.setInterpolator(
new
AccelerateDecelerateInterpolator());
// 對勾的動畫
mTickValueAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(
3
* duration /
5
);
mTickValueAnimator.setInterpolator(
new
LinearInterpolator());
mTickValueAnimator.addUpdateListener(
new
ValueAnimator.AnimatorUpdateListener() {
@Override
public
void
onAnimationUpdate(ValueAnimator valueAnimator) {
// 得到動畫執行進度
tickValue = (
float
) valueAnimator.getAnimatedValue();
postInvalidate();
}
});
mValueAnimator.addUpdateListener(
new
ValueAnimator.AnimatorUpdateListener() {
@Override
public
void
onAnimationUpdate(ValueAnimator valueAnimator) {
// 得到動畫執行進度
animatedValue = (
float
) valueAnimator.getAnimatedValue();
postInvalidate();
}
});
mValueAnimator.addListener(
new
AnimatorListenerAdapter() {
@Override
public
void
onAnimationEnd(Animator animation) {
//當底色的動畫完成後再開始對勾的動畫
mTickValueAnimator.start();
Log.i(TAG,
" mTickValueAnimator.start();"
);
}
});
mValueAnimator.start();
}
// 由選中到未選中的動畫
private
void
uncheckedAnimation() {
animatedValue = 0f;
mValueAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(
2
* duration /
5
);
mValueAnimator.setInterpolator(
new
AccelerateInterpolator());
mValueAnimator.addUpdateListener(
new
ValueAnimator.AnimatorUpdateListener() {
@Override
public
void
onAnimationUpdate(ValueAnimator valueAnimator) {
animatedValue = (
float
) valueAnimator.getAnimatedValue();
postInvalidate();
}
});
mValueAnimator.start();
}
|
這兩組動畫在點擊SmoothCheckBox的時候會調用。相似的,都是在動畫執行中得到動畫執行的進度,再來調用postInvalidate();讓SmoothCheckBox重繪。看完這個之後就是終極大招onDraw(Canvas canvas)了:
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
|
@Override
protected
void
onDraw(Canvas canvas) {
super
.onDraw(canvas);
canvas.save();
drawBorder(canvas);
drawTrim(canvas);
if
(isChecked) {
drawTick(canvas);
}
canvas.restore();
}
// 畫對勾
private
void
drawTick(Canvas canvas) {
// 得到畫對勾的進度
float
temp = tickValue * totalLength;
Log.i(TAG,
"temp:"
+ temp +
"downlength :"
+ downLength);
//判斷是否是剛開始畫對勾的時候,即等於startPoint
if
(Float.compare(tickValue, 0f) ==
0
) {
Log.i(TAG,
"startPoint : "
+ startPoint.x +
", "
+ startPoint.y);
path.reset();
path.moveTo(startPoint.x, startPoint.y);
}
// 如果畫對勾的進度已經超過breakPoint的時候,即(breakPoint,endPoint]
if
(temp > downLength) {
path.moveTo(startPoint.x, startPoint.y);
path.lineTo(breakPoint.x, breakPoint.y);
Log.i(TAG,
"endPoint : "
+ endPoint.x +
", "
+ endPoint.y);
path.lineTo((endPoint.x - breakPoint.x) * (temp - downLength) / upLength + breakPoint.x, (endPoint.y - breakPoint.y) * (temp - downLength) / upLength + breakPoint.y);
}
else
{
//畫對勾的進度介於startPoinit和breakPoint之間,即(startPoint,breakPoint]
Log.i(TAG,
"down x : "
+ (breakPoint.x - startPoint.x) * temp / downLength +
",down y: "
+ (breakPoint.y - startPoint.y) * temp / downLength);
path.lineTo((breakPoint.x - startPoint.x) * temp / downLength + startPoint.x, (breakPoint.y - startPoint.y) * temp / downLength + startPoint.y);
}
canvas.drawPath(path, tickPaint);
}
// 畫邊框
private
void
drawBorder(Canvas canvas) {
float
temp;
// 通過animatedValue讓邊框產生一個「OverShooting」的動畫
if
(animatedValue > 1f) {
temp = animatedValue * mRadius;
}
else
{
temp = mRadius;
}
canvas.drawCircle(center, center, temp, borderPaint);
}
// 畫checkbox內部
private
void
drawTrim(Canvas canvas) {
canvas.drawCircle(center, center, (mRadius - mStrokeWidth) * animatedValue, trimPaint);
}
|
onDraw(Canvas canvas)代碼中的邏輯基本都加了註釋,主要就是原理搞懂了就比較簡單了。在繪製對勾時要區分當前處於繪製對勾的哪種狀態,然後對應做處理畫出線條,剩下的就簡單了。關於SmoothCheckBox的講解到這裏就差不多了。
下面就貼出SmoothCheckBox的完整代碼:
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
|
public
class
SmoothCheckBox
extends
View
implements
View.OnClickListener {
// 動畫持續時間
private
long
duration;
// 邊框寬度
private
float
mStrokeWidth;
// 對勾寬度
private
float
mTickWidth;
// 內飾畫筆
private
Paint trimPaint;
// 邊框畫筆
private
Paint borderPaint;
// 對勾畫筆
private
Paint tickPaint;
// 默認邊框寬度
private
float
defaultStrikeWidth;
// 默認對勾寬度
private
float
defaultTickWidth;
// 寬度
private
int
mWidth;
// 高度
private
int
mHeight;
// 邊框顏色
private
int
borderColor;
// 內飾顏色
private
int
trimColor;
// 對勾顏色
private
int
tickColor;
// 半徑
private
int
mRadius;
// 中心點
private
int
center;
// 是否是選中
private
boolean
isChecked;
//對勾向下的長度
private
float
downLength;
//對勾向上的長度
private
float
upLength;
// 對勾的總長度
private
float
totalLength;
// 監聽器
private
OnCheckedChangeListener listener;
private
ValueAnimator mValueAnimator;
private
ValueAnimator mTickValueAnimator;
private
float
animatedValue;
private
float
tickValue;
// 對勾開始點
private
Point startPoint =
new
Point();
// 對勾轉折點
private
Point breakPoint =
new
Point();
// 對勾結束點
private
Point endPoint =
new
Point();
private
static
final
String TAG =
"SmoothCheckBox"
;
private
static
final
String KEY_INSTANCE_STATE =
"InstanceState"
;
private
Path path =
new
Path();
public
void
setOnCheckedChangeListener(OnCheckedChangeListener listener) {
this
.listener = listener;
}
public
SmoothCheckBox(Context context) {
this
(context,
null
);
}
public
SmoothCheckBox(Context context, AttributeSet attrs) {
this
(context, attrs,
0
);
}
public
SmoothCheckBox(Context context, AttributeSet attrs,
int
defStyleAttr) {
super
(context, attrs, defStyleAttr);
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.SmoothCheckBox);
duration = a.getInt(R.styleable.SmoothCheckBox_duration,
600
);
defaultStrikeWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
1
, getResources().getDisplayMetrics());
mStrokeWidth = a.getDimension(R.styleable.SmoothCheckBox_strikeWidth, defaultStrikeWidth);
defaultTickWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
2
, getResources().getDisplayMetrics());
mTickWidth = a.getDimension(R.styleable.SmoothCheckBox_tickWidth, defaultTickWidth);
borderColor = a.getColor(R.styleable.SmoothCheckBox_borderColor, getResources().getColor(android.R.color.darker_gray));
trimColor = a.getColor(R.styleable.SmoothCheckBox_trimColor, getResources().getColor(android.R.color.holo_green_light));
tickColor = a.getColor(R.styleable.SmoothCheckBox_tickColor, getResources().getColor(android.R.color.white));
a.recycle();
trimPaint =
new
Paint(Paint.ANTI_ALIAS_FLAG);
trimPaint.setStyle(Paint.Style.FILL);
trimPaint.setColor(trimColor);
borderPaint =
new
Paint(Paint.ANTI_ALIAS_FLAG);
borderPaint.setStrokeWidth(mStrokeWidth);
borderPaint.setColor(borderColor);
borderPaint.setStyle(Paint.Style.STROKE);
tickPaint =
new
Paint(Paint.ANTI_ALIAS_FLAG);
tickPaint.setColor(tickColor);
tickPaint.setStyle(Paint.Style.STROKE);
tickPaint.setStrokeCap(Paint.Cap.ROUND);
tickPaint.setStrokeWidth(mTickWidth);
setOnClickListener(
this
);
}
@Override
protected
void
onMeasure(
int
widthMeasureSpec,
int
heightMeasureSpec) {
super
.onMeasure(widthMeasureSpec, heightMeasureSpec);
int
widthSize = MeasureSpec.getSize(widthMeasureSpec);
int
widthMode = MeasureSpec.getMode(widthMeasureSpec);
if
(widthMode == MeasureSpec.EXACTLY) {
mWidth = widthSize;
}
else
{
mWidth =
40
;
}
int
heightSize = MeasureSpec.getSize(heightMeasureSpec);
int
heightMode = MeasureSpec.getMode(heightMeasureSpec);
if
(heightMode == MeasureSpec.EXACTLY) {
mHeight = heightSize;
}
else
{
|