Android View自定义锁屏图案

时间:2022-02-26 05:49:10

前言

Android 自定义 View 技能是成为高级工程师所必备的,笔者觉得自定义 View 没有什么捷径可走,唯有经常练习才能解决产品需求。笔者也好久没有写自定义 View 了,赶紧写个控件找点感觉回来。

本文实现的是一个 锁屏图案的自定义控件。效果图如下:

Github 地址:AndroidSample

Android View自定义锁屏图案

LockView 介绍

自定义属性:

Android View自定义锁屏图案

引用方式:

(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();
   }
  });

实现思路

  1. 以默认状态绘制 rowCount * rowCount 个圆,外圆颜色需要在内圆颜色上加上一定的透明度。
  2. 在 onTouchEvent() 方法中,判断当前触摸点与各个圆的圆心距离是否小于圆的半径,决定各个圆此时处于哪个状态(normal,move,error),调用 invalidate() 重新绘制,更新颜色。
  3. ​将手指滑动触摸过的圆的坐标添加到一个 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: xing-java@foxmail.com
 * 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