Android自定义控件实现手势密码

 更新时间:2016年07月18日 11:35:30   作者:cc_lova_wxf  
这篇文章主要介绍了Android自定义控件实现手势密码的相关资料,实现手势解锁功能,感兴趣的小伙伴们可以参考一下

GPT4.0+Midjourney绘画+国内大模型 会员永久免费使用!
如果你想靠AI翻身,你先需要一个靠谱的工具!

Android手势解锁密码效果图 

     首先呢想写这个手势密码的想法呢,完全是凭空而来的,然后笔者就花了一天时间弄出来了。本以为这个东西很简单,实际上手的时候发现,还有很多逻辑需要处理,稍不注意就容易乱套。写个UI效果图大约只花了3个小时,但是处理逻辑就处理了2个小时!废话不多说,下面开始讲解。 
    楼主呢,自己比较自定义控件,什么东西都掌握在自己的手里感觉那是相当不错(对于赶工期的小伙瓣儿们还是别手贱了,非常容易掉坑),一有了这个目标,我就开始构思实现方式。 
    1、整个自定义控件是继承View还是SurfaceView呢?我的经验告诉我:需要一直不断绘制的最好继承SurfaceView,而需要频繁与用户交互的最好就继承View。(求大神来打脸) 
    2、为了实现控件的屏幕适配性,当然必须重写onMeasure方法,然后在onDraw方法中进行绘制。 
    3、面向对象性:这个控件其实由两个对象组成:1、9个圆球;2、圆球之间的连线。 
    4、仔细观察圆球的特征:普通状态是白色、touch状态是蓝色、错误状态是红色、整体分为外围空心圆和内实心圆、所代表的位置信息(密码值) 
    5、仔细观察连线的特征:普通状态为蓝色、错误状态为红色、始终连接两个圆的中心、跟随手指移动而拓展连线、连线之间未点亮的圆球也要点亮。 
    6、通过外露参数来设置圆球的颜色、大小等等 
    7、通过上面的分析,真个控件可模块化为三个任务:onMeasure计算控件宽高以及小球半径、onDraw绘制小球与连线、onTouchEvent控制绘制变化。 

    我把整个源码分为三个类文件:LockView、Circle、Util,其中LockView代表整个控件,Circle代表小圆球、Util封装工具方法(Path因为太简单就没封装,若有代码洁癖请自行封装),下面展示Util类的源代码。 

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
public class Util{
  
 private static final String SP_NAME = "LOCKVIEW";
 private static final String SP_KEY = "PASSWORD";
 
 public static void savePwd(Context mContext ,List<Integer> password){
  SharedPreferences sp = mContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
  sp.edit().putString(SP_KEY, listToString(password)).commit();
 }
  
 public static String getPwd(Context mContext){
  SharedPreferences sp = mContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
  return sp.getString(SP_KEY, "");
 }
  
 public static void clearPwd(Context mContext){
  SharedPreferences sp = mContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
  sp.edit().remove(SP_KEY).commit();
 }
  
 public static String listToString(List<Integer> lists){
  StringBuffer sb = new StringBuffer();
  for(int i = 0; i < lists.size(); i++){
   sb.append(lists.get(i));
  }
  return sb.toString();
 }
  
 public static List<Integer> stringToList(String string){
  List<Integer> lists = new ArrayList<>();
  for(int i = 0; i < string.length(); i++){
   lists.add(Integer.parseInt(string.charAt(i) + ""));
  }
  return lists;
 }
}

     这个工具方法其实很简单,就是对SharedPreferences的一个读写,还有就是List与String类型的互相转换。这里就不描述了。下面展示Circle的源码 

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
public class Circle{
 //默认值
 public static final int DEFAULT_COLOR = Color.WHITE;
 public static final int DEFAULT_BOUND = 5;
 public static final int DEFAULT_CENTER_BOUND = 15;
 //状态值
 public static final int STATUS_DEFAULT = 0;
 public static final int STATUS_TOUCH = 1;
 public static final int STATUS_SUCCESS = 2;
 public static final int STATUS_FAILED = 3;
  
 //圆形的中点X、Y坐标
 private int centerX;
 private int centerY;
 //圆形的颜色值
 private int colorDefault = DEFAULT_COLOR;
 private int colorSuccess;
 private int colorFailed;
 //圆形的宽度
 private int bound = DEFAULT_BOUND;
 //中心的宽度
 private int centerBound = DEFAULT_CENTER_BOUND;
 //圆形的半径
 private int radius;
 //圆形的状态
 private int status = STATUS_DEFAULT;
 //圆形的位置
 private int position;
  
 public Circle(int centerX, int centerY, int colorSuccess, int colorFailed, int radius, int position){
  super();
  this.centerX = centerX;
  this.centerY = centerY;
  this.colorSuccess = colorSuccess;
  this.colorFailed = colorFailed;
  this.radius = radius;
  this.position = position;
 }
 
 public Circle(int centerX, int centerY, int colorDefault, int colorSuccess, int colorFailed, int bound,
   int centerBound, int radius, int status, int position){
  super();
  this.centerX = centerX;
  this.centerY = centerY;
  this.colorDefault = colorDefault;
  this.colorSuccess = colorSuccess;
  this.colorFailed = colorFailed;
  this.bound = bound;
  this.centerBound = centerBound;
  this.radius = radius;
  this.status = status;
  this.position = position;
 }
 
 public int getCenterX(){
  return centerX;
 }
 
 public void setCenterX(int centerX){
  this.centerX = centerX;
 }
 
 public int getCenterY(){
  return centerY;
 }
 
 public void setCenterY(int centerY){
  this.centerY = centerY;
 }
 
 public int getColorDefault(){
  return colorDefault;
 }
 
 public void setColorDefault(int colorDefault){
  this.colorDefault = colorDefault;
 }
 
 public int getColorSuccess(){
  return colorSuccess;
 }
 
 public void setColorSuccess(int colorSuccess){
  this.colorSuccess = colorSuccess;
 }
 
 public int getColorFailed(){
  return colorFailed;
 }
 
 public void setColorFailed(int colorFailed){
  this.colorFailed = colorFailed;
 }
 
 public int getBound(){
  return bound;
 }
 
 public void setBound(int bound){
  this.bound = bound;
 }
 
 public int getCenterBound(){
  return centerBound;
 }
 
 public void setCenterBound(int centerBound){
  this.centerBound = centerBound;
 }
 
 public int getRadius(){
  return radius;
 }
 
 public void setRadius(int radius){
  this.radius = radius;
 }
 
 public int getStatus(){
  return status;
 }
 
 public void setStatus(int status){
  this.status = status;
 }
 
 public int getPosition(){
  return position;
 }
 
 public void setPosition(int position){
  this.position = position;
 }
 
 /**
  * @Description:改变圆球当前状态
 */
 public void changeStatus(int status){
  this.status = status;
 }
  
 /**
  * @Description:绘制这个圆形
 */
 public void draw(Canvas canvas ,Paint paint){
  switch(status){
   case STATUS_DEFAULT:
    paint.setColor(colorDefault);
    break;
   case STATUS_TOUCH:
   case STATUS_SUCCESS:
    paint.setColor(colorSuccess);
    break;
   case STATUS_FAILED:
    paint.setColor(colorFailed);
    break;
   default:
    paint.setColor(colorDefault);
    break;
  }
  paint.setStyle(Paint.Style.FILL);
  //绘制中心实心圆
  canvas.drawCircle(centerX, centerY, centerBound, paint);
  //绘制空心圆
  paint.setStyle(Paint.Style.STROKE);
  paint.setStrokeWidth(bound);
  canvas.drawCircle(centerX, centerY, radius, paint);
 }
}

     这个Circle其实也非常简单。上面定义的成员变量一眼便明,并且有注释。重点在最后的draw方法,首先呢根据当前圆球的不同状态设置不同的颜色值,然后绘制中心的实心圆,再绘制外围的空心圆。所有的参数要么是外界传递,要么是默认值。(ps:面向对象真的非常有用,解耦良好的代码写起来也舒服看起来也舒服)。 

    最后的重点来了,LockView的源码,首先贴源码,然后再针对性讲解。 

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
public class LockView extends View{
  
 private static final int COUNT_PER_RAW = 3;
 private static final int DURATION = 1500;
 private static final int MIN_PWD_NUMBER = 6;
 //@Fields STATUS_NO_PWD : 当前没有保存密码
 public static final int STATUS_NO_PWD = 0;
 //@Fields STATUS_RETRY_PWD : 需要再输入一次密码
 public static final int STATUS_RETRY_PWD = 1;
 //@Fields STATUS_SAVE_PWD : 成功保存密码
 public static final int STATUS_SAVE_PWD = 2;
 //@Fields STATUS_SUCCESS_PWD : 成功验证密码
 public static final int STATUS_SUCCESS_PWD = 3;
 //@Fields STATUS_FAILED_PWD : 验证密码失败
 public static final int STATUS_FAILED_PWD = 4;
 //@Fields STATUS_ERROR : 输入密码长度不够
 public static final int STATUS_ERROR = 5;
  
 private int width;
 private int height;
 private int padding = 0;
 private int colorSuccess = Color.BLUE;
 private int colorFailed = Color.RED;
 private int minPwdNumber = MIN_PWD_NUMBER;
 private List<Circle> circles = new ArrayList<>();
 private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
 private Path mPath = new Path();
 private Path backupsPath = new Path();
 private List<Integer> result = new ArrayList<>();
 private int status = STATUS_NO_PWD;
 private OnLockListener listener;
 private Handler handler = new Handler();
 
 public LockView(Context context, AttributeSet attrs, int defStyle){
  super(context, attrs, defStyle);
  initStatus();
 }
 
 public LockView(Context context, AttributeSet attrs){
  super(context, attrs);
  initStatus();
 }
 
 public LockView(Context context){
  super(context);
  initStatus();
 }
 
 /**
  * @Description:初始化当前密码的状态
 */
 public void initStatus(){
  if(TextUtils.isEmpty(Util.getPwd(getContext()))){
   status = STATUS_NO_PWD;
  }else{
   status = STATUS_SAVE_PWD;
  }
 }
  
 public int getCurrentStatus(){
  return status;
 }
 
 /**
  * @Description:初始化参数,若不调用则使用默认值
  * @param padding 圆球之间的间距
  * @param colorSuccess 密码正确时圆球的颜色
  * @param colorFailed 密码错误时圆球的颜色
  * @return LockView
 */
 public LockView initParam(int padding ,int colorSuccess ,int colorFailed ,int minPwdNumber){
  this.padding = padding;
  this.colorSuccess = colorSuccess;
  this.colorFailed = colorFailed;
  this.minPwdNumber = minPwdNumber;
  init();
  return this;
 }
  
 /**
  * @Description:若第一次调用则创建圆球,否则更新圆球
 */
 private void init(){
  int circleRadius = (width - (COUNT_PER_RAW + 1) * padding) / COUNT_PER_RAW /2;
  if(circles.size() == 0){  
   for(int i = 0; i < COUNT_PER_RAW * COUNT_PER_RAW; i++){
    createCircles(circleRadius, i);
   }
  }else{
   for(int i = 0; i < COUNT_PER_RAW * COUNT_PER_RAW; i++){
    updateCircles(circles.get(i), circleRadius);
   }
  }
 }
  
 private void createCircles(int radius, int position){
  int centerX = (position % 3 + 1) * padding + (position % 3 * 2 + 1) * radius;
  int centerY = (position / 3 + 1) * padding + (position / 3 * 2 + 1) * radius;
  Circle circle = new Circle(centerX, centerY, colorSuccess, colorFailed, radius, position);
  circles.add(circle);
 }
  
 private void updateCircles(Circle circle ,int radius){
  int centerX = (circle.getPosition() % 3 + 1) * padding + (circle.getPosition() % 3 * 2 + 1) * radius;
  int centerY = (circle.getPosition() / 3 + 1) * padding + (circle.getPosition() / 3 * 2 + 1) * radius;
  circle.setCenterX(centerX);
  circle.setCenterY(centerY);
  circle.setRadius(radius);
  circle.setColorSuccess(colorSuccess);
  circle.setColorFailed(colorFailed);
 }
 
 @Override
 protected void onDraw(Canvas canvas){
  init();
  //绘制圆
  for(int i = 0; i < circles.size() ;i++){
   circles.get(i).draw(canvas, mPaint);
  }
  if(result.size() != 0){  
   //绘制Path
   Circle temp = circles.get(result.get(0));
   mPaint.setColor(temp.getStatus() == Circle.STATUS_FAILED ? colorFailed : colorSuccess);
   mPaint.setStrokeWidth(Circle.DEFAULT_CENTER_BOUND);
   canvas.drawPath(mPath, mPaint);
  }
 }
  
 @Override
 public boolean onTouchEvent(MotionEvent event){
  switch(event.getAction()){
   case MotionEvent.ACTION_DOWN:
    backupsPath.reset();
    for(int i = 0; i < circles.size() ;i++){
     Circle circle = circles.get(i);
     if(event.getX() >= circle.getCenterX() - circle.getRadius()
       && event.getX() <= circle.getCenterX() + circle.getRadius()
       && event.getY() >= circle.getCenterY() - circle.getRadius()
       && event.getY() <= circle.getCenterY() + circle.getRadius()){
      circle.setStatus(Circle.STATUS_TOUCH);
      //将这个点放入Path
      backupsPath.moveTo(circle.getCenterX(), circle.getCenterY());
      //放入结果
      result.add(circle.getPosition());
      break;
     }
    }
    invalidate();
    return true;
     
   case MotionEvent.ACTION_MOVE:
    for(int i = 0; i < circles.size() ;i++){
     Circle circle = circles.get(i);
     if(event.getX() >= circle.getCenterX() - circle.getRadius()
       && event.getX() <= circle.getCenterX() + circle.getRadius()
       && event.getY() >= circle.getCenterY() - circle.getRadius()
       && event.getY() <= circle.getCenterY() + circle.getRadius()){
      if(!result.contains(circle.getPosition())){      
       circle.setStatus(Circle.STATUS_TOUCH);
       //首先判断是否连线中间也有满足条件的圆
       Circle lastCircle = circles.get(result.get(result.size() - 1));
       int cx = (lastCircle.getCenterX() + circle.getCenterX()) / 2;
       int cy = (lastCircle.getCenterY() + circle.getCenterY()) / 2;
       for(int j = 0; j < circles.size(); j++){
        Circle tempCircle = circles.get(j);
        if(cx >= tempCircle.getCenterX() - tempCircle.getRadius()
          && cx <= tempCircle.getCenterX() + tempCircle.getRadius()
          && cy >= tempCircle.getCenterY() - tempCircle.getRadius()
          && cy <= tempCircle.getCenterY() + tempCircle.getRadius()){
         //处理满足条件的圆
         backupsPath.lineTo(tempCircle.getCenterX(), tempCircle.getCenterY());
         //放入结果
         tempCircle.setStatus(Circle.STATUS_TOUCH);
         result.add(tempCircle.getPosition());
        }
       }
       //处理现在的圆
       backupsPath.lineTo(circle.getCenterX(), circle.getCenterY());
       //放入结果
       circle.setStatus(Circle.STATUS_TOUCH);
       result.add(circle.getPosition());
       break;
      }
     }
    }
    mPath.reset();
    mPath.addPath(backupsPath);
    mPath.lineTo(event.getX(), event.getY());
    invalidate();
    break;
     
   case MotionEvent.ACTION_UP:
    mPath.reset();
    mPath.addPath(backupsPath);
    invalidate();
    if(result.size() < minPwdNumber){
     if(listener != null){     
      listener.onError();
     }
     if(status == STATUS_RETRY_PWD){
      Util.clearPwd(getContext());
     }
     status = STATUS_ERROR;
     for(int i = 0; i < result.size(); i++){
      circles.get(result.get(i)).setStatus(Circle.STATUS_FAILED);
     }
    }else{
     if(status == STATUS_NO_PWD){ //当前没有密码
      //保存密码,重新录入
      Util.savePwd(getContext(), result);
      status = STATUS_RETRY_PWD;
      if(listener != null){
       listener.onTypeInOnce(Util.listToString(result));
      }
     }else if(status == STATUS_RETRY_PWD){ //需要重新绘制密码
      //判断两次输入是否相等
      if(Util.getPwd(getContext()).equals(Util.listToString(result))){
       status = STATUS_SAVE_PWD;
       if(listener != null){
        listener.onTypeInTwice(Util.listToString(result), true);
       }
       for(int i = 0; i < result.size(); i++){
        circles.get(result.get(i)).setStatus(Circle.STATUS_SUCCESS);
       }
      }else{
       status = STATUS_NO_PWD;
       Util.clearPwd(getContext());
       if(listener != null){
        listener.onTypeInTwice(Util.listToString(result), false);
       }
       for(int i = 0; i < result.size(); i++){
        circles.get(result.get(i)).setStatus(Circle.STATUS_FAILED);
       }
      }
     }else if(status == STATUS_SAVE_PWD){ //验证密码
      //判断密码是否正确
      if(Util.getPwd(getContext()).equals(Util.listToString(result))){
       status = STATUS_SUCCESS_PWD;
       if(listener != null){
        listener.onUnLock(Util.listToString(result), true);
       }
       for(int i = 0; i < result.size(); i++){
        circles.get(result.get(i)).setStatus(Circle.STATUS_SUCCESS);
       }
      }else{
       status = STATUS_FAILED_PWD;
       if(listener != null){
        listener.onUnLock(Util.listToString(result), false);
       }
       for(int i = 0; i < result.size(); i++){
        circles.get(result.get(i)).setStatus(Circle.STATUS_FAILED);
       }
      }
     }
    }
    invalidate();
    handler.postDelayed(new Runnable(){
      
     @Override
     public void run(){
      result.clear();
      mPath.reset();
      backupsPath.reset();
     //  initStatus();
      // 重置下状态
      if(status == STATUS_SUCCESS_PWD || status == STATUS_FAILED_PWD){
       status = STATUS_SAVE_PWD;
      }else if(status == STATUS_ERROR){
       initStatus();
      }
      for(int i = 0; i < circles.size(); i++){
       circles.get(i).setStatus(Circle.STATUS_DEFAULT);
      }
      invalidate();
     }
    }, DURATION);
    break;
   default:
    break;
  }
  return super.onTouchEvent(event);
 }
 
 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
  width = MeasureSpec.getSize(widthMeasureSpec);
  height = width - getPaddingLeft() - getPaddingRight() + getPaddingTop() + getPaddingBottom();
  setMeasuredDimension(width, height);
 }
  
 public void setOnLockListener(OnLockListener listener){
  this.listener = listener;
 }
  
 public interface OnLockListener{
  /**
   * @Description:没有密码时,第一次录入密码触发器
  */
  void onTypeInOnce(String input);
  /**
   * @Description:已经录入第一次密码,录入第二次密码触发器
   */
  void onTypeInTwice(String input ,boolean isSuccess);
  /**
   * @Description:验证密码触发器
  */
  void onUnLock(String input ,boolean isSuccess);
   
  /**
   * @Description:密码长度不够
   */
  void onError();
 }
}

好了,逐次讲解。 

 首先是对status的初始化,其实在static域我已经申明了6个状态,分别是: 

1
2
3
4
5
6
7
8
9
10
11
12
//当前没有保存密码
public static final int STATUS_NO_PWD = 0;
//需要再输入一次密码
public static final int STATUS_RETRY_PWD = 1;
//成功保存密码
public static final int STATUS_SAVE_PWD = 2;
//成功验证密码
public static final int STATUS_SUCCESS_PWD = 3;
//验证密码失败
public static final int STATUS_FAILED_PWD = 4;
//输入密码长度不够
public static final int STATUS_ERROR = 5;

 在刚初始化的时候,就初始化当前的状态,初始化状态就只有2个状态:有密码、无密码。 

1
2
3
4
5
6
7
8
9
10
11
public void initStatus(){
 if(TextUtils.isEmpty(Util.getPwd(getContext()))){
  status = STATUS_NO_PWD;
 }else{
  status = STATUS_SAVE_PWD;
 }
}
 
public int getCurrentStatus(){
 return status;
}

     然后就是通过外界的设置初始化一些参数(若不调用initParam方法,则采用默认值): 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public LockView initParam(int padding ,int colorSuccess ,int colorFailed ,int minPwdNumber){
 this.padding = padding;
 this.colorSuccess = colorSuccess;
 this.colorFailed = colorFailed;
 this.minPwdNumber = minPwdNumber;
 init();
 return this;
}
 
/**
 * @Description:若第一次调用则创建圆球,否则更新圆球
*/
private void init(){
 int circleRadius = (width - (COUNT_PER_RAW + 1) * padding) / COUNT_PER_RAW /2;
 if(circles.size() == 0){  
  for(int i = 0; i < COUNT_PER_RAW * COUNT_PER_RAW; i++){
   createCircles(circleRadius, i);
  }
 }else{
  for(int i = 0; i < COUNT_PER_RAW * COUNT_PER_RAW; i++){
   updateCircles(circles.get(i), circleRadius);
  }
 }
}

上述代码主要根据设置的padding值,计算出小球的大小,然后判断是否是初始化小球,还是更新小球。 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void createCircles(int radius, int position){
 int centerX = (position % 3 + 1) * padding + (position % 3 * 2 + 1) * radius;
 int centerY = (position / 3 + 1) * padding + (position / 3 * 2 + 1) * radius;
 Circle circle = new Circle(centerX, centerY, colorSuccess, colorFailed, radius, position);
 circles.add(circle);
}
 
private void updateCircles(Circle circle ,int radius){
 int centerX = (circle.getPosition() % 3 + 1) * padding + (circle.getPosition() % 3 * 2 + 1) * radius;
 int centerY = (circle.getPosition() / 3 + 1) * padding + (circle.getPosition() / 3 * 2 + 1) * radius;
 circle.setCenterX(centerX);
 circle.setCenterY(centerY);
 circle.setRadius(radius);
 circle.setColorSuccess(colorSuccess);
 circle.setColorFailed(colorFailed);
}

别忘了上面的方法依赖一个width值,这个值是在onMeasure中计算出来的 

1
2
3
4
5
6
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
 width = MeasureSpec.getSize(widthMeasureSpec);
 height = width - getPaddingLeft() - getPaddingRight() + getPaddingTop() + getPaddingBottom();
 setMeasuredDimension(width, height);
}

然后就是绘制方法了,因为我们的高度解耦性,本应该非常复杂的onDraw方法,却如此简单。就只绘制了小球和路径。 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected void onDraw(Canvas canvas){
 init();
 //绘制圆
 for(int i = 0; i < circles.size() ;i++){
  circles.get(i).draw(canvas, mPaint);
 }
 if(result.size() != 0){  
  //绘制Path
  Circle temp = circles.get(result.get(0));
  mPaint.setColor(temp.getStatus() == Circle.STATUS_FAILED ? colorFailed : colorSuccess);
  mPaint.setStrokeWidth(Circle.DEFAULT_CENTER_BOUND);
  canvas.drawPath(mPath, mPaint);
 }
}

控件是需要和外界进行交互的,我喜欢的方法就是自定义监听器,然后接口回调。 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void setOnLockListener(OnLockListener listener){
 this.listener = listener;
}
 
public interface OnLockListener{
 /**
  * @Description:没有密码时,第一次录入密码触发器
 */
 void onTypeInOnce(String input);
 /**
  * @Description:已经录入第一次密码,录入第二次密码触发器
  */
 void onTypeInTwice(String input ,boolean isSuccess);
 /**
  * @Description:验证密码触发器
 */
 void onUnLock(String input ,boolean isSuccess);
  
 /**
  * @Description:密码长度不够
  */
 void onError();
}

最后最最最重要的一个部分来了,onTouchEvent方法,这个方法其实也可以分为三个部分讲解:down事件、move事件和up事件。首先贴出down事件代码 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
case MotionEvent.ACTION_DOWN:
   backupsPath.reset();
   for(int i = 0; i < circles.size() ;i++){
    Circle circle = circles.get(i);
    if(event.getX() >= circle.getCenterX() - circle.getRadius()
      && event.getX() <= circle.getCenterX() + circle.getRadius()
      && event.getY() >= circle.getCenterY() - circle.getRadius()
      && event.getY() <= circle.getCenterY() + circle.getRadius()){
     circle.setStatus(Circle.STATUS_TOUCH);
     //将这个点放入Path
     backupsPath.moveTo(circle.getCenterX(), circle.getCenterY());
     //放入结果
     result.add(circle.getPosition());
     break;
    }
   }
   invalidate();
   return true;

也就是对按下的x、y坐标进行判断,是否属于我们的小球范围内,若属于,则放入路径集合、更改状态、加入密码结果集。这里别忘了return true,大家都知道吧。 
然后是move事件,move事件主要做三件事情:变更小球的状态、添加到路径集合、对路径覆盖的未点亮小球进行点亮。代码有详细注释就不过多讲解了。 

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
case MotionEvent.ACTION_MOVE:
    for(int i = 0; i < circles.size() ;i++){
     Circle circle = circles.get(i);
     if(event.getX() >= circle.getCenterX() - circle.getRadius()
       && event.getX() <= circle.getCenterX() + circle.getRadius()
       && event.getY() >= circle.getCenterY() - circle.getRadius()
       && event.getY() <= circle.getCenterY() + circle.getRadius()){
      if(!result.contains(circle.getPosition())){      
       circle.setStatus(Circle.STATUS_TOUCH);
       //首先判断是否连线中间也有满足条件的圆
       Circle lastCircle = circles.get(result.get(result.size() - 1));
       int cx = (lastCircle.getCenterX() + circle.getCenterX()) / 2;
       int cy = (lastCircle.getCenterY() + circle.getCenterY()) / 2;
       for(int j = 0; j < circles.size(); j++){
        Circle tempCircle = circles.get(j);
        if(cx >= tempCircle.getCenterX() - tempCircle.getRadius()
          && cx <= tempCircle.getCenterX() + tempCircle.getRadius()
          && cy >= tempCircle.getCenterY() - tempCircle.getRadius()
          && cy <= tempCircle.getCenterY() + tempCircle.getRadius()){
         //处理满足条件的圆
         backupsPath.lineTo(tempCircle.getCenterX(), tempCircle.getCenterY());
         //放入结果
         tempCircle.setStatus(Circle.STATUS_TOUCH);
         result.add(tempCircle.getPosition());
        }
       }
       //处理现在的圆
       backupsPath.lineTo(circle.getCenterX(), circle.getCenterY());
       //放入结果
       circle.setStatus(Circle.STATUS_TOUCH);
       result.add(circle.getPosition());
       break;
      }
     }
    }
    mPath.reset();
    mPath.addPath(backupsPath);
    mPath.lineTo(event.getX(), event.getY());
    invalidate();
    break;

这里我用了两个Path对象,backupsPath用于只存放小球的中点坐标,mPath不仅要存储小球的中点坐标,还要存储当前手指触碰坐标,为了实现连线跟随手指运动的效果。 
最后是up事件,这里有太多复杂的状态转换,我估计文字讲解是描述不清的,大家还是看源代码吧。           

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
case MotionEvent.ACTION_UP:
   mPath.reset();
   mPath.addPath(backupsPath);
   invalidate();
   if(result.size() < minPwdNumber){
    if(listener != null){     
     listener.onError();
    }
    if(status == STATUS_RETRY_PWD){
     Util.clearPwd(getContext());
    }
    status = STATUS_ERROR;
    for(int i = 0; i < result.size(); i++){
     circles.get(result.get(i)).setStatus(Circle.STATUS_FAILED);
    }
   }else{
    if(status == STATUS_NO_PWD){ //当前没有密码
     //保存密码,重新录入
     Util.savePwd(getContext(), result);
     status = STATUS_RETRY_PWD;
     if(listener != null){
      listener.onTypeInOnce(Util.listToString(result));
     }
    }else if(status == STATUS_RETRY_PWD){ //需要重新绘制密码
     //判断两次输入是否相等
     if(Util.getPwd(getContext()).equals(Util.listToString(result))){
      status = STATUS_SAVE_PWD;
      if(listener != null){
       listener.onTypeInTwice(Util.listToString(result), true);
      }
      for(int i = 0; i < result.size(); i++){
       circles.get(result.get(i)).setStatus(Circle.STATUS_SUCCESS);
      }
     }else{
      status = STATUS_NO_PWD;
      Util.clearPwd(getContext());
      if(listener != null){
       listener.onTypeInTwice(Util.listToString(result), false);
      }
      for(int i = 0; i < result.size(); i++){
       circles.get(result.get(i)).setStatus(Circle.STATUS_FAILED);
      }
     }
    }else if(status == STATUS_SAVE_PWD){ //验证密码
     //判断密码是否正确
     if(Util.getPwd(getContext()).equals(Util.listToString(result))){
      status = STATUS_SUCCESS_PWD;
      if(listener != null){
       listener.onUnLock(Util.listToString(result), true);
      }
      for(int i = 0; i < result.size(); i++){
       circles.get(result.get(i)).setStatus(Circle.STATUS_SUCCESS);
      }
     }else{
      status = STATUS_FAILED_PWD;
      if(listener != null){
       listener.onUnLock(Util.listToString(result), false);
      }
      for(int i = 0; i < result.size(); i++){
       circles.get(result.get(i)).setStatus(Circle.STATUS_FAILED);
      }
     }
    }
   }
   invalidate();
   handler.postDelayed(new Runnable(){
     
    @Override
    public void run(){
     result.clear();
     mPath.reset();
     backupsPath.reset();
    //  initStatus();
     // 重置下状态
     if(status == STATUS_SUCCESS_PWD || status == STATUS_FAILED_PWD){
      status = STATUS_SAVE_PWD;
     }else if(status == STATUS_ERROR){
      initStatus();
     }
     for(int i = 0; i < circles.size(); i++){
      circles.get(i).setStatus(Circle.STATUS_DEFAULT);
     }
     invalidate();
    }
   }, DURATION);
   break;

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

蓄力AI

微信公众号搜索 “ 脚本之家 ” ,选择关注

程序猿的那些事、送书等活动等着你

相关文章

  • Android中layer-list基本使用详解

    Android中layer-list基本使用详解

    这篇文章主要介绍了Android中layer-list基本使用详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-12-12
  • Android端“被挤下线”功能的单点登录实现

    Android端“被挤下线”功能的单点登录实现

    本篇文章主要介绍了Android端“被挤下线”功能的单点登录实现,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-11-11
  • Flutter交互并使用小工具管理其状态widget的state详解

    Flutter交互并使用小工具管理其状态widget的state详解

    这篇文章主要为大家介绍了Flutter交互并使用小工具管理其状态widget的state详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • android仿知乎ScrollView滚动改变标题栏透明度

    android仿知乎ScrollView滚动改变标题栏透明度

    这篇文章主要为大家详细介绍了android仿知乎ScrollView滚动改变标题栏透明度,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-06-06
  • Android 使用FragmentTabhost代替Tabhost

    Android 使用FragmentTabhost代替Tabhost

    这篇文章主要介绍了Android 使用FragmentTabhost代替Tabhost的相关资料,需要的朋友可以参考下
    2017-05-05
  • Android监听电池状态实例代码

    Android监听电池状态实例代码

    这篇文章给大家介绍Android监听电池状态实例代码,对android监听电池状态相关知识感兴趣的朋友一起学习吧
    2016-03-03
  • Android Kotlin全面详细类使用语法学习指南

    Android Kotlin全面详细类使用语法学习指南

    这篇文章主要为大家介绍了Android Kotlin全面详细类使用语法学习指南,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-06-06
  • android中图片的三级缓存cache策略(内存/文件/网络)

    android中图片的三级缓存cache策略(内存/文件/网络)

    实现图片缓存也不难,需要有相应的cache策略。这里我采用 内存-文件-网络 三层cache机制,其中内存缓存包括强引用缓存和软引用缓存(SoftReference),其实网络不算cache,这里姑且也把它划到缓存的层次结构中
    2013-06-06
  • android实现滚动文本效果

    android实现滚动文本效果

    这篇文章主要为大家详细介绍了android实现滚动文本效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-05-05
  • Android EditText密码的隐藏和显示功能

    Android EditText密码的隐藏和显示功能

    这篇文章主要介绍了Android EditText密码的隐藏和显示功能的相关资料,主要是利用EditText和CheckBox 来实现该功能,需要的朋友可以参考下
    2017-07-07

最新评论