【iOS】Quartz 2D之手势解锁

目前电子设备的密码基本有四大类:字符密码、手势解锁、指纹密码、人脸识别。

字符密码无非就是各种字符按照一定顺序组在一起,在产品中几乎都可以看到他的身影。指纹密码和人脸识别需要硬件设备的支持,不是软件开发人员能够控制的。手势解锁其实是一种特殊的字符密码,每一个按钮代码一个字符,一共九个字符,按照设定的规则连接在一起就组成了一串字符。

一、手势密码的逻辑

  1. 自定义view,使用九宫格排布九个按钮,并把按钮放在自定义view上;
  2. 监听view的手势事件,当点击view时判断触摸点是否在按钮上;
  3. 如果点击事件在按钮上就把按钮的状态变为高亮状态,并把按钮放到选中栈中,否则不做任何处理;
  4. 当滑动事件发生时,判断滑动轨迹是否在按钮范围内;
  5. 如果滑动轨迹在按钮范围内,就把按钮的状态变为高亮状态,并把按钮放到选中栈中,否则不做任何处理;
  6. 以第一个高亮按钮为出发点绘制直线,按照选中栈中的顺序依次绘制;
  7. 当连线按钮达到限制条件时(默认选中5个),把每一个按钮绑定的数字组成字符串通知给代理;
  8. 当取消事件发生时,把已选按钮状态恢复到之前未高亮状态。

二、完整代码及效果

UI界面

效果预览

ViewController.m

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
#import "ViewController.h"
#import "DBGestureLockView.h"

@interface ViewController ()<DBGestureLockViewDelegate>

@property (weak, nonatomic) IBOutlet DBGestureLockView *gestureLockView;
/*
* 提示语
*/
@property (weak, nonatomic) IBOutlet UILabel *hintLabel;

@end

@implementation ViewController
static NSString * const kInitPwdKey = @"initPwd";

- (void)viewDidLoad {
[super viewDidLoad];

self.gestureLockView.delegate = self;

NSString *initPwd = [[NSUserDefaults standardUserDefaults] objectForKey:kInitPwdKey];
if (!initPwd) {
self.hintLabel.text = @"请设置初始化密码";
} else {
self.hintLabel.text = @"请解锁";
}
}

#pragma mark - DBGestureLockViewDelegate
- (void)gestureLockViewDidEnd:(NSString *)result {
NSString *initPwd = [[NSUserDefaults standardUserDefaults] objectForKey:kInitPwdKey];
if (!initPwd) { // 首次输入密码
if (result.length !=0 && result.length == self.gestureLockView.allowSelectedCount) {
[[NSUserDefaults standardUserDefaults] setObject:result forKey:kInitPwdKey];
self.hintLabel.text = @"密码设置完成";
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.hintLabel.text = nil;
});
}
} else if (result.length != 0 && [result isEqualToString:initPwd]) {
self.hintLabel.text = @"恭喜,解锁成功";
} else {
self.hintLabel.text = @"密码错误,请重试";
}
}

@end

DBGestureLockView

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
// DBGestureLockView.h
#import <UIKit/UIKit.h>

@protocol DBGestureLockViewDelegate <NSObject>

/**
* 手势结束后调用
* @param result 手势密码结果(0~9数字组成)
*/
- (void)gestureLockViewDidEnd:(NSString *)result;

@end

@interface DBGestureLockView : UIView

@property (nonatomic, weak) id<DBGestureLockViewDelegate> delegate;

/**
* 允许选择数(密码位数),默认5位数
*/
@property (nonatomic, assign) NSInteger allowSelectedCount;

@end

// DBGestureLockView.m
#import "DBGestureLockView.h"

@interface DBGestureLockView ()
{
NSArray *_itemsArray; // 所有按钮
CGPoint _curPoint; // 当前触摸点
}

// 当前选中的按钮
@property (nonatomic, strong) NSMutableArray *selectedItemsArray;

@end

@implementation DBGestureLockView

- (void)awakeFromNib {
[super awakeFromNib];
[self initSubviews];
}

- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self initSubviews];
}
return self;
}

// 初始化子视图
- (void)initSubviews {
NSMutableArray *itemsArr = [NSMutableArray array];
for (NSInteger i = 0; i < 9; i++) {
UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.tag = i;
[btn setImage:[UIImage imageNamed:@"gesture_node_normal"] forState:UIControlStateNormal];
[btn setImage:[UIImage imageNamed:@"gesture_node_selected"] forState:UIControlStateSelected];
[btn setImage:[UIImage imageNamed:@"gesture_node_selected"] forState:UIControlStateHighlighted];
btn.userInteractionEnabled = NO;
[itemsArr addObject:btn];
[self addSubview:btn];
}
_itemsArray = itemsArr;
self.allowSelectedCount = 5;
}

- (void)layoutSubviews {
[super layoutSubviews];
NSInteger column = 3;
CGFloat item_w = 60.0;
CGFloat item_h = 60.0;
CGFloat item_x = 0.0;
CGFloat item_y = 0.0;
CGFloat item_space = (self.bounds.size.width - column * item_w) / (column - 1);
for (NSInteger i = 0; i < _itemsArray.count; i++) {
UIButton *btn = _itemsArray[i];
item_x = i % column * (item_w + item_space);
item_y = i / column * (item_h + item_space);
btn.frame = CGRectMake(item_x, item_y, item_w, item_h);
}
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
CGPoint curPoint = [self getCurrentPointWithTouches:touches];
UIButton *btn = [self itemContainsPoint:curPoint];
if (btn) {
btn.selected = YES;
[self.selectedItemsArray addObject:btn];
}
[self setNeedsDisplay];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
CGPoint curPoint = [self getCurrentPointWithTouches:touches];
_curPoint = curPoint;
UIButton *btn = [self itemContainsPoint:curPoint];
if (btn && btn.selected == NO && self.selectedItemsArray.count < self.allowSelectedCount) {
btn.selected = YES;
[self.selectedItemsArray addObject:btn];
}
[self setNeedsDisplay];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSMutableString *result = [NSMutableString string];
for (UIButton *btn in self.selectedItemsArray) {
btn.selected = NO;
[result appendString:[NSString stringWithFormat:@"%ld", btn.tag]];
}
if (self.delegate && [self.delegate respondsToSelector:@selector(gestureLockViewDidEnd:)]) {
[self.delegate gestureLockViewDidEnd:result];
}

[self.selectedItemsArray removeAllObjects];
_curPoint = CGPointZero;
[self setNeedsDisplay];
}

// 获取当前触摸点
- (CGPoint)getCurrentPointWithTouches:(NSSet<UITouch *> *)touches {
UITouch *curTouch = [touches anyObject];
CGPoint curPoint = [curTouch locationInView:self];
return curPoint;
}

// 判断触摸点是否在btn范围内
- (UIButton *)itemContainsPoint:(CGPoint)point {
for (UIButton *btn in _itemsArray) {
BOOL isInside = CGRectContainsPoint(btn.frame, point);
if (isInside) {
return btn;
}
}
return nil;
}

- (void)drawRect:(CGRect)rect {
// 控制台警告:[Unknown process name] CGPathAddLineToPoint: no current point.
if (self.selectedItemsArray.count == 0) return;

// 创建路径
UIBezierPath *path = [UIBezierPath bezierPath];
// 第一个选中的按钮作为起点
for (NSInteger i = 0; i < self.selectedItemsArray.count; i++) {
UIButton *btn = self.selectedItemsArray[i];
if (i == 0) {
[path moveToPoint:btn.center];
} else {
[path addLineToPoint:btn.center];
}
}
// 添加跟随手指移动的线
[path addLineToPoint:_curPoint];
// 上下文状态
[[UIColor systemTealColor] set];
path.lineWidth = 8.0;
path.lineJoinStyle = kCGLineCapRound;
// 绘制路径
[path stroke];
}

#pragma mark - Setter and Getter
- (NSMutableArray *)selectedItemsArray {
if (!_selectedItemsArray) {
_selectedItemsArray = [NSMutableArray array];
}
return _selectedItemsArray;
}

@end