实现仿支付宝九宫格手势密码

文章也同时在个人博客 http://kimihe.com/更新

1. 引言

前几天支付宝刚刚更新了双十一logo,各电商备战的氛围越来越浓厚。支付宝每天都用得相当频繁,为提高账户安全性,我开启了“手势密码”功能,首次进入敏感页面会要求验证九宫格手势来解锁。

手势密码

这个九宫格和安卓上面的绘图解锁非常类似,这引起了我自制一个的想法。而且最近一直在看微机汇编,有段时间没有搞iOS了,为避免生疏就拿它练手吧。

2. Demo地址

KMNineBoxDemo
Demo中仍有不少可以进一步改进的地方,欢迎各位读者提出宝贵意见。

3. 效果预览

这里的图案是绘制数字2的形状,对应的手势序列就是123654789。

效果预览

可以看到九宫格手势较好地检测了触摸点的位置,跟踪了手势,并在验证后反馈不同的显示效果。绘制的手势图案除了简单的一步到底的图案,你也可以绘制一些连线交叉的复杂图案。

4. 几点说明

  • 虽说是仿支付宝的手势密码,但由于水平有限,难以100%仿制。大家可以看到图案的颜色和大小有些许不同,这里也是尽量模仿。
  • 支付宝的手势密码在绘制图案时,会有箭头指向以及实时更新的连线,连线会跟随用户的手指不断移动,这里的箭头我没有增加进去,但应该是较容易实现的,主要是没有箭头的切图。而实时跟踪的连线,我暂时还没有想到特别好的融合方法,虽说也可以加入,但总觉得以目前水平写出来的实时连线,效率不高,故没有加入。
  • 至于上方的两个label,都可以通过KMNineBoxView的接口中拿到验证和手势序列信息,设置起来非常简单。

5. 如何使用该控件

参考ViewController.m中的示例,结合模拟器运行,大家可以快读地理解该控件的使用场景。

KMNineBoxView.h中说明了几个接口的使用说明。这里需要注意的一点是,为了能够较好地根据验证结果返回显示,在使用前需要设置好正确的密码序列,然后在用户绘制完手势图案后,会进行比较,根据结果显示不同的颜色。

- (void)nineBoxDidFinishWithState:(KMNineBoxState)state passSequence:(NSString *)passSequence;这个接口中也会返回验证结果和用户绘制的序列,KMNineBoxView本身已经会自动根据验证结果设置不同的显示,这里还提供这个接口主要是给用户更大的自由度,方便用户进行更多的个性化设置。例如用户可以根据拿到的用户序列,提示用户一些信息,正如Demo里面的使用情景。

6. Demo结构

Demo主要由KMUIKitMacro.h头文件,KMMathHelper类,KMNineBoxView类,以及ViewController类构成。

  • KMUIKitMacro.h头文件是一些预定义的宏,用于简化代码。
  • KMMathHelper类提供了一些数学计算工具。
  • KMNineBoxView类就是我们实现的九宫格手势密码,后文会着重讲解它。
  • ViewController类结合storyboard使用我们的自定义控件。

7. 原理讲解

7.1 难点

首先罗列一些实现过程中我认为的难点,其实与其说是难点,不如说是需要注意或者是可以优化的地方。

  • Layer的布局,显示正确的图案。
  • 用户手势的状态,以此对应设置KMNineBoxView状态。
  • 用户触摸点的位置判断,即落在9格圆圈的哪一个里面。
  • 跟踪用户触摸过的轨迹,即记录下9个圆圈哪些被触摸过,以及它们的顺序。
  • 圆圈间的连接线。
  • 给予手势反馈结果,即验证通过还是手势错误。

下面的讲解着重说明思路和注意点,代码会贴上一些关键部分,完整的代码请见工程。

7.2 Layer布局

常见地重写- (instancetype)initWithFrame:(CGRect)frame方法,提供初始化方法,并在其中计算一些布局尺寸和画出9个基本的圆圈,如下:

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self initData];
        [self setupBoundsAndFrame];
        [self drawNineBox];
    }
    return self;
}

考虑到用户可能会跳针view的frame,因此需要启用- (void)layoutSubviews方法,在其中重新计算布局,并调整圆圈的frame,如下:

- (void)layoutSubviews
{
    //改变frame,subviews,sublayers会跑进来
    [self setupBoundsAndFrame];
    [self reloadNineBox];
}

这里需要注意layoutSubviews这个方法的使用,如果不明白其中的原理,可以戳这篇文章学习一下:layoutSubviews小结,否则可能会遇到一些奇怪的问题。

常见的一个问题就是在其中重新布局时,需要重绘view或者layer,重绘这一步本身没什么问题,但是一般我们后续都会不自觉地加上addSubviewaddSublayer,这里add之后又会进入layoutSubviews,结果就是循环,虽然不至于死锁,但是你会失去对图层的控制,一些图层将无法按照你的预期进行修改或者销毁,请务必注意!

所以我在这里调用的reloadNineBox方法中,没有重绘操纵,只有修改布局。而对于修改布局,以往常用的layer的position属性在这里有点迷,似乎是一个相对位置,控制起来不直观。考虑到layer的圆圈和中心点是基于UIBezierPath来绘制的,我索性就更新了这个path。

上述的layer圆圈和中心点在drawNineBox中完成绘制,其中绘制所需的布局信息在setupBoundsAndFrame中进行计算。并且信息存储在两个成员数组中,这两个数组非常重要:

//保存9个circleLayer的数组会随frame变化
NSMutableArray *_nineCirclesArr;  

//保存9个中心点的位置,会随frame变化
NSArray *_boxCentersArr;                

可以看到在initWithFrame:layoutSubviews中都调用了setupBoundsAndFrame方法,这就是所谓实现了整个view能够根据用户对于frame的调整,自动配置内部各layer的位置,使整个view能够正确布局。

7.3 用户手势的状态

整个view需要区分不同的几个状态,从而实现各阶段的功能,我在这里通过枚举把状态分为四个:

  • KMNineBoxStateNormal:普通状态,即初始的用户未进行任何触摸操作前的状态,同时在完成验证反馈结果后,需要返回这个状态。
  • KMNineBoxStateTouched:触摸状态,表明用户正在绘制手势图案,被扫过的圆圈会高亮加粗,并且进行连线。
  • KMNineBoxStatePassed:验证通过状态,在完成绘制后会立刻进行手势密码的验证,这里表明手势正确,通过验证。
  • KMNineBoxStateFailed:验证失败状态,表明手势密码错误,同时被扫过的圆圈和连线变成红色,提升用户手势错误。

- (void)setNineBoxState:(KMNineBoxState)nineBoxState方法中实现对不同状态的操作。

7.4 用户触摸点的位置判断

绘制九宫格手势时,需要适时标记被扫过的圆圈,使其高亮加粗,这就需要对用户的触摸点进行位置判断。

思路就是现获取用户手指的位置,然后再计算落在9个圆圈哪一个的“管辖范围”内。由于9个圆圈不相交,所以触摸点同一时刻只可能落在一个圆圈的范围内。

首先,通过UIResponder的三个touch event获取手指的三个不同状态:

  • 刚刚触碰到屏幕
  • 在屏幕上移动
  • 离开了屏幕

三个接口如下:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event
{
    CGPoint point = [[touches anyObject] locationInView:self];
    
    KMNineBoxIndex index = [self checkLocationWithTouchPoint:point];
    [self decorateCircleWithBoxIndex:index];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    CGPoint point = [[touches anyObject] locationInView:self];
    
    KMNineBoxIndex index = [self checkLocationWithTouchPoint:point];
    [self decorateCircleWithBoxIndex:index];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // sth reset
    ...
    ...
}

可以看到在Began和Moved状态中进行了触摸点位置的判断。

- (KMNineBoxIndex)checkLocationWithTouchPoint:(CGPoint)touchPoint方法中进行来上述操作。它会返回一个KMNineBoxIndex枚举类型,表明现在手中落在哪一个圆圈范围内,例如落在圆圈3中时,就会返回KMNineBoxIndex3。

而具体的判断落点的算法,我在实现时思路比较简单,就是计算触摸点与九个圆圈中心点距离,找出最短距离,在最短距离不大于圆圈半径的前提下,就认为触摸点落在这个圆的范围内。这里面的计算最短距离,甚至整个落点的判断都是可以进一步优化的,欢迎大家提出建议。

之后便可以根据这个index信息去修饰对应的圆圈,使其高亮加粗。通过- (void)decorateCircleWithBoxIndex:(KMNineBoxIndex)index方法来进行。

7.5 跟踪用户触摸过的轨迹

完成扫过的圆圈高亮加粗后,我们还需要记录下这些被扫过的圆圈,以及它们的序列。目的就是为了能够在移动手指时,提供圆圈间的连线,并且在手势验证失败时,把这些圆圈加以红色。此外还要能够方便我们提取出手势的序列,在接口中提供用户使用。

这里是用了一个成员数组来记录上述信息:

// 保存九宫格序列的数组,会随触摸手势变化
NSMutableArray *_sequenceArr;           

记录过程如下:

NSString *checkStr = [NSString stringWithFormat:@"%ld", circleIndex+1];//加1
if (![self checkString:checkStr isInArray:_sequenceArr]) {
    // 不允许重复添加
    [_sequenceArr addObject:checkStr];
}

其中结合- (BOOL)checkString:isInArray:方法实现去重添加,因为手势是实时检测的,那么对于落点的判断结果就会实时反馈,对于某一个圆圈,我们只希望记录下一次,于是去重是必须的。

_sequenceArr记录下了手势移动的轨迹序列,这也是一部非常关键的操作。

7.6 圆圈间的连接线

有了上述步骤记录下的轨迹序列,我们就能方便地画出连线了。思路比较简单,取出_sequenceArr中的最后两个元素,作为最近扫过的两个点,在两点之间进行连线。

不过需要注意,我们不希望重复画线,即两点间已经有连线了,在下一次绘制时最新的两点没有变化时,就不需要再画线了,判断代码如下:

// 新的两点连线,才继续画
if ([KMMathHelper point1:currentBoxCenter EqualToPoint2:_currentBoxCenter] &&
    [KMMathHelper point1:previousBoxCenter EqualToPoint2:_previousBoxCenter]) {
    return;
}

此外,只有一个序列点时,也不画线,因为二维平面内两个点才确定一条直线嘛!

7.7 给予手势反馈结果

经过上述一系列步骤,我们的手势密码基本成型,接下来就只需要反馈验证的结果,以及提供接口给用户。

在手势密码本身的反馈显示中,如果密码验证成功,将会返回普通状态。而如果验证失败,则被扫过的圆圈会以红色提醒用户,代码如下:

if ([self.predefinedPassSeq isEqualToString:sequenceStr]) {
    [self setNineBoxState:KMNineBoxStatePassed];
}
else {
    [self setNineBoxState:KMNineBoxStateFailed];
}

而对于协议接口给予用户的信息,首先需要提取出手势序列,将其转化成数字1-9组成的字符串:

NSString *sequenceStr = @"";
for (int i = 0; i < [_sequenceArr count]; i++) {
    NSString *tmp = [NSString stringWithFormat:@"%@", _sequenceArr[i]];
    sequenceStr = [NSString stringWithFormat:@"%@%@", sequenceStr, tmp];
}

然后结合状态信息,一并通过接口提供给用户:

[self.delegate nineBoxDidFinishWithState:_nineBoxState passSequence:sequenceStr];

最后就是一些提升用户体验的步骤,比如自动重置KMNineBoxView的状态,控制用户触摸响应等等,这里就不再赘述了。

8. 总结

本文介绍了如何仿制一个支付宝的手势密码,涉及到了较多页面布局和手势检测的知识。不过让我感受最深的还是其中有一些计算技巧,需要平时的基础积累。这又再次提醒我:该回去补数据结构和算法啦!还不快ACM!

总之,希望这篇文章对大家有所帮助。更多iOS的知识,请继续关注后续的文章,感谢阅读!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容