原網址: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
*)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 ==
4
)
{
pts[
3
] = CGPointMake((pts[
2
]
.x
+ pts[
4
]
.x
)/
2
.0
, (pts[
2
]
.y
+ pts[
4
]
.y
)/
2
.0
);
// move the endpoint to the middle of the line joining the second control point of the first Bezier segment and the first control point of the second Bezier segment
[path
moveToPoint
:pts[
0
]];
[path
addCurveToPoint
:pts[
3
]
controlPoint1
:pts[
1
]
controlPoint2
:pts[
2
]];
// add a cubic Bezier from pt[0] to pt[3], with control points pt[1] and pt[2]
[
self
setNeedsDisplay
];
// replace points and get ready to handle the next segment
pts[
0
] = pts[
3
];
pts[
1
] = pts[
4
];
ctr =
1
;
}
}
- (
void
)touchesEnded:(
NSSet
*)touches
withEvent
:(
UIEvent
*)event
{
[
self
drawBitmap
];
[
self
setNeedsDisplay
];
[path
removeAllPoints
];
ctr =
0
;
}
- (
void
)touchesCancelled:(
NSSet
*)touches
withEvent
:(
UIEvent
|