前序
最近公司APP要做改版,其中要使用手势密码锁,所以提前写了一个小Demo练练手,该Demo参考了DBGuestureLock。这里将详细阐述代码细节,希望可以帮助到需要帮助的人。
相关界面GIF
代码实现
一、创建相关类
- GesturePasswordButton
该类主要是表示手势密码中的圆点 - GesturePasswordView
该类是手势密码View - GesturePasswordViewController
该类是用于使用GesturePasswordView
二、绘制界面
从GIF图可以看出,手势密码一共有三种状态(正常、选择、不正确),因此现在GesturePasswordView.h文件中创建一个枚举类型
typedef NS_ENUM(NSInteger, GesturePasswordButtonState) {
GesturePasswordButtonStateNormal = 0,
GesturePasswordButtonStateSelected,
GesturePasswordButtonStateIncorrect,
};
根据Button的形状,可以在GesturePasswordView.h中定义以下属性(之所以写在GesturePasswordView.h中是为了根据不同的状态改变button的样式)
@property (nonatomic, assign)CGFloat strokeWidth; //圆弧的宽度
@property (nonatomic, assign)CGFloat circleRadius; //半径
@property (nonatomic, assign)CGFloat centerPointRadius; //中心圆半径
@property (nonatomic, assign)CGFloat lineWidth; //连接线宽度
@property (nonatomic, strong)UIColor *strokeColor; //圆弧的填充颜色
@property (nonatomic, strong)UIColor *fillColor; //除中心圆点外 其他部分的填充色
@property (nonatomic, strong)UIColor *centerPointColor; //中心圆点的颜色
@property (nonatomic, strong)UIColor *lineColor; //线条填充颜色
@property (nonatomic, assign)BOOL showCenterPoint; //是否显示中心圆
@property (nonatomic, assign)BOOL fillCenterPoint; //是否填充中心圆
在GesturePasswordView.m文件中定义以下属性,用于后面的代码编写。(GesturePasswordStatus枚举类型将会在后面写出)
@property (nonatomic, strong) NSMutableArray *selectorAry;//存储已经选择的按钮
@property (nonatomic, assign)CGPoint currentPoint;//当前处于哪个按钮范围内
@property (nonatomic, assign)GesturePasswordStatus status;//当前控件器所处状态(设置、重新设置、登录)
@property (nonatomic, assign)NSInteger inputNum;//输入的次数
@property (nonatomic, assign)NSInteger resetInputNum;//重置密码时验证旧密码 输入的次数
@property (nonatomic, strong)NSString *firstPassword;//表示设置密码时 第一次输入的手势密码
在GesturePasswordView.m文件中写一个方法来改变button当前的属性值
- (void)setPropertiesByState:(GesturePasswordButtonState)buttonState{
switch (buttonState) {
case GesturePasswordButtonStateNormal:
self.fillCenterPoint = YES;
self.showCenterPoint = YES;
self.strokeWidth = 1.f;
self.centerPointRadius = 10.f;
self.lineWidth = 2.f;
self.lineColor = [UIColor whiteColor];
self.fillColor = [UIColor whiteColor];
self.strokeColor = [UIColor whiteColor];
self.centerPointColor = [UIColor grayColor];
break;
case GesturePasswordButtonStateSelected:
self.fillCenterPoint = YES;
self.showCenterPoint = YES;
self.strokeWidth = 1.f;
self.centerPointRadius = 10.f;
self.lineWidth = 2.f;
self.lineColor = [UIColor greenColor];
self.fillColor = RGBA(127, 255, 212, 1);
self.strokeColor = RGBA(127, 255, 212, 1);
self.centerPointColor = [UIColor greenColor];
break;
case GesturePasswordButtonStateIncorrect:
self.fillCenterPoint = YES;
self.showCenterPoint = YES;
self.strokeWidth = 1.f;
self.centerPointRadius = 10.f;
self.lineWidth = 2.f;
self.lineColor = [UIColor orangeColor];
self.fillColor = RGBA(255, 245, 238, 1);
self.strokeColor = RGBA(255, 245, 238, 1);
self.centerPointColor = [UIColor orangeColor];
break;
default:
break;
}
}
在GesturePasswordView.m文件中使用initWithFrame:(CGRect)frame方法绘制手势图,这里GesturePasswordView的大小以(320,320)为基准。
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.selectorAry = [[NSMutableArray alloc] init];
[self setPropertiesByState:GesturePasswordButtonStateNormal];
NSInteger distance = 320/3;
NSInteger size = distance/1.5;
NSInteger margin = size/4;
float ins = (SCREEN_WIDTH-320)/2;
self.circleRadius = size/2;
for (int i = 0; i < 9; i++) {
NSInteger row = i/3;
NSInteger col = i%3;
GesturePasswordButton *gesturePasswordButton = [[GesturePasswordButton alloc] initWithFrame:CGRectMake(ins+col*distance+margin, row*distance+margin, size, size)];
[gesturePasswordButton setTag:i+1];
[self addSubview:gesturePasswordButton];
}
[self setBackgroundColor:[UIColor clearColor]];
}
return self;
}
在GesturePasswordButton.m文件中重新initWithFrame:(CGRect)frame方法。
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.userInteractionEnabled = NO;
}
return self;
}
在GesturePasswordButton.m文件中重新- (void)drawRect:(CGRect)rect方法。
- 获取到父视图(GesturePasswordView),使用其相关描述button的属性。
__weak GesturePasswordView *gesView = nil;
if ([self.superview isKindOfClass:[GesturePasswordView class]]) {
gesView = (GesturePasswordView *)self.superview;
}
- 绘制外圈大圆
CGFloat radius = gesView.circleRadius - gesView.strokeWidth;
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, gesView.strokeWidth);
CGPoint centerPoint = CGPointMake(rect.size.width/2, rect.size.height/2);
CGFloat startAngle = -((CGFloat)M_PI/2);
CGFloat endAngle = ((2 * (CGFloat)M_PI) + startAngle);
[gesView.strokeColor setStroke];
CGContextAddArc(context, centerPoint.x, centerPoint.y, radius+gesView.strokeWidth/2, startAngle, endAngle, 0);
CGContextStrokePath(context);//CGContextStrokePath来描线,即形状
- 根据是否显示中心点绘制形状
if (gesView.showCenterPoint) {
[gesView.fillColor set];//同时设置填充和边框色
CGContextAddArc(context, centerPoint.x, centerPoint.y, radius, startAngle, endAngle, 0);
CGContextFillPath(context);//CGContextFillPath来填充形状内的颜色
if (gesView.fillCenterPoint) {
[gesView.centerPointColor set];//同时设置填充和边框色
}else{
[gesView.centerPointColor setStroke];//设置边框色
}
CGContextAddArc(context, centerPoint.x, centerPoint.y, gesView.centerPointRadius, startAngle, endAngle, 0);
if (gesView.fillCenterPoint) {
CGContextFillPath(context);//CGContextFillPath来填充形状内的颜色
}else{
CGContextStrokePath(context);//CGContextStrokePath来描线,即形状
}
}
整体drawRect方法如下:
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
__weak GesturePasswordView *gesView = nil;
if ([self.superview isKindOfClass:[GesturePasswordView class]]) {
gesView = (GesturePasswordView *)self.superview;
}
CGFloat width = rect.size.height > rect.size.width ? rect.size.width : rect.size.height;
CGFloat radius = (width - 2*gesView.strokeWidth)/2;
if (radius > (gesView.circleRadius - gesView.strokeWidth)) {
radius = gesView.circleRadius - gesView.strokeWidth;
}
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, gesView.strokeWidth);
CGPoint centerPoint = CGPointMake(rect.size.width/2, rect.size.height/2);
CGFloat startAngle = -((CGFloat)M_PI/2);
CGFloat endAngle = ((2 * (CGFloat)M_PI) + startAngle);
[gesView.strokeColor setStroke];
CGContextAddArc(context, centerPoint.x, centerPoint.y, radius+gesView.strokeWidth/2, startAngle, endAngle, 0);
CGContextStrokePath(context);//CGContextStrokePath来描线,即形状
if (gesView.showCenterPoint) {
[gesView.fillColor set];//同时设置填充和边框色
CGContextAddArc(context, centerPoint.x, centerPoint.y, radius, startAngle, endAngle, 0);
CGContextFillPath(context);//CGContextFillPath来填充形状内的颜色
if (gesView.fillCenterPoint) {
[gesView.centerPointColor set];//同时设置填充和边框色
}else{
[gesView.centerPointColor setStroke];//设置边框色
}
CGContextAddArc(context, centerPoint.x, centerPoint.y, gesView.centerPointRadius, startAngle, endAngle, 0);
if (gesView.fillCenterPoint) {
CGContextFillPath(context);//CGContextFillPath来填充形状内的颜色
}else{
CGContextStrokePath(context);//CGContextStrokePath来描线,即形状
}
}
}
这样的话只需要在GesturePasswordViewController中加载GesturePasswordView就可以显示出手势密码的界面。
三、实现三种手势状态
为了实现手势的三种状态,我们需要获取到用户的触摸动作,这里我们将使用UIResponder类,UIResponder类是专门用来响应用户的操作处理各种事件的,所以我们需要在GesturePasswordView.m实现touchesBegan、touchesMoved、touchesEnded方法。
- touchesBegan方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self];//获取触摸点在view中的位置
self.currentPoint = point;
for (GesturePasswordButton *btn in self.subviews) {
if (CGRectContainsPoint(btn.frame, point)) {//判断触摸点是否在当前btn的范围之内
[btn setSelected:YES];
if (![self.selectorAry containsObject:btn]) {
[self.selectorAry addObject:btn];
[self setPropertiesByState:GesturePasswordButtonStateSelected];//改变该btn的状态
}
}
}
[self setNeedsDisplay];//重绘view来画线
}
- touchesMoved方法
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesMoved:touches withEvent:event];
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self];
self.currentPoint = point;
for (GesturePasswordButton *btn in self.subviews) {
if (CGRectContainsPoint(btn.frame, point)) {
[btn setSelected:YES];
if (![self.selectorAry containsObject:btn]) {
[self.selectorAry addObject:btn];
[self setPropertiesByState:GesturePasswordButtonStateSelected];
}
}
}
[self setNeedsDisplay];
}
- touchesEnded方法
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
GesturePasswordButton *btn = [self.selectorAry lastObject];
[self setCurrentPoint:btn.center];
[self setNeedsDisplay];
}
- 调用drawRect方法 重绘view
- (void)drawRect:(CGRect)rect{
[super drawRect:rect];
if ([self.selectorAry count] == 0) {
return;
}
UIBezierPath *path = [UIBezierPath bezierPath];
[path setLineWidth:self.lineWidth];
[self.lineColor set];
[path setLineJoinStyle:kCGLineJoinRound];
[path setLineCapStyle:kCGLineCapRound];
for (NSInteger i = 0; i < self.selectorAry.count; i ++) {
GesturePasswordButton *btn = self.selectorAry[i];
if (i == 0) {
[path moveToPoint:[btn center]];
}else{
[path addLineToPoint:[btn center]];
}
[btn setNeedsDisplay];
}
[path addLineToPoint:self.currentPoint];
[path stroke];
}
- 为了实现当用户输入错误后,不正确状态显示0.5秒后恢复为正常状态,需要在上面实现的setPropertiesByState方法中加入如下代码,另外实现resetButtons和lockState方法
case GesturePasswordButtonStateNormal:
[self setUserInteractionEnabled:YES];\\设置当前用户可以进行触摸操作
[self resetButtons];
self.fillCenterPoint = YES;
case GesturePasswordButtonStateIncorrect:
[self setUserInteractionEnabled:NO];\\设置当前用户不可进行触摸操作
self.fillCenterPoint = YES;
self.showCenterPoint = YES;
self.strokeWidth = 1.f;
self.centerPointRadius = 10.f;
self.lineWidth = 2.f;
self.lineColor = [UIColor orangeColor];
self.fillColor = RGBA(255, 245, 238, 1);
self.strokeColor = RGBA(255, 245, 238, 1);
self.centerPointColor = [UIColor orangeColor];
[self performSelector:@selector(lockState:) withObject:[NSArray arrayWithObject:[NSNumber numberWithInteger:GesturePasswordButtonStateNormal]] afterDelay:0.5f];\\0.5秒后执行lockState方法
\\重置selectorAry数组
- (void)resetButtons {
for (NSInteger i=0; i<[self.selectorAry count]; i++) {
GesturePasswordButton *button = self.selectorAry[i];
[button setSelected:NO];
}
[self.selectorAry removeAllObjects];
[self setNeedsDisplay];
}
\\切换btn状态
- (void)lockState:(NSArray *)states {
NSNumber *stateNumber = [states objectAtIndex:0];
[self setPropertiesByState:[stateNumber integerValue]];
}
四、调用GesturePasswordView
现在我们要明确一下这个GesturePasswordView主要用于手势登录、设置手势密码、重置手势密码这三种情况。那么在GesturePasswordViewController.h中可以创建一个枚举类型。
typedef NS_ENUM(NSInteger, GesturePasswordStatus) {
GesturePasswordStatusSet = 0,\\设置
GesturePasswordStatusReset = 1,\\重置
GesturePasswordStatusLogin = 2,\\登录
};
登录、设置、重置三种状态下的流程图
根据流程图我们可以在GesturePasswordView.h中创建六种类型的Block方法
@property (nonatomic, copy) void(^verificationPassword)();//验证旧密码正确
@property (nonatomic, copy) void(^verificationError)();//验证旧密码错误
@property (nonatomic, copy) void(^onPasswordSet)();//第一次输入密码
@property (nonatomic, copy) void(^onGetCorrectPswd)();//第二次输入密码且和第一次一样
@property (nonatomic, copy) void(^onGetIncorrectPswd)();//第二次输入密码且和第一次不一样
@property (nonatomic, copy) void(^errorInput)();//手势密码小于四位数
根据三种状态我们可以在GesturePasswordView.h中创建三种初始化方法
+(instancetype)status:(GesturePasswordStatus)status frame:(CGRect)frame onGetCorrectPswd:(void (^)())GetCorrectPswd onGetIncorrectPswd:(void (^)())GetIncorrectPswd errorInput:(void (^)())errorInput;//用于登录
+(instancetype)status:(GesturePasswordStatus)status frame:(CGRect)frame onPasswordSet:(void (^)())onPasswordSet onGetCorrectPswd:(void (^)())GetCorrectPswd onGetIncorrectPswd:(void (^)())GetIncorrectPswd errorInput:(void (^)())errorInput;//用于设置
+(instancetype)status:(GesturePasswordStatus)status frame:(CGRect)frame verificationPassword:(void (^)())verificationPassword verificationError:(void (^)())verificationError onPasswordSet:(void (^)())onPasswordSet onGetCorrectPswd:(void (^)())GetCorrectPswd onGetIncorrectPswd:(void (^)())GetIncorrectPswd errorInput:(void (^)())errorInput;//用于重置
在GesturePasswordView.m中实现这三种方法
+(instancetype)status:(GesturePasswordStatus)status frame:(CGRect)frame onGetCorrectPswd:(void (^)())GetCorrectPswd onGetIncorrectPswd:(void (^)())GetIncorrectPswd errorInput:(void (^)())errorInput
{
return [self status:status frame:frame onPasswordSet:nil onGetCorrectPswd:GetCorrectPswd onGetIncorrectPswd:GetIncorrectPswd errorInput:errorInput];
}
+(instancetype)status:(GesturePasswordStatus)status frame:(CGRect)frame onPasswordSet:(void (^)())onPasswordSet onGetCorrectPswd:(void (^)())GetCorrectPswd onGetIncorrectPswd:(void (^)())GetIncorrectPswd errorInput:(void (^)())errorInput
{
return [self status:status frame:frame verificationPassword:nil verificationError:nil onPasswordSet:onPasswordSet onGetCorrectPswd:GetCorrectPswd onGetIncorrectPswd:GetIncorrectPswd errorInput:errorInput];
}
+(instancetype)status:(GesturePasswordStatus)status frame:(CGRect)frame verificationPassword:(void (^)())verificationPassword verificationError:(void (^)())verificationError onPasswordSet:(void (^)())onPasswordSet onGetCorrectPswd:(void (^)())GetCorrectPswd onGetIncorrectPswd:(void (^)())GetIncorrectPswd errorInput:(void (^)())errorInput
{
GesturePasswordView *gesView = [[GesturePasswordView alloc] initWithFrame:frame];
gesView.status = status;
gesView.verificationPassword = verificationPassword;
gesView.verificationError = verificationError;
gesView.onPasswordSet = onPasswordSet;
gesView.onGetCorrectPswd = GetCorrectPswd;
gesView.onGetIncorrectPswd = GetIncorrectPswd;
gesView.errorInput = errorInput;
return gesView;
}
在touchesEnded方法中,我们可以根据当前出入哪种状态做出相应的处理,所以touchesEnded方法应该是这样的:
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
if (self.selectorAry.count < 4) {
self.errorInput();
[self setPropertiesByState:GesturePasswordButtonStateNormal];
}else if (self.status == GesturePasswordStatusSet) {
[self setPasswordBlock];
}else if(self.status == GesturePasswordStatusReset){
NSString *password = [self getPassword];
NSString *inputPassword = [[NSString alloc] init];
if (self.resetInputNum == 0) {
for (GesturePasswordButton *btn in self.selectorAry) {
inputPassword = [inputPassword stringByAppendingFormat:@"%@",@(btn.tag)];
}
if ([inputPassword isEqualToString:password]) {
self.verificationPassword();
self.resetInputNum += 1;
[self performSelector:@selector(lockState:) withObject:[NSArray arrayWithObject:[NSNumber numberWithInteger:GesturePasswordButtonStateNormal]] afterDelay:0.3f];
}else{
self.verificationError();
[self setPropertiesByState:GesturePasswordButtonStateIncorrect];
}
}else if(self.resetInputNum == 1){
[self setPasswordBlock];
}
}else if(self.status == GesturePasswordStatusLogin){
NSString *password = [self getPassword];
NSString *inputPassword = [[NSString alloc] init];
for (GesturePasswordButton *btn in self.selectorAry) {
inputPassword = [inputPassword stringByAppendingFormat:@"%@",@(btn.tag)];
}
if ([inputPassword isEqualToString:password]) {
self.onGetCorrectPswd();
[self setPropertiesByState:GesturePasswordButtonStateNormal];
}else{
self.onGetIncorrectPswd();
[self setPropertiesByState:GesturePasswordButtonStateIncorrect];
}
}
GesturePasswordButton *btn = [self.selectorAry lastObject];
[self setCurrentPoint:btn.center];
[self setNeedsDisplay];
}
- (void)setPasswordBlock
{
if (self.inputNum == 0) {
self.firstPassword = [[NSString alloc] init];
for (GesturePasswordButton *btn in self.selectorAry) {
self.firstPassword = [self.firstPassword stringByAppendingFormat:@"%@",@(btn.tag)];
}
self.onPasswordSet();
self.inputNum += 1;
[self performSelector:@selector(lockState:) withObject:[NSArray arrayWithObject:[NSNumber numberWithInteger:GesturePasswordButtonStateNormal]] afterDelay:0.3f];
}else{
NSString *secondPassword = [[NSString alloc] init];
for (GesturePasswordButton *btn in self.selectorAry) {
secondPassword = [secondPassword stringByAppendingFormat:@"%@",@(btn.tag)];
}
if ([self.firstPassword isEqualToString:secondPassword]) {
[self savePassWord:secondPassword];
self.onGetCorrectPswd();
[self performSelector:@selector(lockState:) withObject:[NSArray arrayWithObject:[NSNumber numberWithInteger:GesturePasswordButtonStateNormal]] afterDelay:0.3f];
}else{
self.onGetIncorrectPswd();
[self setPropertiesByState:GesturePasswordButtonStateIncorrect];
self.inputNum -= 1;
}
}
}
存取密码的方法
- (void)savePassWord:(NSString *)password
{
NSUserDefaults *userdefault = [NSUserDefaults standardUserDefaults];
[userdefault setObject:@{@"password":password} forKey:GESTUREPASSWORD];
[userdefault synchronize];
}
- (NSString *)getPassword
{
NSDictionary *dic = [[NSUserDefaults standardUserDefaults] objectForKey:GESTUREPASSWORD];
return [dic objectForKey:@"password"];
}
在GesturePasswordViewController.h这样调用
if (self.status == GesturePasswordStatusSet) {
self.infoLabel.text = @"请设置手势密码";
GesturePasswordView *view = [GesturePasswordView status:GesturePasswordStatusSet frame:CGRectMake(0, 200, SCREEN_WIDTH, 400) onPasswordSet:^() {
self.infoLabel.text = @"请重新输入刚才设置的手势密码";
} onGetCorrectPswd:^ {
self.infoLabel.text = @"设置成功";
[weakSelf setGesturePasswordSwitchOpen];
} onGetIncorrectPswd:^ {
self.infoLabel.text = @"与上一次输入不一致,请重新设置";
} errorInput:^{
self.infoLabel.text = @"请至少连接4个点";
}];
[self.view addSubview:view];
}else if (self.status == GesturePasswordStatusReset){
self.infoLabel.text = @"请验证旧密码";
GesturePasswordView *view = [GesturePasswordView status:GesturePasswordStatusReset frame:CGRectMake(0, 200, SCREEN_WIDTH, 400) verificationPassword:^{
self.infoLabel.text = @"请输入新的手势密码";
} verificationError:^{
self.infoLabel.text = @"旧密码验证错误";
} onPasswordSet:^ {
self.infoLabel.text = @"请重新输入刚才设置的手势密码";
} onGetCorrectPswd:^ {
self.infoLabel.text = @"设置成功";
[weakSelf setGesturePasswordSwitchOpen];
} onGetIncorrectPswd:^ {
self.infoLabel.text = @"与上一次输入不一致,请重新设置";
} errorInput:^{
self.infoLabel.text = @"请至少连接4个点";
}];
[self.view addSubview:view];
}else if (self.status == GesturePasswordStatusLogin){
self.infoLabel.text = @"请输入手势密码";
GesturePasswordView *view = [GesturePasswordView status:GesturePasswordStatusLogin frame:CGRectMake(0, 200, SCREEN_WIDTH, 400) onGetCorrectPswd:^ {
self.infoLabel.text = @"解锁成功";
[self loginSuccess];
} onGetIncorrectPswd:^ {
self.infoLabel.text = @"密码错误,请重新输入";
} errorInput:^{
self.infoLabel.text = @"请至少连接4个点";
}];
[self.view addSubview:view];
}
结束语
整个思路大致就是这样的,主体实现的代码已经在文章中写出来了,当然还有手势密码锁的开启与关闭等一些细节代码没有写出,如果需要完整代码的可以前往github下载Demo。如果有什么不对的地方,希望大家可以及时的指出,谢谢!!!