惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
云风的 BLOG
云风的 BLOG
美团技术团队
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
爱范儿
爱范儿
Stack Overflow Blog
Stack Overflow Blog
WordPress大学
WordPress大学
GbyAI
GbyAI
雷峰网
雷峰网
P
Proofpoint News Feed
IT之家
IT之家
人人都是产品经理
人人都是产品经理
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
aimingoo的专栏
aimingoo的专栏
小众软件
小众软件
T
The Blog of Author Tim Ferriss
月光博客
月光博客
V
Visual Studio Blog
L
LINUX DO - 热门话题
L
Lohrmann on Cybersecurity
T
Troy Hunt's Blog
Project Zero
Project Zero
U
Unit 42
T
Tor Project blog
Scott Helme
Scott Helme
L
LINUX DO - 最新话题
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
I
InfoQ
Cloudbric
Cloudbric
P
Proofpoint News Feed
The Cloudflare Blog
H
Heimdal Security Blog
Google DeepMind News
Google DeepMind News
The GitHub Blog
The GitHub Blog
Attack and Defense Labs
Attack and Defense Labs
有赞技术团队
有赞技术团队
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
J
Java Code Geeks
博客园 - 【当耐特】
Security Latest
Security Latest
The Register - Security
The Register - Security
F
Fortinet All Blogs
I
Intezer
H
Hackread – Cybersecurity News, Data Breaches, AI and More
MongoDB | Blog
MongoDB | Blog
N
Netflix TechBlog - Medium
NISL@THU
NISL@THU
T
Tenable Blog

土土哥的Blog

好久没有更新博客=。= 阿里巴巴国际无线技术部 - 招人啦~求iOS、Android、Java 转-阿里巴巴国际无线技术部 - 在这里遇见最好的自己 反编译分析并模拟实现methodSignatureForSelector方法 反编译分析Xcode8的Bug, release下连续两次调用有二级指针参数的空方法会Crash 有趣的Autolayout示例5-Masonry实现 在对象dealloc的后期执行Task-开源库TTGDeallocTaskHelper 用QuartzCode快速实现一个收藏动画 Swift开源Mac App - BingWallPaper 有趣的Autolayout示例4-Masonry实现 翻译-为什么objc_msgSend必须用汇编实现 API返回结果设计经验与总结 结合访问Out Parameters出现EXC_BAD_ACCESS的例子,反编译汇编解读__autoreleasing 总结一些iOS项目中组织代码的方法 对组件化与模块化的思考与总结 开源项目-TTGTagCollectionView 有趣的Autolayout示例3-Masonry实现 Swift开源项目: TTGEmojiRate的实现 Swift写的库-TTGSnackbar 有趣的Autolayout示例2-Masonry实现 解决iOS项目的版本兼容问题-结合宏、Category和Runtime 用Runtime的手段填充任意NSObject对象的nil属性 有趣的Autolayout示例-Masonry实现 UITextView编辑时插入自定义表情-续-自定义表情图片的大小 RPC框架Thrift例子-PHP调用C++后端程序 GCD使用经验与技巧浅谈 为GCD队列绑定NSObject类型上下文数据-利用__bridge_retained(transfer)转移内存管理权 Enum-枚举的正确使用-Effective-Objective-C-读书笔记-Item-5 @autoreleasepool-内存的分配与释放 有关宏定义的经验与技巧-简化代码-增强Log Effective-Objective-C-读书笔记-Item-4-如何正确定义常量 UITextView编辑时插入自定义表情-简单的图文混编 关于评论不见了=。= Entity和Model的不同-关于代码的数据层 一次审核被拒的经历-关于iCloud到底应该备份什么数据 Block类型变量-缓存Http请求与回调 提升UITableView性能-复杂页面的优化 NSString的Copy与内存分配 利用NSProxy实现消息转发-模块化的网络接口层设计-原创 Effective-Objective-C-读书笔记-Item-3 Effective-Objective-C-读书笔记-Item-2 Effective-Objective-C-读书笔记-Item-1 iOS项目的目录结构-原创 Android开源库-LinkTextView-原创 第一篇Blog
开源项目-拼图验证控件TTGPuzzleVerify的实现
土土哥 · 2016-12-25 · via 土土哥的Blog

前言

最近抽空写了个拼图验证控件,用户可以通过水平、垂直,或者直接拖动拼图块,完成拼图图案,来完成验证。拼图块的形状可以自定义,默认提供了经典的拼图形状、圆形、正方形,整个拼图的图案也是支持从图片生成的。

CocoaPods: pod "TTGPuzzleVerify"
Github: https://github.com/zekunyan/TTGPuzzleVerify

接下来,说说TTGPuzzleVerify的整体设计思路和实现原理。

分析

组成部分

先直观上分析一下控件的组成部分:

组成部分

可以直观看出,控件应该由三个部分组成:

  • 拼图块:就是从原图上“抠”出来的拼图形状的图片块
  • 原始图片:拼图整体图案的图片
  • 镂空背景:从原始图片上抠出拼图块以后,给用户提示用的,透明度更低的拼图块镂空背景

拆解步骤

分析完了控件的组成,现在就来梳理一下控件的整个创建、交互流程。

步骤

  • 输入:图片、拼图形状、拼图大小、镂空的位置、拼图块的起始位置
  • 生成拼图路径:根据拼图类型,生成对应的贝塞尔曲线
  • 从图片抠出拼图块、镂空:有了拼图路径,就可以抠出拼图块和镂空的部分
  • 设置位置、样式:根据参数设置拼图块的初始位置、样式,如阴影、镂空的透明度等
  • 用户拖动:响应用户的头动手势,移动拼图块,根据坐标判断是否完成拼图
  • 回调:用户完成拼图后,要回调,让外部响应拼图完成事件

关键技术点

从上面的步骤可以整理出来几个关键的技术点,如下:

拼图的路径生成
普通的形状,如正方形、圆形之类的还好,代码实现比较方便,但是复杂的拼图图案怎么办?

从图片上“抠出”拼图块、镂空
有了拼图的形状路径,如何从图片上抠出拼图块和镂空?

用户拖动拼图、完成拼图验证
抠出了拼图块,如何将用户的手势对应到移动拼图块,最终完成验证?

下面针对这三个点着重写下实现原理。

实现

拼图的路径生成

既然手写代码太难,那就从现有的图案生成代码。

1. 找SVG素材
既然要能随意改变大小,就要找矢量的图,Google “free svg”、“free icon”之类的,很容易就能找到免费的矢量图素材,例如TTGPuzzleVerify里面用的拼图形状就是从iconmonstr.com找来的。

2. 编辑
直接下载回来的SVG还需要调整一下,如去除背景,我是用Sketch来编辑的。

拼图SVG编辑

3. 生成代码
拼图的路径本质上就是拼图的轮廓,在Sketch里面把拼图SVG的背景色等颜色删除,然后就是将其转换为代码。
这里可以用PaintCode plugin for Sketch,将Sketch的拼图矢量路径导出成代码。

4. 调整代码
直接生成的代码有时可能会不适用,可以人工调整下,然后就可以放到自己的类里面了:

1
2
3
4
5
6
7
8
9
10
11
12
+ (UIBezierPath *)classicPuzzlePath {
static UIBezierPath *puzzleShape = nil;

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
puzzleShape = [UIBezierPath bezierPath];
[puzzleShape moveToPoint:CGPointMake(17.45, 71.16)];

[puzzleShape closePath];
});
return puzzleShape;
}

5. UIBezierPath拼图路径的大小、位置调整
直接导出的拼图路径的大小、位置是固定的,但是控件是要可以支持改变大小、位置的,所以,要对路径做变换,刚好UIBezierPath支持CGAffineTransform变换。

大小变换:

1
2
3

[path applyTransform:CGAffineTransformMakeScale(_puzzleSize.width / path.bounds.size.width,
_puzzleSize.height / path.bounds.size.height)];

位置变换:

1
2
3
4
// 位置变换,path是生成的原始路径,_puzzleBlankPosition是传入的拼图块、镂空位置
[path applyTransform:CGAffineTransformMakeTranslation(
_puzzleBlankPosition.x - path.bounds.origin.x,
_puzzleBlankPosition.y - path.bounds.origin.y)];

至此,拼图的路径就写好了。

从图片上“抠出”拼图块、镂空

1. 拼图块
有了UIBezierPath路径,抠出拼图块还是比较容易的,直接用CAShapeLayer“mask”图片即可:

1
2
3
4
5
6
7
8
9
10

UIBezierPath *puzzlePath = [self getNewScaledPuzzledPath];


CAShapeLayer *puzzleMaskLayer = [CAShapeLayer new];
puzzleMaskLayer.frame = self.bounds;
puzzleMaskLayer.path = puzzlePath.CGPath;


_puzzleImageView.layer.mask = puzzleMaskLayer;

效果就是:

拼图块

2. 镂空图片
抠出拼图块还是很容易的,但是“镂空”怎么办?
其实道理是一样的,都是mask,只不过mask的区域反过来而已。
CAShapeLayerfillRule就可以调整“填充的规则”,此处用kCAFillRuleEvenOdd就可以了,文档的解释如下:

Specifies the even-odd winding rule. Count the total number of path crossings. If the number of crossings is even, the point is outside the path. If the number of crossings is odd, the point is inside the path and the region containing it should be filled.

理解过来就是:区域内一点,往外划一条线,经过奇数个数的交点,则点在区域内,偶数个数的交点,则在区域外。

所以,只要在拼图路径的基础上,补充最外部的边框的路径即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

UIBezierPath *maskPath = [UIBezierPath bezierPathWithRect:self.bounds];

[maskPath appendPath:[UIBezierPath bezierPathWithCGPath:puzzlePath.CGPath]];

maskPath.usesEvenOddFillRule = YES;


CAShapeLayer *frontMaskLayer = [CAShapeLayer new];
frontMaskLayer.frame = self.bounds;
frontMaskLayer.path = maskPath.CGPath;
frontMaskLayer.fillRule = kCAFillRuleEvenOdd;


_frontImageView.layer.mask = frontMaskLayer;

效果:

镂空

3. 镂空背景
有了拼图块、镂空,还差一块,就是镂空的“半透明背景”,其实这个“半透明背景”和拼图块是一模一样的,所以不再重复代码,唯一增加的就是设置其透明度。

4. 组合
将上面的组合到一起,就能形成最终的效果,从Reveal里面看到分层结构如下:

分层

其实就是三张一样的图片,按照不同的mask规则做了处理,叠加起来。

用户拖动拼图、完成拼图验证

1. 拖动图片
有了上面的结构,拖动拼图的处理也就容易多了,只要移动最上层的拼图块就行了,这里直接用UIPanGestureRecognizer实现,移动的时候基于拼图的中心移动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

UIPanGestureRecognizer *panGestureRecognizer = [UIPanGestureRecognizer new];
[panGestureRecognizer addTarget:self action:@selector(onPanGesture:)];
[self addGestureRecognizer:panGestureRecognizer];


- (void)onPanGesture:(UIPanGestureRecognizer *)panGestureRecognizer {

CGPoint panLocation = [panGestureRecognizer locationInView:self];

CGPoint position = CGPointZero;
position.x = panLocation.x - _puzzleSize.width / 2;
position.y = panLocation.y - _puzzleSize.height / 2;

[self setPuzzlePosition:position];

}

2. 验证用户的拼图是否完成
用户移动拼图时,只要判断拼图的坐标和镂空的位置是否一致就行。但是如果完全精确的判断,用户很难准确的把拼图拼对上,所以还要增加一定的“tolerance”,让拼图更加容易,同时暴漏出让外部设置:

1
@property (nonatomic, assign) CGFloat verificationTolerance; 

判断的逻辑如下:

1
2
3
4
5

- (BOOL)isVerified {
return fabsf([self puzzlePosition].x - _puzzleBlankPosition.x) <= _verificationTolerance &&
fabsf([self puzzlePosition].y - _puzzleBlankPosition.y) <= _verificationTolerance;
}

3. 完成拼图
当用户完成拼图后,外部会得到delegateblock的回调响应,接着就可以“完成拼图验证”,对此向外提供一个方法:- (void)completeVerificationWithAnimation:(BOOL)withAnimation,调用后拼图块将会移动回镂空位置,模拟出拼图完成的效果。

至此,TTGPuzzleVerify的关键实现技术点就是这些了~

最后

嗯,圣诞节快乐! – 2016-12-25