題記
在平常的生活中,我們大概經常遇見手勢滑動解鎖---也就是九宮格啊,已經出現好久了,雖然隨著Apple的指紋解鎖的發展手勢解鎖雖然還有但是因為其不如指紋解鎖方便也用的也少了,但是在大多數APP中這兩種方式都是并存的,比如qq,微信,支付寶等等,最近項目里面也剛好有這個需求,趁著剛完成抽出時間來記錄下來當時的一些思路,可能有的地方理解的不到位,還需多總結,閑言少敘了,看重點。
功能描述如圖:大概說一下思路,這個功能用來做相當于密令,用于兩端的匹配,教師端設置了路徑生成密碼,儲存在本地,學生端用來滑動輸入進行驗證。然后根據各種情況上部的label給與各種提示信息,這里只是一個比較原始簡陋的demo,也只是實現了我們最常見的功能,所以重在領會其中的精神了,哈哈哈。

功能模塊分析
根據GIF可以簡單的把這塊兒功能分為幾個部分來理解,第一個就是首頁:ViewController,首頁里面有兩個Controller分別是StudViewController和TeacViewController用來分作不同功能的載體。在StudViewController中分為三部分上中下statusLabel GestureLockView clearBtn,其中GestureLockView是手勢解鎖界面。在TeacViewController中也是分為三部分,statusLabel GestureLockView BottomView下方的bottomView分為兩個button resetBtn重置按鈕和sureBtn確定按鈕。說完大體的結構,接下來分部分說一下每個功能的實現思路。
拆解分析
首先說一下GestureLockView這個view控件:
首先在.h 文件中
定義兩個枚舉:分別用來定義兩端的不同類型,stu端用ResultKindType來對畫的手勢結果進行分類分為這四類,下面都會用到,并說明用途。teac端用TeacKindType來分兩類:
//檢測手勢密碼答案情況 對/錯/不夠4個數字
typedef NS_ENUM(NSUInteger, ResultKindType) {
ResultKindTypeTrue,
ResultKindTypeFalse,
ResultKindTypeNoEnough,
ResultKindTypeClear
};
typedef NS_ENUM(NSUInteger, TeacKindType) {
TeacKindTypeNoEnough,
TeacKindTypeTrue
};
協議:用來監聽手勢變化時傳出的轉化的密碼(這個密碼是用button的tag值來表示的),因為手勢是在變化的,所以這里用了NSMutableString向外傳遞。
@protocol GestureLockDelegate <NSObject> - (void)gestureLockView:(GestureLockView *)lockView drawRectFinished:(NSMutableString *)gesturePassword; @end
屬性:
方法:
- (void)clearLockView;//清除布局 重新開始 - (void)checkPwdResult:(ResultKindType)resultType;//檢測學生端的結果 - (void)checkTeacResult:(TeacKindType)resultType;//檢測老師端的結果
.m文件中(具體創建和應用的方法)
聲明變量供下方使用:
@interface GestureLockView () @property (strong, nonatomic) NSMutableArray *selectBtns;//選中的按鈕數組 @property (nonatomic, strong) NSMutableArray *errorBtns;//錯誤的按鈕數組 @property(nonatomic, assign)BOOL finished;//是否完成 @property (nonatomic, assign) CGPoint currentPoint;//當前觸摸點 @property (nonatomic, assign) ResultKindType resultType;//學生端結果 @property (nonatomic, assign) TeacKindType teacResultType;//教師端結果 @end
懶加載初始化數組
- (NSMutableArray *)selectBtns {
if (!_selectBtns) {
_selectBtns = [NSMutableArray array];
}
return _selectBtns;
}
- (NSMutableArray *)errorBtns {
if (!_errorBtns) {
_errorBtns = [NSMutableArray array];
}
return _errorBtns;
}
子視圖初始化:在這里創建9個按鈕并將它們add到self中,并給self 添加UIPanGestureRecognizer *pan手勢
- (void)initSubviews {
self.backgroundColor = [UIColor clearColor];
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
[self addGestureRecognizer:pan];
//創建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;
這里這一塊是對不同機型進行的適配,因為這個控件有可能會被添加在一個height<width的view上,所以在這里margin是指比較短的長度也就是上下的height,xMargin是用來對width進行伸展的,這塊是這個邏輯,應該可以通用的。
- (void)layoutSubviews {
[super layoutSubviews];
NSUInteger count = self.subviews.count;
int cols = 3;//總列數
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);
}
}
手勢代理方法: 在手勢代理方法中監聽手勢滑動位置的改變,當pan.state == UIGestureRecognizerStateBegan開始滑動的時候,從盛放顯示錯誤狀態的button改變為普通初始狀態,并且將self.errorBtns數組清除。將_currentPoint = [pan locationInView:self];檢測當滑動時當前的位置point,如果劃過的位置包含在button的范圍內,如果button.selected == NO那么將button.selected = YES;//設置為選中并且將button添加到self.selectBtns數組中[self.selectBtns addObject:button];調用[self setNeedsDisplay];方法進行重繪,調用setNeedsDisplay方法系統會自動調用drawReact方法進行界面的重新布局。最后監聽手指是否松開//監聽手指松開 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) {
//點在按鈕上
button.selected = YES;//設置為選中
[self.selectBtns addObject:button];
} else {
}
}
}
//重繪
[self setNeedsDisplay];
//監聽手指松開
if (pan.state == UIGestureRecognizerStateEnded) {
self.finished = YES;
}
}
傳遞設置的手勢密碼方法
//傳遞設置的手勢密碼
- (NSMutableString *)transferGestureResult {
//創建可變字符串
NSMutableString *result = [NSMutableString string];
for (UIButton *btn in self.selectBtns) {
[result appendFormat:@"%ld", btn.tag - 1];
}
return result;
}
兩端的外部操作對內部狀態的改變:
case ResultKindTypeFalse: _errorBtns = [NSMutableArray arrayWithArray:self.selectBtns]; break;
這里如果繪制錯誤,將之前選中的按鈕放在 _errorBtns盛放錯誤按鈕的數組中
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;
當外界改變狀態為“清除”時,將路徑置為透明 并改變errorBtns數組中button的背景色狀態,并且將errorBtns清空,這里為什么在這里做這些改變呢?這是因為在學生端進行清除操作的時候,手勢的狀態已經是finish并且這是后代理方法已經走完,并且我們在調用clearLockView方法的時候也將selectBtns數組清空了,因此在setNeedsDisplay被調用,drawReact進行重繪的時候,檢測到if (_selectBtns.count == 0) return;也就不再繼續往下進行了。
//學生端狀態改變
- (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];
}
//教師端狀態改變
- (void)checkTeacResult:(TeacKindType)resultType {
self.teacResultType = resultType;
switch (resultType) {
case TeacKindTypeTrue:
break;
case TeacKindTypeNoEnough:{
[self clearLockView];
}
break;
default:
break;
}
}
清除方法: 這里將self.finished = NO;因為如果是通過代理將值傳到外界并且外界對該值進行了校驗,對內容的resultType進行改變,這時候其實還是在drawReact方法中,并且已經走完了self.finished 方法,在這里將其設置為NO 是為了改變下一次繪制的finished狀態,雖然不起眼,但是也是寫出來了。
- (void)clearLockView {
self.finished = NO;
//遍歷所有選中的按鈕
for (UIButton *btn in self.selectBtns) {
//取消選中狀態
btn.selected = NO;
}
[self.selectBtns removeAllObjects];
//
[self setNeedsDisplay];
}
利用貝塞爾曲線繪制路徑,并根據對應的狀態修改路徑顏色,按鈕顏色,當系統調用這個方法的時候會進行重繪,重繪時,從self.selectBtns數組中取出選中的button,當button是第一個的時候將其設置為bezierPath的起點[path moveToPoint:btn.center];其余的按鈕繪制路徑[path addLineToPoint:btn.center];。在這里判斷是否松開手指,在這個判斷里邏輯判斷比較多,大致捋一下,當self.finished == YES的時候,就將創建的密碼利用先前聲明的代理傳遞出去在外部進行檢驗。根據外部返回的操作狀態,如果是self.isTeac根據返回的結果狀態進行判斷
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];
}
如果畫的點不足4個,那么清除選中的點并且將繪制路徑的顏色透明(或者如果考慮到性能的話,最好用和背景色一樣的顏色),如果是繪制完成,用“綠色”填充,這里我選擇了教師繪制完畢不清楚繪制路徑,以便查看。
如果是學生端的話:繪制正確清除路徑,錯誤用“紅色”標識路徑并從盛放錯誤按鈕的數組self.errorBtns中取出按鈕進行狀態的改變。
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進行一些相關設置。
- (void)drawRect:(CGRect)rect {
if (_selectBtns.count == 0) return;
// 把所有選中按鈕中心點連線
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]; // 設置起點
} else {
[path addLineToPoint:btn.center];
}
}
//判斷是否松開手指
if (self.finished) {
//松開手
NSMutableString *pwd = [self transferGestureResult];//傳遞創建的密碼
[[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
這個比較容易理解:分別跳轉兩個界面:
- (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
主要的應用就是調用GestureLockViewDelegate了,接受并校驗密碼,
#pragma mark gestureLockView代理事件
- (void)gestureLockView:(GestureLockView *)lockView drawRectFinished:(NSMutableString *)gesturePassword {
[self createGesturesPassword:gesturePassword];
}
//創建手勢密碼
- (void)createGesturesPassword:(NSMutableString *)gesturePassword {
if (self.lastGesturePsw.length == 0) {
if (gesturePassword.length < 4) {
self.lastGesturePsw = nil;
[self.gestureLockView checkTeacResult:TeacKindTypeNoEnough];
self.statusLabel.text = @"至少連接4個點,重新輸入";
[self shakeAnimationForView:self.statusLabel];
return;
}
self.lastGesturePsw = gesturePassword;
[self.gestureLockView checkTeacResult:TeacKindTypeTrue];
NSLog(@"---%@", self.lastGesturePsw);
self.statusLabel.text = [NSString stringWithFormat:@"密碼是%@", gesturePassword];
}
}
兩個按鈕的點擊事件
#pragma mark 按鈕點擊事件
//重置按鈕
- (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 = @"密碼設置成功";
NSLog(@"resetPwd == %@, resetUserDefaultsPwd == %@", self.lastGesturePsw, [TeacViewController gesturePassword]);
// [self.navigationController popViewControllerAnimated:YES];
}
用本地存儲進行模擬
#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代理方法 并校驗對當前的手勢密碼和本地存儲的教師端密碼
#pragma mark 手勢密碼界面代理
- (void)gestureLockView:(GestureLockView *)lockView drawRectFinished:(NSMutableString *)gesturePassword {
[self validateGesturePassword:gesturePassword];
}
//校驗手勢密碼
- (void)validateGesturePassword:(NSMutableString *)gesturePassword {
if (gesturePassword.length < 4) {
self.statusLabel.text = @"至少連接4個點,重新輸入";
[self.gestureLockView checkPwdResult:ResultKindTypeNoEnough];
[self shakeAnimationForView:self.statusLabel];
return;
}
self.lastGesturePsw = gesturePassword;
/*滑完直接校驗*/
NSLog(@"validPwd == %@, validUserDefaultsPwd == %@", self.lastGesturePsw, [StudViewController gesturePassword]);
static NSInteger errorCount = 5;
if ([self.lastGesturePsw isEqualToString:[StudViewController gesturePassword]]) {
[self.gestureLockView checkPwdResult:ResultKindTypeTrue];
self.statusLabel.text = @"密碼校驗成功";
[self shakeAnimationForView:self.statusLabel];
} else {
[self.gestureLockView checkPwdResult:ResultKindTypeFalse];
errorCount = errorCount - 1;
if (errorCount == 0) {
//已經輸錯5次
self.statusLabel.text = @"請重新輸入密碼";
errorCount = 5;
return;
}
self.statusLabel.text = [NSString stringWithFormat:@"密碼錯誤, 還可以再輸入%ld次", errorCount];
[self shakeAnimationForView:self.statusLabel];
}
}
清除按鈕點擊事件 改變self.gestureLockView中的resultType 進行界面的重繪等調整
- (void)clearBtnClick:(UIButton *)btn {
self.statusLabel.text = @"清除!!!";
[self.gestureLockView checkPwdResult:ResultKindTypeClear];
[self shakeAnimationForView:self.statusLabel];
}
本地存儲:這里也有一個本地存儲 和教師端對應 (其實可以單獨封裝出來的)
#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];
}
以上,就是這個功能實現的大體流程了,捋順了思路來看其實還是蠻明確的,當時做的時候也是走路挖坑填坑的,發現這樣把自己的思路寫下來還真是蠻有收獲的,希望自己更好的進步,如果您看到這兒覺得有不合理的地方歡迎隨時和我溝通哈。
源代碼連接:https://github.com/irembeu/LGJGestureLockDemo.git
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。