一、先來看一下最終效果
二、需要用到的主要知識
- 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實作手勢解鎖