前言
Android 自定義 View 技能是成為高級工程師所必備的,筆者覺得自定義 View 沒有什么捷徑可走,唯有經常練習才能解決產品需求。筆者也好久沒有寫自定義 View 了,趕緊寫個控件找點感覺回來。
本文實現的是一個 鎖屏圖案的自定義控件。效果圖如下:
Github 地址:AndroidSample
LockView 介紹
自定義屬性:
引用方式:
(1) 在布局文件中引入
1
2
3
4
5
6
7
8
9
|
<com.xing.androidsample.view.LockView android:id= "@+id/lock_view" app:rowCount= "4" app:normalColor= "" app:moveColor= "" app:errorColor= "" android:layout_width= "match_parent" android:layout_height= "match_parent" android:layout_margin= "40dp" /> |
(2) 在代碼中設置正確的圖案,用于校驗是否匹配成功,并在回調中獲取結果
1
2
3
4
5
6
7
8
9
10
11
12
|
List<Integer> intList = new ArrayList<>(); intList.add( 3 ); intList.add( 7 ); intList.add( 4 ); intList.add( 2 ); lockView.setStandard(intList); lockView.setOnDrawCompleteListener( new LockView.OnDrawCompleteListener() { @Override public void onComplete( boolean isSuccess) { Toast.makeText(CustomViewActivity. this , isSuccess ? "success" : "fail" , Toast.LENGTH_SHORT).show(); } }); |
實現思路
- 以默認狀態繪制 rowCount * rowCount 個圓,外圓顏色需要在內圓顏色上加上一定的透明度。
- 在 onTouchEvent() 方法中,判斷當前觸摸點與各個圓的圓心距離是否小于圓的半徑,決定各個圓此時處于哪個狀態(normal,move,error),調用 invalidate() 重新繪制,更新顏色。
- ?將手指滑動觸摸過的圓的坐標添加到一個 ArrayList 中,使用 Path 連接該集合中選中的圓,即可繪制出劃過的路徑線。
實現步驟
自定義屬性
在 res/values 目錄下新建 attrs.xml 文件:
1
2
3
4
5
6
7
8
9
|
<? xml version = "1.0" encoding = "utf-8" ?> < resources > < declare-styleable name = "LockView" > < attr name = "normalColor" format = "color|reference" /> <!--默認圓顏色--> < attr name = "moveColor" format = "color|reference" /> <!--手指觸摸選中圓顏色--> < attr name = "errorColor" format = "color|reference" /> <!--手指抬起錯誤圓顏色--> < attr name = "rowCount" format = "integer" /> <!--每行每列圓的個數--> </ declare-styleable > </ resources > |
獲取自定義屬性
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
|
public LockView(Context context) { this (context, null ); } public LockView(Context context, @Nullable AttributeSet attrs) { super (context, attrs); readAttrs(context, attrs); init(); } /** * 獲取自定義屬性 */ private void readAttrs(Context context, AttributeSet attrs) { TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LockView); normalColor = typedArray.getColor(R.styleable.LockView_normalColor, DEFAULT_NORMAL_COLOR); moveColor = typedArray.getColor(R.styleable.LockView_moveColor, DEFAULT_MOVE_COLOR); errorColor = typedArray.getColor(R.styleable.LockView_errorColor, DEFAULT_ERROR_COLOR); rowCount = typedArray.getInteger(R.styleable.LockView_rowCount, DEFAULT_ROW_COUNT); typedArray.recycle(); } /** * 初始化 */ private void init() { stateSparseArray = new SparseIntArray(rowCount * rowCount); points = new PointF[rowCount * rowCount]; innerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); innerCirclePaint.setStyle(Paint.Style.FILL); outerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); outerCirclePaint.setStyle(Paint.Style.FILL); linePaint = new Paint(Paint.ANTI_ALIAS_FLAG); linePaint.setStyle(Paint.Style.STROKE); linePaint.setStrokeCap(Paint.Cap.ROUND); linePaint.setStrokeJoin(Paint.Join.ROUND); linePaint.setStrokeWidth( 30 ); linePaint.setColor(moveColor); } |
計算圓的半徑
設定外圓半徑和相鄰兩圓之間間距相同,內圓半徑是外圓半徑的一半,所以半徑計算方式為:
1
|
radius = Math.min(w, h) / ( 2 * rowCount + rowCount - 1 ) * 1 .0f; |
設置各圓坐標
各圓坐標使用一維數組保存,計算方式為:
1
2
3
4
5
|
// 各個圓設置坐標點 for ( int i = 0 ; i < rowCount * rowCount; i++) { points[i] = new PointF( 0 , 0 ); points[i].set((i % rowCount * 3 + 1 ) * radius, (i / rowCount * 3 + 1 ) * radius); } |
測量 View 寬高
根據測量模式設置控件的寬高,當布局文件中設置的是 wrap_content ,默認將控件寬高設置為 600dp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@Override protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) { super .onMeasure(widthMeasureSpec, heightMeasureSpec); int width = getSize(widthMeasureSpec); int height = getSize(heightMeasureSpec); setMeasuredDimension(width, height); } private int getSize( int measureSpec) { int mode = MeasureSpec.getMode(measureSpec); int size = MeasureSpec.getSize(measureSpec); if (mode == MeasureSpec.EXACTLY) { return size; } else if (mode == MeasureSpec.AT_MOST) { return Math.min(size, dp2Px( 600 )); } return dp2Px( 600 ); } |
onTouchEvent() 觸摸事件
在手指滑動過程中,根據當前觸摸點坐標是否落在圓的范圍內,更新該圓的狀態,在重新繪制時,繪制成新的顏色。手指抬起時,將存放狀態的 list,選中圓的 list ,linePath 重置,并將結果回調出來。
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
|
private PointF touchPoint; @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: reset(); case MotionEvent.ACTION_MOVE: if (touchPoint == null ) { touchPoint = new PointF(event.getX(), event.getY()); } else { touchPoint.set(event.getX(), event.getY()); } for ( int i = 0 ; i < rowCount * rowCount; i++) { // 是否觸摸在圓的范圍內 if (getDistance(touchPoint, points[i]) < radius) { stateSparseArray.put(i, STATE_MOVE); if (!selectedList.contains(points[i])) { selectedList.add(points[i]); } break ; } } break ; case MotionEvent.ACTION_UP: if (check()) { // 正確圖案 if (listener != null ) { listener.onComplete( true ); } for ( int i = 0 ; i < stateSparseArray.size(); i++) { int index = stateSparseArray.keyAt(i); stateSparseArray.put(index, STATE_MOVE); } } else { // 錯誤圖案 for ( int i = 0 ; i < stateSparseArray.size(); i++) { int index = stateSparseArray.keyAt(i); stateSparseArray.put(index, STATE_ERROR); } linePaint.setColor( 0xeeff0000 ); if (listener != null ) { listener.onComplete( false ); } } touchPoint = null ; if (timer == null ) { timer = new Timer(); } timer.schedule( new TimerTask() { @Override public void run() { linePath.reset(); linePaint.setColor( 0xee0000ff ); selectedList.clear(); stateSparseArray.clear(); postInvalidate(); } }, 1000 ); break ; } invalidate(); return true ; } |
繪制各圓和各圓之間連接線段
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
|
@Override protected void onDraw(Canvas canvas) { super .onDraw(canvas); drawCircle(canvas); drawLinePath(canvas); } private void drawCircle(Canvas canvas) { // 依次從索引 0 到索引 8,根據不同狀態繪制圓點 for ( int index = 0 ; index < rowCount * rowCount; index++) { int state = stateSparseArray.get(index); switch (state) { case STATE_NORMAL: innerCirclePaint.setColor(normalColor); outerCirclePaint.setColor(normalColor & 0x66ffffff ); break ; case STATE_MOVE: innerCirclePaint.setColor(moveColor); outerCirclePaint.setColor(moveColor & 0x66ffffff ); break ; case STATE_ERROR: innerCirclePaint.setColor(errorColor); outerCirclePaint.setColor(errorColor & 0x66ffffff ); break ; } canvas.drawCircle(points[index].x, points[index].y, radius, outerCirclePaint); canvas.drawCircle(points[index].x, points[index].y, radius / 2f, innerCirclePaint); } } |
完整 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
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
|
/** * Created by star.tao on 2018/5/30. * email: [email protected] * github: https://github.com/xing16 */ public class LockView extends View { private static final int DEFAULT_NORMAL_COLOR = 0xee776666 ; private static final int DEFAULT_MOVE_COLOR = 0xee0000ff ; private static final int DEFAULT_ERROR_COLOR = 0xeeff0000 ; private static final int DEFAULT_ROW_COUNT = 3 ; private static final int STATE_NORMAL = 0 ; private static final int STATE_MOVE = 1 ; private static final int STATE_ERROR = 2 ; private int normalColor; // 無滑動默認顏色 private int moveColor; // 滑動選中顏色 private int errorColor; // 錯誤顏色 private float radius; // 外圓半徑 private int rowCount; private PointF[] points; // 一維數組記錄所有圓點的坐標點 private Paint innerCirclePaint; // 內圓畫筆 private Paint outerCirclePaint; // 外圓畫筆 private SparseIntArray stateSparseArray; private List<PointF> selectedList = new ArrayList<>(); private List<Integer> standardPointsIndexList = new ArrayList<>(); private Path linePath = new Path(); // 手指移動的路徑 private Paint linePaint; private Timer timer; public LockView(Context context) { this (context, null ); } public LockView(Context context, @Nullable AttributeSet attrs) { super (context, attrs); readAttrs(context, attrs); init(); } private void readAttrs(Context context, AttributeSet attrs) { TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LockView); normalColor = typedArray.getColor(R.styleable.LockView_normalColor, DEFAULT_NORMAL_COLOR); moveColor = typedArray.getColor(R.styleable.LockView_moveColor, DEFAULT_MOVE_COLOR); errorColor = typedArray.getColor(R.styleable.LockView_errorColor, DEFAULT_ERROR_COLOR); rowCount = typedArray.getInteger(R.styleable.LockView_rowCount, DEFAULT_ROW_COUNT); typedArray.recycle(); } private void init() { stateSparseArray = new SparseIntArray(rowCount * rowCount); points = new PointF[rowCount * rowCount]; innerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); innerCirclePaint.setStyle(Paint.Style.FILL); outerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); outerCirclePaint.setStyle(Paint.Style.FILL); linePaint = new Paint(Paint.ANTI_ALIAS_FLAG); linePaint.setStyle(Paint.Style.STROKE); linePaint.setStrokeCap(Paint.Cap.ROUND); linePaint.setStrokeJoin(Paint.Join.ROUND); linePaint.setStrokeWidth( 30 ); linePaint.setColor(moveColor); } @Override protected void onSizeChanged( int w, int h, int oldw, int oldh) { super .onSizeChanged(w, h, oldw, oldh); // 外圓半徑 = 相鄰外圓之間間距 = 2倍內圓半徑 radius = Math.min(w, h) / ( 2 * rowCount + rowCount - 1 ) * 1 .0f; // 各個圓設置坐標點 for ( int i = 0 ; i < rowCount * rowCount; i++) { points[i] = new PointF( 0 , 0 ); points[i].set((i % rowCount * 3 + 1 ) * radius, (i / rowCount * 3 + 1 ) * radius); } } @Override protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) { super .onMeasure(widthMeasureSpec, heightMeasureSpec); int width = getSize(widthMeasureSpec); int height = getSize(heightMeasureSpec); setMeasuredDimension(width, height); } private int getSize( int measureSpec) { int mode = MeasureSpec.getMode(measureSpec); int size = MeasureSpec.getSize(measureSpec); if (mode == MeasureSpec.EXACTLY) { return size; } else if (mode == MeasureSpec.AT_MOST) { return Math.min(size, dp2Px( 600 )); } return dp2Px( 600 ); } @Override protected void onDraw(Canvas canvas) { super .onDraw(canvas); drawCircle(canvas); drawLinePath(canvas); } private void drawCircle(Canvas canvas) { // 依次從索引 0 到索引 8,根據不同狀態繪制圓點 for ( int index = 0 ; index < rowCount * rowCount; index++) { int state = stateSparseArray.get(index); switch (state) { case STATE_NORMAL: innerCirclePaint.setColor(normalColor); outerCirclePaint.setColor(normalColor & 0x66ffffff ); break ; case STATE_MOVE: innerCirclePaint.setColor(moveColor); outerCirclePaint.setColor(moveColor & 0x66ffffff ); break ; case STATE_ERROR: innerCirclePaint.setColor(errorColor); outerCirclePaint.setColor(errorColor & 0x66ffffff ); break ; } canvas.drawCircle(points[index].x, points[index].y, radius, outerCirclePaint); canvas.drawCircle(points[index].x, points[index].y, radius / 2f, innerCirclePaint); } } /** * 繪制選中點之間相連的路徑 * * @param canvas */ private void drawLinePath(Canvas canvas) { // 重置linePath linePath.reset(); // 選中點個數大于 0 時,才繪制連接線段 if (selectedList.size() > 0 ) { // 起點移動到按下點位置 linePath.moveTo(selectedList.get( 0 ).x, selectedList.get( 0 ).y); for ( int i = 1 ; i < selectedList.size(); i++) { linePath.lineTo(selectedList.get(i).x, selectedList.get(i).y); } // 手指抬起時,touchPoint設置為null,使得已經繪制游離的路徑,消失掉, if (touchPoint != null ) { linePath.lineTo(touchPoint.x, touchPoint.y); } canvas.drawPath(linePath, linePaint); } } private PointF touchPoint; @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: reset(); case MotionEvent.ACTION_MOVE: if (touchPoint == null ) { touchPoint = new PointF(event.getX(), event.getY()); } else { touchPoint.set(event.getX(), event.getY()); } for ( int i = 0 ; i < rowCount * rowCount; i++) { // 是否觸摸在圓的范圍內 if (getDistance(touchPoint, points[i]) < radius) { stateSparseArray.put(i, STATE_MOVE); if (!selectedList.contains(points[i])) { selectedList.add(points[i]); } break ; } } break ; case MotionEvent.ACTION_UP: if (check()) { // 正確圖案 if (listener != null ) { listener.onComplete( true ); } for ( int i = 0 ; i < stateSparseArray.size(); i++) { int index = stateSparseArray.keyAt(i); stateSparseArray.put(index, STATE_MOVE); } } else { // 錯誤圖案 for ( int i = 0 ; i < stateSparseArray.size(); i++) { int index = stateSparseArray.keyAt(i); stateSparseArray.put(index, STATE_ERROR); } linePaint.setColor( 0xeeff0000 ); if (listener != null ) { listener.onComplete( false ); } } touchPoint = null ; if (timer == null ) { timer = new Timer(); } timer.schedule( new TimerTask() { @Override public void run() { linePath.reset(); linePaint.setColor( 0xee0000ff ); selectedList.clear(); stateSparseArray.clear(); postInvalidate(); } }, 1000 ); break ; } invalidate(); return true ; } /** * 清除繪制圖案的條件,當觸發 invalidate() 時將清空圖案 */ private void reset() { touchPoint = null ; linePath.reset(); linePaint.setColor( 0xee0000ff ); selectedList.clear(); stateSparseArray.clear(); } public void onStop() { timer.cancel(); } private boolean check() { if (selectedList.size() != standardPointsIndexList.size()) { return false ; } for ( int i = 0 ; i < standardPointsIndexList.size(); i++) { Integer index = standardPointsIndexList.get(i); if (points[index] != selectedList.get(i)) { return false ; } } return true ; } public void setStandard(List<Integer> pointsList) { if (pointsList == null ) { throw new IllegalArgumentException( "standard points index can't null" ); } if (pointsList.size() > rowCount * rowCount) { throw new IllegalArgumentException( "standard points index list can't large to rowcount * columncount" ); } standardPointsIndexList = pointsList; } private OnDrawCompleteListener listener; public void setOnDrawCompleteListener(OnDrawCompleteListener listener) { this .listener = listener; } public interface OnDrawCompleteListener { void onComplete( boolean isSuccess); } private float getDistance(PointF centerPoint, PointF downPoint) { return ( float ) Math.sqrt(Math.pow(centerPoint.x - downPoint.x, 2 ) + Math.pow(centerPoint.y - downPoint.y, 2 )); } private int dp2Px( int dpValue) { return ( int ) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, getResources().getDisplayMetrics()); } } |
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:https://blog.csdn.net/xingxtao/article/details/80545120