iOS梅开二度 - 手势键盘的处理 (嗷嗷待哺版本)

写在文章的最前面:

时隔三年半,我又重新拾起了博客,这三年多可能是我最飘忽不定的几年,也有诸多的感慨和体悟,本想专写一篇文章来对这几年的经历做一个总结,但每当要起头的的时候,都会不知所措,也可能是感触太多容易上头,也有可能是担心自己太过于多愁善感,索性就写在三年后的第一篇博客里面作为开头语吧。

人生真的就是起起伏伏,从14年底加入到了一个新的家庭,对,我称之为“家庭”。可能是我运气不太坏进入到了这个部门,滑稽的是我其实是被截胡的,本来我要去另外一个部门,而且也是那个部门对我进行的面试,但当我入职的时候被现在的这个部门截胡了。其实我也很庆幸,因为在这,我遇到了一个好的领导,也是因为此我才有了后面的兜兜转转。省略过程中的几万字,最终的结果却又完完全全的回到了iOS这个职业上,为什么说“完完全全”呢,因为之前一直处于兼iOS Leader的同时又奔波于其他的工作(大家理解了意思就好,再聊真的就要专门写一篇了)。

重新回到这个职业上发现,技术落后了许多也生疏了,基础的知识也有些记忆模糊了。所以也是下定决心重新拾起它(不然也没办法,容易丢饭碗),同时也想把博客重新写起来,不光是对自己这个过程的一个见证,也算是提供给大家一些可参考的资料。(我自认为,不过写的不好,还请各位客官见谅)

对之前在文章评论中提出一些疑问的人 说声抱歉,这几年博客被遗忘在角落,也就没有去回复大家的问题,道友们,见谅。

 

好了,话不多说,下面就直接上干货。

先来一波gif图片让大家对于此篇文章的内容有一个了解:

 

ok,看完图片开始此篇文章的正文。

一、手势键盘的逻辑梳理

1、关于绘制:涉及到图形,首先想到了CoreGraphics,不过此时有一个前提条件就是有了三种状态的图片(一张选中、一张错误、一张普通),所以舍弃了对手势按钮进行绘制,而是使用UIButton来作为手势按钮的承载者,设置三种按钮状态的图片,根据需要来改变相应按钮的state。

2、关于两个被触摸按钮之间的连线:计算每个按钮的中心点,然后两个中心点作为StartPoint和EndPoint进行线段的绘制,由于在第一点中舍弃了CoreGraphics框架,索性就舍弃到底,所以选择使用CoreAnimation框架(UIBezierPath+CAShapeLayer)来绘制线段,实现连接的效果。此种方式基于GPU进行绘制处理,同时也减缓了CPU的压力。(我们都知道CPU作为中央处理器支撑了整个应用,但并不是说把所有的图形、动画等处理统统交予CPU去解决,这样也会使得CPU的使用率偏高压力过大,也会造成应用的卡顿等现象,所以我们要合理的利用CPU和GPU)

3、关于触摸事件处理:说到事件就可以想到UIResponder响应者对事件的处理,实现UITouch的touchesBegan、touchesMoved、touchesEnded方法来进行触摸坐标的接收处理工作。(说到这,简要描述一下触摸事件的传递过程: 手机屏幕感受到触摸 -- 交于IOKit进行处理 -- IOKit反手一个封装就踢给了系统进程(SpringBoard) -- 而SpringBoard只能触发主线程RunLoop来求助处理 -- 主线程经过自己的倒腾辨别出当前正有APP在前台运行,就直接把转化完的事件抛出了 -- APP接收事件添加到UIApplication队列中寻找最佳响应者)

4、关于连线与触摸按钮的交叉处理:在键盘范围内拖动绘制线段去连接下一个按钮,在这个过程中有多种情况出现:1、手指直接触碰到下一个按钮;2、手指经过中间按钮,但是连线却接触到此按钮;这些情况都需要将这个按钮标记为选中状态,也就是说绘制的线段所经过的路线和按钮有接触的时候,就当作是选择了此按钮。

敲黑板:(第四点的关键点补充)

使用数学原理中的垂线的概念来处理交叉计算问题,我们都知道“点到直线的距离,垂线段最短”,所以我们以此来计算手势按钮的“圆心”到手指所绘制的“直线”之间的垂线段长度,当垂线段的距离小于当前手势按钮的半径的时候,就证明直线一定经过了这个手势按钮,那么我们就可以标记为此按钮为选中状态。

 

二、代码层级逻辑

1、ViewController的职责:

  • VerifyView(验证页面)和SetView(设置页面)两个展示类的初始化
  • ViewModel(逻辑+数据处理)类初始化
  • 手势页面类型的赋值和处理,加载相应的页面

2、VerifyView的职责:

  • 初始化并添加GestureView(手势页面)
  • 处理手势验证结果:验证成功和失败的页面效果逻辑处理

3、SetView的职责:

  • 初始化并添加GestureView(手势页面)
  • 处理手势设置结果:设置成功和失败的页面效果逻辑处理(第一次设置新密码,第二次验证第一次设置的密码)

4、GestureView的职责:

  • 一个公共的手势页面类
  • 创建包含九个手势按钮的页面效果且对外提供页面初始化的调用方法
  • 改变手势按钮的图片状态为高亮(即触摸选中状态)
  • 改变手势按钮的图片状态为普通(即未被选中状态)
  • 改变手势按钮的图片状态为错误(即手势错误状态)
  • 绘制触摸绘制的连线(正常绘制状态)
  • 改变已绘制的连线(手势错误状态)
  • 触摸事件的接收和处理

5、ViewModel的职责:

  • 定义所需要的枚举
  • 定义所需要的宏
  • 对设置手势密码过程中的密码数据进行处理和保存,并返回处理结果
  • 对验证手势密码过程中的密码数据进行校验,并返回处理结果
  • 统一处理页面跳转、数据清除等事件

 

三、关键代码具体实现

1、配置宏变量和枚举变量

ViewModel:

//resultsBlock 集合成为字典,收集所有的返回数据,定义宏变量进行说明
#define gesture_stateType @"gesture_stateType"//GesturePasswordStateType
#define results_bool @"results_bool"//处理结果 YES|NO
#define valid_nums @"valid_nums"//验证密码的时候 有效的录入验证次数
#define highlightedIndex_array @"highlightedIndex_array"//被触摸选中的按钮的下标值数组
#define conform_minLimit @"conform_minLimit"//设置密码的时候,密码长度需要大于4

#import <Foundation/Foundation.h>

/*! 手势密码模块的样式类型 */
typedef NS_ENUM(NSInteger, GesturePasswordType) {
    /*! 手势密码验证模式 */
    GesturePasswordType_Verify,
    /*! 手势密码设置模式 */
    GesturePasswordType_Set,
    /*! 手势密码修改模式 */
    GesturePasswordType_Modify
};

/*! 手势密码所处状态的样式类型 */
typedef NS_ENUM(NSInteger, GesturePasswordStateType) {
    /*! 手势密码-第一次密码设置 */
    GesturePasswordStateType_First,
    /*! 手势密码-第二次密码设置 */
    GesturePasswordStateType_Second,
    /*! 手势密码-验证 */
    GesturePasswordStateType_Verify,
};

2、对手势密码进行验证处理

ViewModel:

/*!
 * 对密码多次录入的逻辑处理
 *
 * @param gesture_type GesturePasswordType_Verify|GesturePasswordType_Set 区分密码模式,verify:第二次进行密码验证 set:第一次进行密码设置,第二次进行密码验证
 * @param button_index 所接触到的按钮的下标值,用作密码进行字符串拼接,最终拼接为下标值组成的密码字符串
 * @param isEnd 是否出发了TouchesEnded函数,结束了绘制
 * @param resultsBlock 每次执行密码逻辑处理的结果返回,view根据此状态进行UI处理
 */
- (void)dealWithGesturePasswordWithType:(GesturePasswordType)gesture_type withButtonIndex:(NSInteger)button_index withInputState:(BOOL)isEnd withResultsBlock:(void (^)(NSDictionary * _Nonnull))resultsBlock
{
    switch (gesture_type) {
        case GesturePasswordType_Set://密码设置
        {
            if (!isVerifyInput) {//first
                [self saveGesturePasswordWithButtonIndex:button_index];
            }else {
                [self saveVerifyGesturePasswordWithButtonIndex:button_index];
            }
            /*!
             * 录入结束,当结束之后第一次录入密码进行保存,并返回第一次密码处理完成
             * 第二次录入密码模式:和第一次密码进行验证,并返回验证结果:成功|失败
             */
            if (isEnd) {
                if (!isVerifyInput) {
                    NSMutableArray *gestureArray = [NSMutableArray combinationArrayWithString:self.gesturePasswordString];
                    
                    //the resultsDict of need to return
                    NSMutableDictionary *resultsDict = [[NSMutableDictionary alloc] initWithCapacity:4];
                    [resultsDict setObject:[NSNumber numberWithInteger:GesturePasswordStateType_First] forKey:gesture_stateType];
                    [resultsDict setObject:[NSNumber numberWithBool:YES] forKey:results_bool];
                    [resultsDict setObject:gestureArray forKey:highlightedIndex_array];
                    [resultsDict setObject:[NSNumber numberWithBool:YES] forKey:conform_minLimit];
                    
                    //判断密码长度是否符合限制
                    if ([gestureArray count] >= minLimitOfGesturePassword) {
                        //1. 设置值为YES,进入验证录入模式
                        isVerifyInput = YES;
                        //2. 保存密码到NSUsersDefaults中
                        
                    }else {//不符合
                        [resultsDict setObject:[NSNumber numberWithBool:NO] forKey:conform_minLimit];
                        
                        //设置密码如果不符合长度限制,则清空
                        self.gesturePasswordString = @"";
                    }
                    
                    //3. 返回结果:第一次密码设置完成,可进入第二步验证工作
                    resultsBlock(resultsDict);
                }else {
                    //1. 验证
                    BOOL verify_bool = [self verifyGesturePassword];
                    //2. 验证失败,清空原始密码
                    if (!verify_bool) {
                        self.verifyGesturePasswordString = @"";
                    }else {//验证成功
                        //3. 恢复默认值
                        isVerifyInput = NO;
                        
                    }
                    //4. 返回结果:验证已完成
                    NSMutableDictionary *resultsDict = [[NSMutableDictionary alloc] initWithCapacity:4];
                    [resultsDict setObject:[NSNumber numberWithInteger:GesturePasswordStateType_Second] forKey:gesture_stateType];
                    [resultsDict setObject:[NSNumber numberWithBool:verify_bool] forKey:results_bool];
                    [resultsDict setObject:[NSNumber numberWithInt:0] forKey:valid_nums];
                    resultsBlock(resultsDict);
                }
            }
        }
            break;
        case GesturePasswordType_Verify://密码验证
        {
            [self saveVerifyGesturePasswordWithButtonIndex:button_index];
            if (isEnd) {
                //在屏幕上触摸了很久,但是一个按钮都没有触发,所以字符串应该是空的,排除此情况
                if (self.verifyGesturePasswordString.length == 0) {
                    NSMutableDictionary *resultsDict = [[NSMutableDictionary alloc] initWithCapacity:4];
                    [resultsDict setObject:[NSNumber numberWithInteger:GesturePasswordStateType_Verify] forKey:gesture_stateType];
                    [resultsDict setObject:[NSNumber numberWithBool:NO] forKey:results_bool];
                    [resultsDict setObject:[NSNumber numberWithInt:5] forKey:valid_nums];
                    //设置特定的条件来判断是否是此条件NO+5
                    resultsBlock(resultsDict);
                }else {
                    //1. 验证
                    BOOL verify_bool = [self verifyGesturePassword];
                    //2. 清空验证的密码数据
                    self.verifyGesturePasswordString = @"";
                    
                    //3. 返回结果:验证已完成
                    NSMutableDictionary *resultsDict = [[NSMutableDictionary alloc] initWithCapacity:4];
                    [resultsDict setObject:[NSNumber numberWithInteger:GesturePasswordStateType_Verify] forKey:gesture_stateType];
                    [resultsDict setObject:[NSNumber numberWithBool:verify_bool] forKey:results_bool];
                    [resultsDict setObject:[NSNumber numberWithInteger:(--numsOfValidLogin)] forKey:valid_nums];
                    resultsBlock(resultsDict);
                    //4. 判断剩余验证次数,次数为0时,则直接清空账户信息,进入APP首页(未登录状态)
                    if (numsOfValidLogin > 0) {
                    }else {
                        [self ClearUsersDataAndEnterAPP];
                    }
                }
            }
        }
            break;
            
        default:
            break;
    }
}

3、创建手势键盘中的单个按钮,设置三种状态的图片

LKGesturePasswordSingleButton:

- (void)setStateType:(GesturePasswordButtonStateType)stateType
{
    _stateType = stateType;
    switch (stateType) {
        case GesturePasswordButtonStateType_Normal:
        {
            self.selected = NO;
            self.highlighted = NO;
        }
            break;
        case GesturePasswordButtonStateType_Highlighted:
        {
            self.selected = NO;
            self.highlighted = YES;
        }
            break;
        case GesturePasswordButtonStateType_Error:
        {
            self.selected = YES;
            self.highlighted = NO;
        }
            break;
            
        default:
            break;
    }
}

/*!
 * 普通状态的按钮图标图片名称设置 Getter
 */
- (NSString *)normalImageName
{
    if (!_normalImageName) {
        _normalImageName = @"gesture_password_normal_icon.png";
    }
    return _normalImageName;
}

/*!
 * 被Touch状态的按钮图标图片名称设置 Getter(使用highlighted属性来标记)
 */
- (NSString *)highlightedImageName
{
    if (!_highlightedImageName) {
        _highlightedImageName = @"gesture_password_highlighted_icon.png";
    }
    return _highlightedImageName;
}

/*!
 * 密码设置错误状态的按钮图标图片名称设置 Getter (使用Selected 属性来标记)
 */
- (NSString *)errorImageName
{
    if (!_errorImageName) {
        _errorImageName = @"gesture_password_error_icon.png";
    }
    return _errorImageName;
}

 

4、手势键盘触摸事件的处理

LKGesturePasswordView:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    theLastButtonIndex = 9;//每次重新触摸屏幕的时候,重置此值
    [self.pointsArray removeAllObjects];//每次重新触摸屏幕的时候,清空数组
    UITouch *touch = [touches anyObject];
    //touch的坐标
    CGPoint touchPoint = [touch locationInView:self];
    [self setHighlightedStateWithPoint:touchPoint withIsTouchesEnded:NO];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    //touch的坐标
    CGPoint touchPoint = [touch locationInView:self];
    
    //防止手势移动到手势键盘范围之外导致连线超出,对x和y进行处理,限制在范围之内
    CGFloat validPoint_x = touchPoint.x;
    if (validPoint_x < 0) {
        validPoint_x = 0;
    }else if (validPoint_x > self.frame.size.width) {
        validPoint_x = self.frame.size.width;
    }
    
    CGFloat validPoint_y = touchPoint.y;
    if (validPoint_y < 0) {
        validPoint_y = 0;
    }else if (validPoint_y > self.frame.size.height) {
        validPoint_y = self.frame.size.height;
    }
    [self setHighlightedStateWithPoint:CGPointMake(validPoint_x, validPoint_y) withIsTouchesEnded:NO];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //结束绘制,且不可继续绘制,然后进行数据验证
    self.userInteractionEnabled = NO;
    
    UITouch *touch = [touches anyObject];
    //touch的坐标
    CGPoint touchPoint = [touch locationInView:self];
    [self setHighlightedStateWithPoint:touchPoint withIsTouchesEnded:YES];
}

5、通过坐标x,y的值计算出手势接触到的按钮

LKGesturePasswordView:

/*!
 * 查找最大可能相接触到的按钮,只是为了实现更精准查找,替代了(subviews 子视图遍历一一进行重叠判断),此方法最终在寻找出按钮的index之后,需要进行二次验证
 *
 * @param touchPoint Touch事件所获得的坐标值
 * @return 按钮的下标值
 */
- (NSInteger)getButtonTagWithPoint:(CGPoint)touchPoint
{
    //1. 获取x和y的实际值
    CGFloat touchPoint_x = touchPoint.x;
    CGFloat touchPoint_y = touchPoint.y;
    
    //2. 按钮之间的间隔为41,按钮的高宽为53
    CGFloat interval_wh = 41*Standard;
    CGFloat button_wh = 53*Standard;
    
    //3. 首先计算x有几个interval_wh+button_wh的间距,并且去掉相同个数的间距值
    NSInteger interval_wh_x = touchPoint_x/(interval_wh+button_wh);
    CGFloat index_x_tmp = touchPoint_x-interval_wh_x*(interval_wh+button_wh);
    
    //4. 其次计算y有几个interval_wh+button_wh的间距,并且去掉相同个数的间距值
    NSInteger interval_wh_y = touchPoint_y/(interval_wh+button_wh);
    CGFloat index_y_tmp = touchPoint_y-interval_wh_y*(interval_wh+button_wh);
    
    //3.4.可以把x和y的值缩放到第一个按钮的有效范围内,从而和第一个按钮的区间值进行比较
    
    NSInteger button_index = 9;//无效值
    if (index_x_tmp <= button_wh && index_y_tmp <= button_wh) {
        //5. x 和 y均在与button的重合范围内,计算出button_index的值
        button_index = interval_wh_x + interval_wh_y*3;
    }
    return button_index;
}

6、利用垂线原理计算绘制过程中直线将会经过的手势按钮,与第五点相结合进行处理

LKGesturePasswordView:

/*!
 * 根据手指绘制的起点(按钮的中心点)和结束点(手指接触屏幕的点)来计算出与所绘制直线相交的按钮(使用垂线的计算方式)
 *
 * @param startPoint 起点
 * @param endPoint 结束点
 */
- (void)getPossibleButtonWithStartPoint:(CGPoint)startPoint withEndPoint:(CGPoint)endPoint
{
    //遍历中心点坐标数组,找到与直线会相交的按钮
    for (int i = 0; i < [self.centerPointArray count]; i ++) {
        NSValue *pointValue = self.centerPointArray[i];
        CGPoint centerPoint = [pointValue CGPointValue];
        //我也不知道到底应该叫什么名字合适,暂且ABCDEF...吧
        CGFloat A = endPoint.y - startPoint.y;
        CGFloat B = startPoint.x - endPoint.x;
        CGFloat C = endPoint.x*startPoint.y - startPoint.x*endPoint.y;
        
        //中心点到直线的距离,俗称垂线,fabs 取正数
        CGFloat centerPointToLine = fabs((A*centerPoint.x+B*centerPoint.y+C)/sqrt(A*A+B*B));
        //计算垂足坐标值,用于来判断垂足在线段之内,否则会出现混乱
        CGFloat footPoint_x = (B*B*centerPoint.x-A*B*centerPoint.y-A*C)/(A*A+B*B);
        CGFloat footPoint_y = (-A*B*centerPoint.x+A*A*centerPoint.y-B*C)/(A*A+B*B);
        //超级恶心的判断,我自己想到的比较繁琐的比较,用于四种线段(以原点为基准,向↘|向↗|向↖|向↙)
        BOOL isValid_f = footPoint_x>startPoint.x && footPoint_x<endPoint.x && footPoint_y>startPoint.y && footPoint_y<endPoint.y;//向↘
        BOOL isValid_s = footPoint_x>startPoint.x && footPoint_x<endPoint.x && footPoint_y>endPoint.y && footPoint_y<startPoint.y;//向↗
        BOOL isValid_t = footPoint_x>endPoint.x && footPoint_x<startPoint.x && footPoint_y>endPoint.y && footPoint_y<startPoint.y;//向↖
        BOOL isValid_n = footPoint_x>endPoint.x && footPoint_x<startPoint.x && footPoint_y>startPoint.y && footPoint_y<endPoint.y;//向↙
        //取其一即可
        BOOL isValid_footPoint = isValid_f || isValid_s || isValid_t || isValid_n;
        
        //判断垂直距离是否小于圆的半径,如果小于,则证明相交了
        CGFloat radius = (53*Standard)/2;
        if (centerPointToLine > 0 && centerPointToLine <= radius && isValid_footPoint) {
            //1. 获取相交的按钮
            LKGesturePasswordSingleButton *gestureButton = (LKGesturePasswordSingleButton *)[self viewWithTag:GESTURE_SINGLE_BUTTON_TAG_START+i];
            //2. 判断state是否为highlighted
            if (gestureButton) {
                if (gestureButton.stateType != GesturePasswordButtonStateType_Highlighted) {
                    //3. 如果不是此状态,则标记为此状态
                    [self setHighlightedStateWithPoint:centerPoint withIsTouchesEnded:NO];
                    //只要出现一个即可break,尽可能的避免过多的循环(哭脸)
                    break;
                }
            }
            break;
        }
    }
}

7、绘制手势键盘上的连接线

LKGesturePasswordView:

/*!
 * 根据起点和结束点来初始化获取ShapeLayer对象,调用此方法来获取并添加至view的layer层
 *
 * @param startPoint 起点
 * @param endPoint 结束点
 */
- (CAShapeLayer *)getShapeLayerWithStartPoint:(CGPoint)startPoint withEndPoint:(CGPoint)endPoint
{
    UIBezierPath *bezierPath = [UIBezierPath bezierPath];
    [bezierPath moveToPoint:startPoint];
    [bezierPath addLineToPoint:endPoint];
    
    CAShapeLayer *shapeLayer = [[CAShapeLayer alloc] init];
    UIColor *color_stroke = [UIColor colorWithHex:0x5FAFFC];
    shapeLayer.strokeColor = color_stroke.CGColor;
    shapeLayer.lineWidth = 1;
    shapeLayer.path = bezierPath.CGPath;
    shapeLayer.fillColor = nil;
    return shapeLayer;
}

以上为手势键盘的相对核心代码,代码写的不是很好,各位客官可只浏览文章的第一、二部分理解本文的思想即可。如有不正确的地方,欢迎各位大神对小弟进行指点。

 

四、传送门(按照博客惯例,附上GitHub的下载地址)

点我,点我,点我,我是传送门。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值