您好,登錄后才能下訂單哦!
題記
在平常的生活中,我們大概經(jīng)常遇見手勢滑動解鎖---也就是九宮格啊,已經(jīng)出現(xiàn)好久了,雖然隨著Apple的指紋解鎖的發(fā)展手勢解鎖雖然還有但是因?yàn)槠洳蝗缰讣y解鎖方便也用的也少了,但是在大多數(shù)APP中這兩種方式都是并存的,比如qq,微信,支付寶等等,最近項(xiàng)目里面也剛好有這個需求,趁著剛完成抽出時間來記錄下來當(dāng)時的一些思路,可能有的地方理解的不到位,還需多總結(jié),閑言少敘了,看重點(diǎn)。
功能描述如圖:大概說一下思路,這個功能用來做相當(dāng)于密令,用于兩端的匹配,教師端設(shè)置了路徑生成密碼,儲存在本地,學(xué)生端用來滑動輸入進(jìn)行驗(yàn)證。然后根據(jù)各種情況上部的label給與各種提示信息,這里只是一個比較原始簡陋的demo,也只是實(shí)現(xiàn)了我們最常見的功能,所以重在領(lǐng)會其中的精神了,哈哈哈。
功能模塊分析
根據(jù)GIF可以簡單的把這塊兒功能分為幾個部分來理解,第一個就是首頁:ViewController,首頁里面有兩個Controller分別是StudViewController和TeacViewController用來分作不同功能的載體。在StudViewController中分為三部分上中下statusLabel GestureLockView clearBtn,其中GestureLockView是手勢解鎖界面。在TeacViewController中也是分為三部分,statusLabel GestureLockView BottomView下方的bottomView分為兩個button resetBtn重置按鈕和sureBtn確定按鈕。說完大體的結(jié)構(gòu),接下來分部分說一下每個功能的實(shí)現(xiàn)思路。
拆解分析
首先說一下GestureLockView這個view控件:
首先在.h 文件中
定義兩個枚舉:分別用來定義兩端的不同類型,stu端用ResultKindType來對畫的手勢結(jié)果進(jìn)行分類分為這四類,下面都會用到,并說明用途。teac端用TeacKindType來分兩類:
//檢測手勢密碼答案情況 對/錯/不夠4個數(shù)字 typedef NS_ENUM(NSUInteger, ResultKindType) { ResultKindTypeTrue, ResultKindTypeFalse, ResultKindTypeNoEnough, ResultKindTypeClear }; typedef NS_ENUM(NSUInteger, TeacKindType) { TeacKindTypeNoEnough, TeacKindTypeTrue };
協(xié)議:用來監(jiān)聽手勢變化時傳出的轉(zhuǎn)化的密碼(這個密碼是用button的tag值來表示的),因?yàn)槭謩菔窃谧兓?,所以這里用了NSMutableString向外傳遞。
@protocol GestureLockDelegate <NSObject> - (void)gestureLockView:(GestureLockView *)lockView drawRectFinished:(NSMutableString *)gesturePassword; @end
屬性:
方法:
- (void)clearLockView;//清除布局 重新開始 - (void)checkPwdResult:(ResultKindType)resultType;//檢測學(xué)生端的結(jié)果 - (void)checkTeacResult:(TeacKindType)resultType;//檢測老師端的結(jié)果
.m文件中(具體創(chuàng)建和應(yīng)用的方法)
聲明變量供下方使用:
@interface GestureLockView () @property (strong, nonatomic) NSMutableArray *selectBtns;//選中的按鈕數(shù)組 @property (nonatomic, strong) NSMutableArray *errorBtns;//錯誤的按鈕數(shù)組 @property(nonatomic, assign)BOOL finished;//是否完成 @property (nonatomic, assign) CGPoint currentPoint;//當(dāng)前觸摸點(diǎn) @property (nonatomic, assign) ResultKindType resultType;//學(xué)生端結(jié)果 @property (nonatomic, assign) TeacKindType teacResultType;//教師端結(jié)果 @end
懶加載初始化數(shù)組
- (NSMutableArray *)selectBtns { if (!_selectBtns) { _selectBtns = [NSMutableArray array]; } return _selectBtns; } - (NSMutableArray *)errorBtns { if (!_errorBtns) { _errorBtns = [NSMutableArray array]; } return _errorBtns; }
子視圖初始化:在這里創(chuàng)建9個按鈕并將它們add到self中,并給self 添加UIPanGestureRecognizer *pan手勢
- (void)initSubviews { self.backgroundColor = [UIColor clearColor]; UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)]; [self addGestureRecognizer:pan]; //創(chuàng)建9個按鈕 for (NSInteger i = 0; i < 9; i++) { UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom]; btn.userInteractionEnabled = NO; [btn setImage:[UIImage imageNamed:@"sign_img_circle_n"] forState:UIControlStateNormal]; [btn setImage:[UIImage imageNamed:@"sign_img_circle_s"] forState:UIControlStateSelected]; btn.tag = i+1; [self addSubview:btn]; } }
界面布局:
CGFloat minWidth = MIN(self.bounds.size.height, self.bounds.size.width); CGFloat boundsWidth = self.bounds.size.width; CGFloat margin = (minWidth - cols * w) / (cols + 1);//間距 CGFloat xMargin = (boundsWidth-2*margin-3*w)/2;
這里這一塊是對不同機(jī)型進(jìn)行的適配,因?yàn)檫@個控件有可能會被添加在一個height<width的view上,所以在這里margin是指比較短的長度也就是上下的height,xMargin是用來對width進(jìn)行伸展的,這塊是這個邏輯,應(yīng)該可以通用的。
- (void)layoutSubviews { [super layoutSubviews]; NSUInteger count = self.subviews.count; int cols = 3;//總列數(shù) CGFloat x = 0,y = 0,w = 0,h = 0; if (SCREEN_WIDTH == 320) { w = 50; h = 50; } else { w = 60; h = 60; } CGFloat minWidth = MIN(self.bounds.size.height, self.bounds.size.width); CGFloat boundsWidth = self.bounds.size.width; CGFloat margin = (minWidth - cols * w) / (cols + 1);//間距 CGFloat xMargin = (boundsWidth-2*margin-3*w)/2; CGFloat col = 0; CGFloat row = 0; for (int i = 0; i < count; i++) { col = i % cols; row = i / cols; if (i == 0 || i == 3 || i == 6) { x = xMargin; } else if (i == 1 || i == 4 || i == 7) { x = xMargin + w + margin; } else { x = xMargin + 2 * (margin+w); } y = (w+margin)*row; UIButton *btn = self.subviews[i]; btn.frame = CGRectMake(x, y, w, h); } }
手勢代理方法: 在手勢代理方法中監(jiān)聽手勢滑動位置的改變,當(dāng)pan.state == UIGestureRecognizerStateBegan開始滑動的時候,從盛放顯示錯誤狀態(tài)的button改變?yōu)槠胀ǔ跏紶顟B(tài),并且將self.errorBtns數(shù)組清除。將_currentPoint = [pan locationInView:self];檢測當(dāng)滑動時當(dāng)前的位置point,如果劃過的位置包含在button的范圍內(nèi),如果button.selected == NO那么將button.selected = YES;//設(shè)置為選中并且將button添加到self.selectBtns數(shù)組中[self.selectBtns addObject:button];調(diào)用[self setNeedsDisplay];方法進(jìn)行重繪,調(diào)用setNeedsDisplay方法系統(tǒng)會自動調(diào)用drawReact方法進(jìn)行界面的重新布局。最后監(jiān)聽手指是否松開//監(jiān)聽手指松開 if (pan.state == UIGestureRecognizerStateEnded) { self.finished = YES; }如果松開將self.finished = YES;
#pragma mark 手勢 - (void)pan:(UIPanGestureRecognizer *)pan { if (pan.state == UIGestureRecognizerStateBegan) { for (UIButton *btn in _errorBtns) { [btn setImage:[UIImage imageNamed:@"sign_img_circle_n"] forState:UIControlStateNormal]; [btn setImage:[UIImage imageNamed:@"sign_img_circle_s"] forState:UIControlStateSelected]; } [self.errorBtns removeAllObjects]; } _currentPoint = [pan locationInView:self]; for (UIButton *button in self.subviews) { if (CGRectContainsPoint(button.frame, _currentPoint)) { if (button.selected == NO) { //點(diǎn)在按鈕上 button.selected = YES;//設(shè)置為選中 [self.selectBtns addObject:button]; } else { } } } //重繪 [self setNeedsDisplay]; //監(jiān)聽手指松開 if (pan.state == UIGestureRecognizerStateEnded) { self.finished = YES; } }
傳遞設(shè)置的手勢密碼方法
//傳遞設(shè)置的手勢密碼 - (NSMutableString *)transferGestureResult { //創(chuàng)建可變字符串 NSMutableString *result = [NSMutableString string]; for (UIButton *btn in self.selectBtns) { [result appendFormat:@"%ld", btn.tag - 1]; } return result; }
兩端的外部操作對內(nèi)部狀態(tài)的改變:
case ResultKindTypeFalse: _errorBtns = [NSMutableArray arrayWithArray:self.selectBtns]; break;
這里如果繪制錯誤,將之前選中的按鈕放在 _errorBtns盛放錯誤按鈕的數(shù)組中
case ResultKindTypeClear: { [[UIColor clearColor] set]; for (int i = 0; i < self.errorBtns.count; i++) { UIButton *btn = [self.errorBtns objectAtIndex:i]; [btn setImage:[UIImage imageNamed:@"sign_img_circle_n"] forState:UIControlStateNormal]; } [self.errorBtns removeAllObjects]; } break;
當(dāng)外界改變狀態(tài)為“清除”時,將路徑置為透明 并改變errorBtns數(shù)組中button的背景色狀態(tài),并且將errorBtns清空,這里為什么在這里做這些改變呢?這是因?yàn)樵趯W(xué)生端進(jìn)行清除操作的時候,手勢的狀態(tài)已經(jīng)是finish并且這是后代理方法已經(jīng)走完,并且我們在調(diào)用clearLockView方法的時候也將selectBtns數(shù)組清空了,因此在setNeedsDisplay被調(diào)用,drawReact進(jìn)行重繪的時候,檢測到if (_selectBtns.count == 0) return;也就不再繼續(xù)往下進(jìn)行了。
//學(xué)生端狀態(tài)改變 - (void)checkPwdResult:(ResultKindType)resultType { self.resultType = resultType; switch (resultType) { case ResultKindTypeFalse: _errorBtns = [NSMutableArray arrayWithArray:self.selectBtns]; break; case ResultKindTypeTrue: break; case ResultKindTypeNoEnough: break; case ResultKindTypeClear: { [[UIColor clearColor] set]; for (int i = 0; i < self.errorBtns.count; i++) { UIButton *btn = [self.errorBtns objectAtIndex:i]; [btn setImage:[UIImage imageNamed:@"sign_img_circle_n"] forState:UIControlStateNormal]; } [self.errorBtns removeAllObjects]; } break; default: break; } [self clearLockView]; } //教師端狀態(tài)改變 - (void)checkTeacResult:(TeacKindType)resultType { self.teacResultType = resultType; switch (resultType) { case TeacKindTypeTrue: break; case TeacKindTypeNoEnough:{ [self clearLockView]; } break; default: break; } }
清除方法: 這里將self.finished = NO;因?yàn)槿绻峭ㄟ^代理將值傳到外界并且外界對該值進(jìn)行了校驗(yàn),對內(nèi)容的resultType進(jìn)行改變,這時候其實(shí)還是在drawReact方法中,并且已經(jīng)走完了self.finished 方法,在這里將其設(shè)置為NO 是為了改變下一次繪制的finished狀態(tài),雖然不起眼,但是也是寫出來了。
- (void)clearLockView { self.finished = NO; //遍歷所有選中的按鈕 for (UIButton *btn in self.selectBtns) { //取消選中狀態(tài) btn.selected = NO; } [self.selectBtns removeAllObjects]; // [self setNeedsDisplay]; }
利用貝塞爾曲線繪制路徑,并根據(jù)對應(yīng)的狀態(tài)修改路徑顏色,按鈕顏色,當(dāng)系統(tǒng)調(diào)用這個方法的時候會進(jìn)行重繪,重繪時,從self.selectBtns數(shù)組中取出選中的button,當(dāng)button是第一個的時候?qū)⑵湓O(shè)置為bezierPath的起點(diǎn)[path moveToPoint:btn.center];其余的按鈕繪制路徑[path addLineToPoint:btn.center];。在這里判斷是否松開手指,在這個判斷里邏輯判斷比較多,大致捋一下,當(dāng)self.finished == YES的時候,就將創(chuàng)建的密碼利用先前聲明的代理傳遞出去在外部進(jìn)行檢驗(yàn)。根據(jù)外部返回的操作狀態(tài),如果是self.isTeac根據(jù)返回的結(jié)果狀態(tài)進(jìn)行判斷
case TeacKindTypeNoEnough: { [[UIColor clearColor] set]; } break; case TeacKindTypeTrue: { [[UIColor colorWithRed:94/255.0 green:195/255.0 blue:49/255.0 alpha:0.8] set]; }
如果畫的點(diǎn)不足4個,那么清除選中的點(diǎn)并且將繪制路徑的顏色透明(或者如果考慮到性能的話,最好用和背景色一樣的顏色),如果是繪制完成,用“綠色”填充,這里我選擇了教師繪制完畢不清楚繪制路徑,以便查看。
如果是學(xué)生端的話:繪制正確清除路徑,錯誤用“紅色”標(biāo)識路徑并從盛放錯誤按鈕的數(shù)組self.errorBtns中取出按鈕進(jìn)行狀態(tài)的改變。
switch (self.resultType) { case ResultKindTypeTrue: { //正確 [[UIColor clearColor] set]; } break; case ResultKindTypeFalse: { //錯誤 [[UIColor redColor] set]; for (int i = 0; i < self.errorBtns.count; i++) { UIButton *btn = [self.errorBtns objectAtIndex:i]; [btn setImage:[UIImage imageNamed:@"sign_img_circle_p"] forState:UIControlStateNormal]; } break; case ResultKindTypeNoEnough: { [[UIColor clearColor] set]; } break; case ResultKindTypeClear: break; default: break; }
之后便是若沒有finish 就是在繪制路徑了,對path進(jìn)行一些相關(guān)設(shè)置。
- (void)drawRect:(CGRect)rect { if (_selectBtns.count == 0) return; // 把所有選中按鈕中心點(diǎn)連線 UIBezierPath *path = [UIBezierPath bezierPath]; for (int i = 0; i < self.selectBtns.count; i ++) { UIButton *btn = self.selectBtns[i]; if (i == 0) { [path moveToPoint:btn.center]; // 設(shè)置起點(diǎn) } else { [path addLineToPoint:btn.center]; } } //判斷是否松開手指 if (self.finished) { //松開手 NSMutableString *pwd = [self transferGestureResult];//傳遞創(chuàng)建的密碼 [[UIColor colorWithRed:94/255.0 green:195/255.0 blue:49/255.0 alpha:0.8] set]; if ([self.delegate respondsToSelector:@selector(gestureLockView:drawRectFinished:)]) { [self.delegate gestureLockView:self drawRectFinished:pwd]; } if (self.isTeac) { //教師端 switch (self.teacResultType) { case TeacKindTypeNoEnough: { [[UIColor clearColor] set]; } break; case TeacKindTypeTrue: { [[UIColor colorWithRed:94/255.0 green:195/255.0 blue:49/255.0 alpha:0.8] set]; } break; default: break; } } else { switch (self.resultType) { case ResultKindTypeTrue: { //正確 [[UIColor clearColor] set]; } break; case ResultKindTypeFalse: { //錯誤 [[UIColor redColor] set]; for (int i = 0; i < self.errorBtns.count; i++) { UIButton *btn = [self.errorBtns objectAtIndex:i]; [btn setImage:[UIImage imageNamed:@"sign_img_circle_p"] forState:UIControlStateNormal]; } break; case ResultKindTypeNoEnough: { [[UIColor clearColor] set]; } break; case ResultKindTypeClear: break; default: break; } } } } else { [path addLineToPoint:self.currentPoint]; [[UIColor colorWithRed:94/255.0 green:195/255.0 blue:49/255.0 alpha:0.8] set]; } path.lineWidth = 1; path.lineJoinStyle = kCGLineCapRound; path.lineCapStyle = kCGLineCapRound; [path stroke]; }
ViewController
這個比較容易理解:分別跳轉(zhuǎn)兩個界面:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.row == 0) { TeacViewController *vc = [[TeacViewController alloc] init]; [self.navigationController pushViewController:vc animated:YES]; } else { StudViewController *vc = [[StudViewController alloc] init]; [self.navigationController pushViewController:vc animated:YES]; } }
TeacViewController
主要的應(yīng)用就是調(diào)用GestureLockViewDelegate了,接受并校驗(yàn)密碼,
#pragma mark gestureLockView代理事件 - (void)gestureLockView:(GestureLockView *)lockView drawRectFinished:(NSMutableString *)gesturePassword { [self createGesturesPassword:gesturePassword]; } //創(chuàng)建手勢密碼 - (void)createGesturesPassword:(NSMutableString *)gesturePassword { if (self.lastGesturePsw.length == 0) { if (gesturePassword.length < 4) { self.lastGesturePsw = nil; [self.gestureLockView checkTeacResult:TeacKindTypeNoEnough]; self.statusLabel.text = @"至少連接4個點(diǎn),重新輸入"; [self shakeAnimationForView:self.statusLabel]; return; } self.lastGesturePsw = gesturePassword; [self.gestureLockView checkTeacResult:TeacKindTypeTrue]; NSLog(@"---%@", self.lastGesturePsw); self.statusLabel.text = [NSString stringWithFormat:@"密碼是%@", gesturePassword]; } }
兩個按鈕的點(diǎn)擊事件
#pragma mark 按鈕點(diǎn)擊事件 //重置按鈕 - (void)resetBtnClick:(UIButton *)btn { self.lastGesturePsw = nil; [TeacViewController addGesturePassword:@""]; [self.gestureLockView checkTeacResult:TeacKindTypeNoEnough]; self.statusLabel.text = @"請繪制手勢密碼"; NSLog(@"resetPwd == %@, resetUserDefaultsPwd == %@", self.lastGesturePsw, [TeacViewController gesturePassword]); } //確定按鈕 - (void)sureBtnClick:(UIButton *)btn { if (!self.lastGesturePsw) { self.statusLabel.text = @"請繪制手勢密碼"; return; } [TeacViewController addGesturePassword:self.lastGesturePsw]; [self.gestureLockView checkTeacResult:TeacKindTypeTrue]; self.statusLabel.text = @"密碼設(shè)置成功"; NSLog(@"resetPwd == %@, resetUserDefaultsPwd == %@", self.lastGesturePsw, [TeacViewController gesturePassword]); // [self.navigationController popViewControllerAnimated:YES]; }
用本地存儲進(jìn)行模擬
#pragma mark 本地存儲模擬 + (void)deleteGestuesPassword { [[NSUserDefaults standardUserDefaults] removeObjectForKey:GESPWD]; [[NSUserDefaults standardUserDefaults] synchronize]; } + (void)addGesturePassword:(NSString *)gesturePassword { [[NSUserDefaults standardUserDefaults] setObject:gesturePassword forKey:GESPWD]; [[NSUserDefaults standardUserDefaults] synchronize]; } + (NSString *)gesturePassword { return [[NSUserDefaults standardUserDefaults] objectForKey:GESPWD]; }
StudViewController
GestureLockDelegate代理方法 并校驗(yàn)對當(dāng)前的手勢密碼和本地存儲的教師端密碼
#pragma mark 手勢密碼界面代理 - (void)gestureLockView:(GestureLockView *)lockView drawRectFinished:(NSMutableString *)gesturePassword { [self validateGesturePassword:gesturePassword]; } //校驗(yàn)手勢密碼 - (void)validateGesturePassword:(NSMutableString *)gesturePassword { if (gesturePassword.length < 4) { self.statusLabel.text = @"至少連接4個點(diǎn),重新輸入"; [self.gestureLockView checkPwdResult:ResultKindTypeNoEnough]; [self shakeAnimationForView:self.statusLabel]; return; } self.lastGesturePsw = gesturePassword; /*滑完直接校驗(yàn)*/ NSLog(@"validPwd == %@, validUserDefaultsPwd == %@", self.lastGesturePsw, [StudViewController gesturePassword]); static NSInteger errorCount = 5; if ([self.lastGesturePsw isEqualToString:[StudViewController gesturePassword]]) { [self.gestureLockView checkPwdResult:ResultKindTypeTrue]; self.statusLabel.text = @"密碼校驗(yàn)成功"; [self shakeAnimationForView:self.statusLabel]; } else { [self.gestureLockView checkPwdResult:ResultKindTypeFalse]; errorCount = errorCount - 1; if (errorCount == 0) { //已經(jīng)輸錯5次 self.statusLabel.text = @"請重新輸入密碼"; errorCount = 5; return; } self.statusLabel.text = [NSString stringWithFormat:@"密碼錯誤, 還可以再輸入%ld次", errorCount]; [self shakeAnimationForView:self.statusLabel]; } }
清除按鈕點(diǎn)擊事件 改變self.gestureLockView中的resultType 進(jìn)行界面的重繪等調(diào)整
- (void)clearBtnClick:(UIButton *)btn { self.statusLabel.text = @"清除!!!"; [self.gestureLockView checkPwdResult:ResultKindTypeClear]; [self shakeAnimationForView:self.statusLabel]; }
本地存儲:這里也有一個本地存儲 和教師端對應(yīng) (其實(shí)可以單獨(dú)封裝出來的)
#pragma mark 本地存儲模擬 + (void)deleteGestuesPassword { [[NSUserDefaults standardUserDefaults] removeObjectForKey:GESPWD]; [[NSUserDefaults standardUserDefaults] synchronize]; } + (void)addGesturePassword:(NSString *)gesturePassword { [[NSUserDefaults standardUserDefaults] setObject:gesturePassword forKey:GESPWD]; [[NSUserDefaults standardUserDefaults] synchronize]; } + (NSString *)gesturePassword { return [[NSUserDefaults standardUserDefaults] objectForKey:GESPWD]; }
以上,就是這個功能實(shí)現(xiàn)的大體流程了,捋順了思路來看其實(shí)還是蠻明確的,當(dāng)時做的時候也是走路挖坑填坑的,發(fā)現(xiàn)這樣把自己的思路寫下來還真是蠻有收獲的,希望自己更好的進(jìn)步,如果您看到這兒覺得有不合理的地方歡迎隨時和我溝通哈。
源代碼連接:https://github.com/irembeu/LGJGestureLockDemo.git
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。