Android實現炫酷的CheckBox效果

首先貼出實現的效果圖:

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 {
相關文章
相關標籤/搜索