原网址:https://code.tutsplus.com/tutorials/smooth-freehand-drawing-on-ios--mobile-13164
博客中英文转载链接:http://blog.csdn.net/u013410274/article/details/78894413
整理的代码地址 :http://download.csdn.net/download/u013410274/10203776
该文章中文采取的直接网页翻译而来
本教程将教您如何在iOS设备上实现高级绘图算法,以实现流畅的手绘。继续阅读
触摸是用户与iOS设备交互的主要方式。这些设备预期提供的最自然和最明显的功能之一是允许用户用手指在屏幕上画画。目前App Store中有许多徒手绘制和记笔记应用程序,许多公司甚至要求客户在购买时签署iDevice。这些应用程序如何实际工作?让我们停下来思考一下“引擎盖下”是怎么回事。
当用户滚动表格视图,捏放大图片,或在绘画应用程序绘制曲线时,设备显示正在快速更新(例如,每秒60次),应用程序运行循环不断采样用户的手指的位置。在此过程中,拖动屏幕的手指的“模拟”输入必须转换为显示器上的数字点集,并且此转换过程可能构成重大挑战。在我们的绘画应用程序的背景下,我们手上有一个“数据拟合”的问题。当用户在设备上愉快地涂写时,程序员必须插入iOS中报告给我们的采样触点中丢失的模拟信息(“连接点”)。而且,这种内插必须以这样的方式发生,即对于终端用户来说,结果是连续的,自然的,平滑的笔画,就好像他正在用纸笔在笔记本上画草图一样。
本教程的目的是展示如何在iOS上实现徒手画,从一个执行直线插值的基本算法开始,并推进到一个更接近于像Penultimate这样的着名应用程序提供的质量的更复杂的算法。好像创建一个工作起来的算法不够困难,我们也需要确保算法运行良好。正如我们将看到的,一个天真的绘图实现可能会导致一个具有重大性能问题的应用程序,这将使绘图繁琐,最终无法使用。
我假设你对iOS开发并不是全新的东西,所以我已经略过了创建一个新项目,向这个项目添加文件的步骤等等。希望这里没有任何困难,但是为了以防万一完整的项目代码可供您下载和玩耍。
基于“ 单一视图应用程序 ”模板启动一个新的Xcode iPad项目,并命名为“ FreehandDrawingTut ”。一定要启用自动引用计数(ARC),但取消选择故事板和单元测试。您可以使这个项目是一个iPhone或通用的应用程序,这取决于你有什么样的设备可供测试。
接下来,继续在Xcode Navigator中选择“FreeHandDrawingTut”项目,并确保只支持纵向:
如果您要部署到iOS 5.x或更早版本,则可以通过以下方式更改方向支持:
1
2
3
4
|
- (
BOOL
)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return
(interfaceOrientation == UIInterfaceOrientationPortrait);
}
|
我正在这样做,以保持简单,所以我们可以专注于主要的问题。
我想迭代地开发我们的代码,并以渐进的方式改进代码 - 就像你从头开始实际操作一样 - 而不是一下子把最终版本放在你的头上。我希望这种方法能让你更好地处理不同的问题。记住这一点,为了避免在同一个文件中反复删除,修改和添加代码,这可能会变得混乱和容易出错,我将采取以下方法:
在Xcode中,选择File> New> File ...,选择Objective-C类作为模板,然后在下一个屏幕上命名文件LinearInterpView并将其设置为UIView的子类。保存。名称“LinearInterp”是“线性插值”的缩写。为了本教程,我将命名每个我们创建的UIView子类,以强调在类代码中引入的一些概念或方法。
正如我前面提到的,你可以保留头文件。删除LinearInterpView.m文件中的所有代码,并将其替换为以下内容:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
三十
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
#import "LinearInterpView.h"
@implementation
LinearInterpView
{
UIBezierPath
*path;
// (3)
}
- (
id
)initWithCoder:(
NSCoder
*)aDecoder
// (1)
{
if
(
self
= [
super
initWithCoder
:aDecoder])
{
[
self
setMultipleTouchEnabled
:
NO
];
// (2)
[
self
setBackgroundColor
:[
UIColor
whiteColor
]];
path = [
UIBezierPath
bezierPath
];
[path
setLineWidth
:
2
.0
];
}
return
self
;
}
- (
void
)drawRect:(CGRect)rect
// (5)
{
[[
UIColor
blackColor
]
setStroke
];
[path
stroke
];
}
- (
void
)touchesBegan:(
NSSet
*)touches
withEvent
:(
UIEvent
*)event
{
UITouch
*touch = [touches
anyObject
];
CGPoint
p = [touch
locationInView
:
self
];
[path
moveToPoint
:p];
}
- (
void
)touchesMoved:(
NSSet
*)touches
withEvent
:(
UIEvent
*)event
{
UITouch
*touch = [touches
anyObject
];
CGPoint
p = [touch
locationInView
:
self
];
[path
addLineToPoint
:p];
// (4)
[
self
setNeedsDisplay
];
}
- (
void
)touchesEnded:(
NSSet
*)touches
withEvent
:(
UIEvent
*)event
{
[
self
touchesMoved
:touches
withEvent
:event];
}
- (
void
)touchesCancelled:(
NSSet
*)touches
withEvent
:(
UIEvent
*)event
{
[
self
touchesEnded
:touches
withEvent
:event];
}
@end
|
在此代码中,我们直接处理应用程序每次触摸序列时向我们报告的触摸事件。也就是说,用户将手指放在屏幕视图上,将手指移过屏幕,最后将手指从屏幕上抬起。对于这个序列中的每个事件,应用程序向我们发送相应的消息(在iOS术语中,消息被发送到“第一响应者”;可以参考文档以获得详细信息)。
为了处理这些消息,我们实现了-touchesBegan:WithEvent:
在UIView继承的UIResponder类中声明的方法和公司。我们可以编写代码来处理触摸事件,无论我们喜欢什么。在我们的应用程序中,我们要查询触摸的屏幕位置,做一些处理,然后在屏幕上画线。
这些点来自上面的代码相应的评论数字:
-initWithCoder:
是因为视图是由XIB生成的,因为我们将很快建立。UIBezierPath
是一个UIKit类,可让我们在由直线或某些类型的曲线组成的屏幕上绘制形状。-drawRect:
方法。每次添加新的线段时,我们都会通过抚摸路径来完成此操作。-drawRect:
方法时绘制的“画布” ,并且所看到的结果就是屏幕上的视图。我们很快就会遇到另一种绘图环境。在构建应用程序之前,我们需要将刚刚创建的视图子类设置为屏幕视图。
现在构建应用程序。你应该得到一个闪亮的白色的视图,你可以用你的手指画。考虑到我们编写的几行代码,结果并不是太简单!当然,他们也不是很壮观。连接点的外观是相当明显的(是的,我的手写也吸)。
确保你不仅在模拟器上而且在真实的设备上运行应用程序。
如果您在设备上使用应用程序一段时间,您一定会注意到一些事情:最终,UI响应开始滞后,而不是由于某种原因每秒获取的〜60个触点,用户界面能够进一步采样下降。由于点越来越分离,直线插值使绘图甚至比以前更“块”。这当然是不可取的。发生什么了?
让我们回顾一下我们已经做的事情:当我们绘制时,我们获取点,将它们添加到不断增长的路径中,然后在主循环的每个循环中渲染*完整*路径。所以随着路径变长,在每一次迭代中,绘图系统都有更多的绘制,最终变得太多,使得应用难以跟上。由于一切都在主线上发生,我们的绘图代码与UI代码竞争,其中包括在屏幕上对触摸进行采样。
你会被原谅的,认为有一种方法可以在屏幕上显示已经存在的内容。不幸的是,这是我们需要摆脱纸上笔的类比的地方,因为图形系统默认情况下不是那样工作的。虽然凭借我们接下来要写的代码,但我们间接地要实施“借鉴”方法。
虽然有几件事情我们可能会试图解决我们的代码的性能,但我们只是实现一个想法,因为事实证明,这足以满足我们目前的需求。
创建一个新的UIView子类像之前,将其命名为CachedLIView(LI的是提醒我们我们还在做大号 inear 我 nterpolation)。删除CachedLIView.m的所有内容,并将其替换为以下内容:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
三十
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
#import "CachedLIView.h"
@implementation
CachedLIView
{
UIBezierPath
*path;
UIImage
*incrementalImage;
// (1)
}
- (
id
)initWithCoder:(
NSCoder
*)aDecoder
{
if
(
self
= [
super
initWithCoder
:aDecoder])
{
[
self
setMultipleTouchEnabled
:
NO
];
[
self
setBackgroundColor
:[
UIColor
whiteColor
]];
path = [
UIBezierPath
bezierPath
];
[path
setLineWidth
:
2
.0
];
}
return
self
;
}
- (
void
)drawRect:(CGRect)rect
{
[incrementalImage
drawInRect
:rect];
// (3)
[path
stroke
];
}
- (
void
)touchesBegan:(
NSSet
*)touches
withEvent
:(
UIEvent
*)event
{
UITouch
*touch = [touches
anyObject
];
CGPoint
p = [touch
locationInView
:
self
];
[path
moveToPoint
:p];
}
- (
void
)touchesMoved:(
NSSet
*)touches
withEvent
:(
UIEvent
*)event
{
UITouch
*touch = [touches
anyObject
];
CGPoint
p = [touch
locationInView
:
self
];
[path
addLineToPoint
:p];
[
self
setNeedsDisplay
];
}
- (
void
)touchesEnded:(
NSSet
*)touches
withEvent
:(
UIEvent
*)event
// (2)
{
UITouch
*touch = [touches
anyObject
];
CGPoint
p = [touch
locationInView
:
self
];
[path
addLineToPoint
:p];
[
self
drawBitmap
];
// (3)
[
self
setNeedsDisplay
];
[path
removeAllPoints
];
//(4)
}
- (
void
)touchesCancelled:(
NSSet
*)touches
withEvent
:(
UIEvent
*)event
{
[
self
touchesEnded
:touches
withEvent
:event];
}
- (
void
)drawBitmap
// (3)
{
UIGraphicsBeginImageContextWithOptions(
self
.bounds
.size
,
YES
,
0
.0
);
[[
UIColor
blackColor
]
setStroke
];
if
(!incrementalImage)
// first draw; paint background white by ...
{
UIBezierPath
*rectpath = [
UIBezierPath
bezierPathWithRect
:
self
.bounds
];
// enclosing bitmap by a rectangle defined by another UIBezierPath object
[[
UIColor
whiteColor
]
setFill
];
[rectpath
fill
];
// filling it with white
}
[incrementalImage
drawAtPoint
:CGPointZero];
[path
stroke
];
incrementalImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
@end
|
保存之后,请记住将XIB中的视图对象的类更改为CachedLIView!
当用户将他的手指放在屏幕上画画时,我们从一条没有点或线的新路径开始,并且像前面一样向它添加线段。
再次提到评论中的数字:
-drawRect:
这个环境时,会自动提供给我们,并反映我们在屏幕视图中绘制的内容。相反,位图上下文需要被显式地创建和销毁,并且绘制的内容驻留在存储器中。drawRect:
调用,我们首先将内存缓冲区的内容绘制到我们的视图中(在设计上)具有完全相同的大小,因此对于用户,我们保持连续绘制的幻觉,只是以不同于以前的方式。虽然这不是完美的(如果我们的用户在不举起手指的情况下继续绘画,那会怎样?),这对于本教程的范围来说已经足够了。鼓励你自己试验,找到更好的方法。例如,您可以尝试周期性地缓存图形,而不是仅当用户举起手指时。碰巧,这个离屏缓存过程为我们提供了后台处理的机会,如果我们选择实施它的话。但是我们不打算在本教程中这样做。尽管你被邀请自己尝试!
现在让我们把注意力转移到使图画“看起来更好”。到目前为止,我们已经用直线段连接相邻的触点。但通常当我们徒手画画的时候,我们的自然中风有一个自由流动的曲线(而不是块状和刚性的)。我们尝试用曲线而不是线段插入我们的点是有道理的。幸运的是,UIBezierPath类让我们绘制它的同名曲线:贝塞尔曲线。
什么是贝塞尔曲线?在不调用数学定义的情况下,贝塞尔曲线由四个点定义:一条曲线通过的两个端点和两个“控制点”,它们有助于定义曲线在其端点处必须接触的切线(技术上这是一条三次贝塞尔曲线,但为简单起见,我将它简称为“贝塞尔曲线”)。
贝塞尔曲线允许我们绘制各种有趣的形状。
我们现在要尝试的是对四个相邻接触点的序列进行分组,并在Bezier曲线段内插入点序列。为了保持笔画的连续性,每一对相邻的贝塞尔段将共享一个共同的端点。
你现在知道演习。创建一个新的UIView子类并将其命名为BezierInterpView。将以下代码粘贴到.m文件中:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
三十
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
|
#import "BezierInterpView.h"
@implementation
BezierInterpView
{
UIBezierPath
*path;
UIImage
*incrementalImage;
CGPoint
pts[
4
];
// to keep track of the four points of our Bezier segment
uint ctr;
// a counter variable to keep track of the point index
}
- (
id
)initWithCoder:(
NSCoder
*)aDecoder
{
if
(
self
= [
super
initWithCoder
:aDecoder])
{
[
self
setMultipleTouchEnabled
:
NO
];
[
self
setBackgroundColor
:[
UIColor
whiteColor
]];
path = [
UIBezierPath
bezierPath
];
[path
setLineWidth
:
2
.0
];
}
return
self
;
}
- (
void
)drawRect:(CGRect)rect
{
[incrementalImage
drawInRect
:rect];
[path
stroke
];
}
- (
void
)touchesBegan:(
NSSet
*)touches
withEvent
:(
UIEvent
*)event
{
ctr =
0
;
UITouch
*touch = [touches
anyObject
];
pts[
0
] = [touch
locationInView
:
self
];
}
- (
void
)touchesMoved:(
NSSet
*)touches
withEvent
:(
UIEvent
*)event
{
UITouch
*touch = [touches
anyObject
];
CGPoint
p = [touch
locationInView
:
self
];
ctr++;
pts[ctr] = p;
if
(ctr ==
3
)
// 4th point
{
[path
moveToPoint
:pts[
0
]];
[path
addCurveToPoint
:pts[
3
]
controlPoint1
:pts[
1
]
controlPoint2
:pts[
2
]];
// this is how a Bezier curve is appended to a path. We are adding a cubic Bezier from pt[0] to pt[3], with control points pt[1] and pt[2]
[
self
setNeedsDisplay
];
pts[
0
] = [path
currentPoint
];
ctr =
0
;
}
}
- (
void
)touchesEnded:(
NSSet
*)touches
withEvent
:(
UIEvent
*)event
{
[
self
drawBitmap
];
[
self
setNeedsDisplay
];
pts[
0
] = [path
currentPoint
];
// let the second endpoint of the current Bezier segment be the first one for the next Bezier segment
[path
removeAllPoints
];
ctr =
0
;
}
- (
void
)touchesCancelled:(
NSSet
*)touches
withEvent
:(
UIEvent
*)event
{
[
self
touchesEnded
:touches
withEvent
:event];
}
- (
void
)drawBitmap
{
UIGraphicsBeginImageContextWithOptions(
self
.bounds
.size
,
YES
,
0
.0
);
[[
UIColor
blackColor
]
setStroke
];
if
(!incrementalImage)
// first time; paint background white
{
UIBezierPath
*rectpath = [
UIBezierPath
bezierPathWithRect
:
self
.bounds
];
[[
UIColor
whiteColor
]
setFill
];
[rectpath
fill
];
}
[incrementalImage
drawAtPoint
:CGPointZero];
[path
stroke
];
incrementalImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
@end
|
正如在线评论所指出的那样,主要的变化是引入了一些新变量来跟踪贝塞尔曲线中的点,并修改-(void)touchesMoved:withEvent:
了每四个点绘制一个贝塞尔曲线的方法(实际上每三个点,就应用程序向我们报告的情况而言,因为我们为每一对相邻的贝塞尔分段共享一个端点)。
您可能会在这里指出,在我们有足够的点来完成最后的Bezier段之前,我们忽略了用户举起手指并结束触摸顺序的情况。如果是这样,你会是对的!虽然在视觉上这并没有太大的区别,在某些重要的情况下,它确实如此。例如,尝试绘制一个小圆圈。它可能不完全关闭,在一个真正的应用程序中,你想要在-touchesEnded:WithEvent
方法中适当地处理这个。虽然我们在这里,但我们也没有特别注意触摸取消的情况。该touchesCancelled:WithEvent
实例方法处理这个。看看官方文档,看看是否有任何特殊情况需要在这里处理。
那么,结果是什么样的?我再次提醒您在建立之前在XIB中设置正确的课程。
呵呵。这似乎不是一个很大的改进,是吗?我认为这可能比直线插值稍好一些,或许这只是一厢情愿的想法。无论如何,没有什么值得吹嘘的。
以下是我认为正在发生的事情:虽然我们不费力地用平滑的曲线段插入四个点的每个序列,但是我们没有努力使曲线段平滑过渡到下一个曲线段,所以有效地仍然有最终结果的问题。
那么我们能做些什么呢?如果我们要坚持我们在最后一个版本中开始的方法(即使用贝塞尔曲线),则需要考虑两个相邻贝塞尔分段的“交点”的连续性和平滑性。在相应的控制点(第一段的第二控制点和第二段的第一控制点)的终点处的两个切线似乎是关键; 如果这两个切线都具有相同的方向,则曲线在交叉点处将会更平滑。
如果我们将公共端点移动到连接两个控制点的线路上?在不利用关于接触点的附加数据的情况下,最好的一点似乎是考虑到连接两个控制点的线的中点,并且我们对于两个切线的方向所强加的要求将得到满足。我们来试试吧!
创建一个UIView子类(再次),并命名为SmoothedBIView。将.m文件中的所有代码替换为以下内容:
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
|
#import "SmoothedBIView.h"
@implementation
SmoothedBIView
{
UIBezierPath
*path;
UIImage
*incrementalImage;
CGPoint
pts[
5
];
// we now need to keep track of the four points of a Bezier segment and the first control point of the next segment
uint ctr;
}
- (
id
)initWithCoder:(
NSCoder
*)aDecoder
{
if
(
self
= [
super
initWithCoder
:aDecoder])
{
[
self
setMultipleTouchEnabled
:
NO
];
[
self
setBackgroundColor
:[
UIColor
whiteColor
]];
path = [
UIBezierPath
bezierPath
];
[path
setLineWidth
:
2
.0
];
}
return
self
;
}
- (
id
)initWithFrame:(CGRect)frame
{
self
= [
super
initWithFrame
:frame];
if
(
self
) {
[
self
setMultipleTouchEnabled
:
NO
];
path = [
UIBezierPath
bezierPath
];
[path
setLineWidth
:
2
.0
];
}
return
self
;
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (
void
)drawRect:(CGRect)rect
{
[incrementalImage
drawInRect
:rect];
[path
stroke
];
}
- (
void
)touchesBegan:(
NSSet
|