一、先来看一下最终效果
二、需要用到的主要知识
- viewController中点击,移动,点击结束事件的处理
- UIBezierPath的使用
- 重写drawRect的使用
三、实现的具体步骤
1.ViewController中
我们直接使用view的layer的contents属性来设置背景图片
- (void)viewDidLoad {
[super viewDidLoad];
//设置背景
self.view.layer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"Home_refresh_bg"].CGImage);
}
这里需要注意的是CGImage,同时还需要前面的 (__bridge id _Nullable) 桥接。
2.自定义类UnlockView
2.1 画线
为什么要自定义UnlockView呢?为什么不直接在ViewContrller中写呢?是,是可以写到ViewController中,但是那样模块性就不强,如果以后需要用到这个类的时候,直接就可以使用已经写好的类的了,同时新写一个类可以让我们的代码更加专注于管理这个类,如果写在ViewController中就会很冗杂。
UnlockView.m
-(void)awakeFromNib{
[super awakeFromNib];
self.selectedArray = [NSMutableArray array];
self.backgroundColor = [UIColor clearColor];
for(int i =0;i<9;i++){
//创建按钮对象
UIButton * button = [[UIButton alloc]initWithFrame:CGRectZero];
//设置图片
[button setImage:[UIImage imageNamed:@"gesture_node_normal"] forState:UIControlStateNormal];
[button setImage:[UIImage imageNamed:@"gesture_node_selected"] forState:UIControlStateSelected];
//关闭按钮的交互能力
button.userInteractionEnabled = NO;
button.tag = i;
[self addSubview:button];
}
}
由于我们的UnlockView的整个背景View是在storyboard中拖拽上去的,当我们需要进行按钮的初始化的时候,需要在awakeFromNib方法中写,在这个方法中的 selectArray 表示的是我们选择到的按钮。为什么创建按钮时给的frame是 CGRectzero呢?因为控件是通过storyboard或者xib来创建的,需要自己添加的控件的frame在awakeFromNib中可能因为获取不及时而导致不正确,最终布局出现偏差,所以我们在 layoutSubviews中做按钮的布局:
-(void)layoutSubviews{
for(int i = 1;i<self.subviews.count;i++){
UIButton * button = [self.subviews objectAtIndex:i];
//确定是第几列
int column = (i-1)%3;
int row = (i-1)/3;
button.frame = CGRectMake(kPadding+(74+kPadding)*column, kPadding+(74+kPadding)*row, 74, 74);
}
}
这里就是典型的九宫格布局,不用多说。至于为什么从1开始,因为最后我们还需要加一个提示的UILabel,这个UILabel是加在按钮的前面的,所以从1开始。
按钮布局好了之后,我们就开始做响应按钮点击的事件,有三个,touchesBegain,touchMoved,touchesEnded:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//获取触摸点坐标
UITouch * touch = [touches anyObject];
CGPoint location = [touch locationInView:self];
//判断某一个点是否在区域内
for (UIButton *button in self.subviews) {
if (CGRectContainsPoint(button.frame, location)) {
//设置按钮的状态
button.selected = YES;
[self.selectedArray addObject:button];
}
}
}
在刚刚点击上去的时候,我们需要判断当前的点击点是否包含在button的区域内,如果在就改变button的selected属性,然后将这个选中的按钮加入selectArray数组中。
-(void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//获取触摸点坐标
UITouch * touch = [touches anyObject];
CGPoint location = [touch locationInView:self];
//保存当前手的触摸点
self.lastPoint = location;
//判断某一个点是否在区域内
for (UIButton *button in self.subviews) {
if (CGRectContainsPoint(button.frame, location)) {
if (button.selected==NO) {
//设置按钮的状态
button.selected = YES;
[self.selectedArray addObject:button];
}
}
}
//刷新界面
//相当于触发drawRect方法
[self setNeedsDisplay];
}
在touchesMoved方法中,同样判断是否点击在了button中,同时为了保证点亮过的button不会重复加入selectArray中,还需要加一个判断,判断当前的点击点所在的button是否已经被点亮过了。只有当当前button没有被点亮过的情况下才将它点亮,然后加入selectArray数组中。在这个方法中,我们还加入了一个lastpoint变量,它用来记录每次move的坐标,实际上就是每次手在滑动的时的位置,在划线的保证画到我们手的点击处。最后的setNeedDisplay方法调用 drawRect方法,我们在drawRect方法中写具体的画线路径,以达动态画线的效果。
接下来的touchededEnd主要是判断密码的正确与否的逻辑代码,我们后面再说,先来看看画线的代码:
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
//在这里面画线
//1.确定画的路径
UIBezierPath * bpath = [UIBezierPath bezierPath];
//确定线的起始点数组里面第一个按钮
for (int i =0; i<self.selectedArray.count; i++) {
UIButton * button = [self.selectedArray objectAtIndex:i];
//如果是第一个 只需要将path的起始点设到这个按钮的中心
if (i==0) {
[bpath moveToPoint:button.center];
}else{
//否则就需要画线到按钮的中心点
[bpath addLineToPoint:button.center];
}
}
//在最后一个按钮和当前触摸点之间画一条线
[bpath addLineToPoint:_lastPoint];
//2.画上去
//设置线条的宽度
bpath.lineWidth = 5;
//设置线条的连接处样式 为圆滑
bpath.lineJoinStyle = kCGLineJoinRound;
//设置线条的颜色
[[UIColor whiteColor]set];
//按照路径画线条
[bpath stroke];
}
这里我们重写drawRect方法,使用UIBezierPath进行路径的定义。首先我们我们需要确定我们画线的起点,我们画线的起点应该是我们加入到selectArray中的第一个button,然后我们将路径的起点移到这个button中心点(可以理解为将画笔移到这个button的中心点),然后其余的点就依次 addLineto 就可以了。这样只有选中的button之间才有连接的线,我们希望手指移到button的外部时,也能在selectArray中的最后一个按钮与当前触摸点之间有一条线,所以我们在循环外面又加上了一个 addlineto ,到达的点就是我们之前在touchesMoved里面定义的lastPoint。之后就是关于画上去的线条的一些设置,不用多说。
重写的drawRect方法会在程序加载起来的时候调用一次,如果需要手动调用,我们需要 使用 setNeedsDisplay 方法。
2.2 密码操作
这样一来画线的操作就算完成了,接下来就是记录和设置密码的操作。首先我们创建一个label来提示用户的操作:
//UnlockView.m
self.titleLabel = [self viewWithTag:1000];
我们的label是通过stroyboard添加上去的,我们用tag值将它取出。
接下来需要确定label需要显示的内容,我们从 NSUserdefaults 中取出按照 "pwd"键名存入的密码,这里需要加入一步判断,如果取出来有东西,说明之前设置过密码了;反之就没有密码,labeld的内容根据密码的有无来确定:
//UnlockView.m
self.oldPassword = [[NSUserDefaults standardUserDefaults]objectForKey:@"pwd"];
if (!self.oldPassword) {
//请绘制密码
self.titleLabel.text = @"请设置图案密码";
}else{
//有密码啦,请输入密码
self.titleLabel.text = @"请绘制密码";
}
接下来就是绘制图案时记录选中的按钮,由于我们之前给按钮设置了tag值,所以最终的密码可以由一组tag值确定(我们这里设置的tag值都是一位的):
//touchesBegin
//判断某一个点是否在区域内
for (UIButton *button in self.subviews) {
if (CGRectContainsPoint(button.frame, location)) {
//设置按钮的状态
button.selected = YES;
[self.selectedArray addObject:button];
//记录密码
[self.pwdString appendFormat:@"%d",(int)button.tag];
}
}
//touchesMoved
for (UIButton *button in self.subviews) {
if (CGRectContainsPoint(button.frame, location)) {
if (button.selected==NO) {
//设置按钮的状态
button.selected = YES;
[self.selectedArray addObject:button];
//记录密码
[self.pwdString appendFormat:@"%d",(int)button.tag];
}
}
}
我们使用一个 pwdString来保存绘制的密码。
最后当手指离开屏幕时,说明密码绘制完成了,这时我们需要判断此时是第一次设置密码还是输入解锁的密码:
-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
if (self.oldPassword.length>0) {
if ([_oldPassword isEqualToString:self.pwdString]) {
self.titleLabel.text = @"解锁成功";
}else{
self.titleLabel.text = @"解锁失败,请重新绘制";
}
}else{
if (self.firstString.length==0) {
self.firstString = [NSString stringWithString:self.pwdString];
self.titleLabel.text = @"请确认刚刚绘制的密码图案";
}else{
if (![self.firstString isEqualToString:self.pwdString]) {
self.titleLabel.text = @"两次密码绘制不相同,请重新绘制";
self.firstString = @"";
}else{
self.titleLabel.text = @"密码设置成功";
[[NSUserDefaults standardUserDefaults]setObject:self.firstString forKey:@"pwd"];
}
}
}
//现将所有点亮的按钮的状态改变一下
for (UIButton * button in self.selectedArray) {
button.selected = NO;
}
//i清空数组
[self.selectedArray removeAllObjects];
//刷新屏幕
[self setNeedsDisplay];
[self.pwdString setString:@""];
}
基本的逻辑就是首先判断是第一次设置密码,还是直接绘制密码解锁,而在前一种情况下还要判断是第一次设置密码,还是第二次确认密码,而在第二次确认密码的时候,又要分确认密码正确和确认密码错误。密码的逻辑操作之后我们需要将所有按钮的选中状态设置为no,清空数组,刷新屏幕,还要把临时性的 pwdString设置为 “”。
具体demo地址:iOS UIBezierPath实现手势解锁