Swift 全功能的繪圖板開發
原文 http://www.cocoachina.com/swift/20151125/14390.html
要做一個全功能的繪圖板,至少要支持以下這些功能:
-
支持鉛筆繪圖(畫點)
-
支持畫直線
-
支持一些簡單的圖形(矩形、圓形等)
-
做一個真正的橡皮擦
-
能設置畫筆的粗細
-
能設置畫筆的顏色
-
能設置背景色或者背景圖
-
能支持撤消與重做
-
…
我們先做一些基礎性的工作,比如創建工程。
工程搭建
先創建一個Single View Application 工程:
語言選擇Swift:
為了最大程度的利用屏幕區域,我們完全隱藏掉狀態欄,在Info.plist里修改或添加這兩個參數:
然后進入到Main.storyboard,開始搭建我們的UI。
我們給已存在的ViewController的View添加一個UIImageView的子視圖,背景色設為Light Gray,然后添加4個約束,由于要做一個全屏的畫板,必須要讓Constraint to margins保持沒有選中的狀態,否則左右兩邊會留下蘋果建議的空白區域,最后把User Interaction Enabled打開:
然后我們回到ViewController的View上:
-
添加一個放工具欄的容器:UIView,為該View設置約束:
同樣的不要選擇Contraint to margins。
-
在該View里添加一個UISegmentedControl,并給SegmentedControl設置6個選項,分別是:
-
鉛筆
-
直尺
-
虛線
-
矩形
-
圓形
-
橡皮擦
-
給這個SegmentedControl添加約束:
垂直居中,兩邊各留20,高度固定為28。
完整的UI及結構看起來像這樣:
ImageView將會作為實際的繪制區域,頂部的SegmentedControl提供工具的選擇。 到目前為止我們還沒有寫下一行代碼,至此要開始編碼了。
你可能會注意到Board有一部分被擋住了,這只是暫時的~
施工…
Board
我們創建一個Board類,繼承自UIImageView,同時把這個類設置為Main.storyboard中ImageView的Class,這樣當app啟動的時候就會自動創建一個Board的實例了。
增加兩個屬性以及初始化方法:
var strokeWidth: CGFloat var strokeColor: UIColor override init() { self.strokeColor = UIColor.blackColor() self.strokeWidth = 1 super.init() } required init(coder aDecoder: NSCoder) { self.strokeColor = UIColor.blackColor() self.strokeWidth = 1 super.init(coder: aDecoder) }
由于我們是依賴于touches方法來完成繪圖過程,我們需要記錄下每次touch的狀態,比如began、moved、ended等,為此我們創建一個枚舉,在touches方法中進行記錄,并調用私有的繪圖方法drawingImage:
enum DrawingState { case Began, Moved, Ended } class Board: UIImageView { private var drawingState: DrawingState! // 此處省略init方法與另外兩個屬性 // MARK: - touches methods override func touchesBegan(touches: NSSet, withEvent event: UIEvent) { self.drawingState = .Began self.drawingImage() } override func touchesMoved(touches: NSSet, withEvent event: UIEvent) { self.drawingState = .Moved self.drawingImage() } override func touchesEnded(touches: NSSet, withEvent event: UIEvent) { self.drawingState = .Ended self.drawingImage() } // MARK: - drawing private func drawingImage() { // 暫時為空實現 } }
在我們實現drawingImage方法之前,我們先創建另外一個重要的組件:BaseBrush。
BaseBrush
顧名思義,BaseBrush將會作為一個繪圖的基類而存在,我們會在它的基礎上創建一系列的子類,以達到彈性的設計目的。為此,我們創建一個BaseBrush類,并實現一個PaintBrush接口:
import CoreGraphics protocol PaintBrush { func supportedContinuousDrawing() -> Bool; func drawInContext(context: CGContextRef) } class BaseBrush : NSObject, PaintBrush { var beginPoint: CGPoint! var endPoint: CGPoint! var lastPoint: CGPoint? var strokeWidth: CGFloat! func supportedContinuousDrawing() -> Bool { return false } func drawInContext(context: CGContextRef) { assert(false, "must implements in subclass.") } }
BaseBrush實現了PaintBrush接口,PaintBrush聲明了兩個方法:
-
supportedContinuousDrawing,表示是否是連續不斷的繪圖
-
drawInContext,基于Context的繪圖方法,子類必須實現具體的繪圖
只要是實現了PaintBrush接口的類,我們就當作是一個繪圖工具(如鉛筆、直尺等),而BaseBrush除了實現PaintBrush接口以外,我們還為它增加了四個便利屬性:
-
beginPoint,開始點的位置
-
endPoint,結束點的位置
-
lastPoint,最后一個點的位置(也可以稱作是上一個點的位置)
-
strokeWidth,畫筆的寬度
這么一來,子類也可以很方便的獲取到當前的狀態,并作一些深度定制的繪圖方法。
lastPoint的意義:beginPoint和endPoint很好理解,beginPoint是手勢剛識別時的點,只要手勢不結束,那么beginPoint在手勢識別期間是不會變的;endPoint總是表示手勢最后識別的點;除了鉛筆以外,其他的圖形用這兩個屬性就夠了,但是用鉛筆在移動的時候,不能每次從beginPoint畫到endPoint,如果是那樣的話就是畫直線了,而是應該從上一次畫的位置(lastPoint)畫到endPoint,這樣才是連貫的線。
回到Board
我們實現了一個畫筆的基類之后,就可以重新回到Board類了,畢竟我們之前的工作還沒有做完,現在是時候完善Board類了。
我們用Board實際操縱BaseBrush,先為Board添加兩個新的屬性:
var brush: BaseBrush? private var realImage: UIImage?
brush對應到具體的畫筆類,realImage保存當前的圖形,重新修改touches方法,以便增加對brush屬性的處理,完整的touches方法實現如下:
// MARK: - touches methods override func touchesBegan(touches: NSSet, withEvent event: UIEvent) { if let brush = self.brush { brush.lastPoint = nil brush.beginPoint = touches.anyObject()!.locationInView(self) brush.endPoint = brush.beginPoint self.drawingState = .Began self.drawingImage() } } override func touchesMoved(touches: NSSet, withEvent event: UIEvent) { if let brush = self.brush { brush.endPoint = touches.anyObject()!.locationInView(self) self.drawingState = .Moved self.drawingImage() } } override func touchesCancelled(touches: NSSet!, withEvent event: UIEvent!) { if let brush = self.brush { brush.endPoint = nil } } override func touchesEnded(touches: NSSet, withEvent event: UIEvent) { if let brush = self.brush { brush.endPoint = touches.anyObject()!.locationInView(self) self.drawingState = .Ended self.drawingImage() } }
我們需要防止brush為nil的情況,以及為brush設置好beginPoint和endPoint,之后我們就可以完善drawingImage方法了,實現如下:
private func drawingImage() { if let brush = self.brush { // 1. UIGraphicsBeginImageContext(self.bounds.size) // 2. let context = UIGraphicsGetCurrentContext() UIColor.clearColor().setFill() UIRectFill(self.bounds) CGContextSetLineCap(context, kCGLineCapRound) CGContextSetLineWidth(context, self.strokeWidth) CGContextSetStrokeColorWithColor(context, self.strokeColor.CGColor) // 3. if let realImage = self.realImage { realImage.drawInRect(self.bounds) } // 4. brush.strokeWidth = self.strokeWidth brush.drawInContext(context); CGContextStrokePath(context) // 5. let previewImage = UIGraphicsGetImageFromCurrentImageContext() if self.drawingState == .Ended || brush.supportedContinuousDrawing() { self.realImage = previewImage } UIGraphicsEndImageContext() // 6. self.image = previewImage; brush.lastPoint = brush.endPoint } }
步驟解析:
開啟一個新的ImageContext,為保存每次的繪圖狀態作準備。
初始化context,進行基本設置(畫筆寬度、畫筆顏色、畫筆的圓潤度等)。
把之前保存的圖片繪制進context中。
設置brush的基本屬性,以便子類更方便的繪圖;調用具體的繪圖方法,并最終添加到context中。
從當前的context中,得到Image,如果是ended狀態或者需要支持連續不斷的繪圖,則將Image保存到realImage中。
實時顯示當前的繪制狀態,并記錄繪制的最后一個點。
這些工作完成以后,我們就可以開始寫第一個工具了:鉛筆工具。
鉛筆工具
鉛筆工具應該支持連續不斷的繪圖(不斷的保存到realImage中),這也是我們給PaintBrush接口增加supportedContinuousDrawing方法的原因,考慮到用戶的手指可能快速的移動,導致從一個點到另一個點有著跳躍性的動作,我們對鉛筆工具采用畫直線的方式來實現。
首先創建一個類,名為PencilBrush,繼承自BaseBrush類,實現如下:
class PencilBrush: BaseBrush { override func drawInContext(context: CGContextRef) { if let lastPoint = self.lastPoint { CGContextMoveToPoint(context, lastPoint.x, lastPoint.y) CGContextAddLineToPoint(context, endPoint.x, endPoint.y) } else { CGContextMoveToPoint(context, beginPoint.x, beginPoint.y) CGContextAddLineToPoint(context, endPoint.x, endPoint.y) } } override func supportedContinuousDrawing() -> Bool { return true } }
如果lastPoint為nil,則基于beginPoint畫線,反之則基于lastPoint畫線。
這樣一來,一個鉛筆工具就完成了,怎么樣,很簡單吧。
測試
到目前為止,我們的ViewController還保持著默認的狀態,是時候先為鉛筆工具寫一些測試代碼了。
在ViewController添加board屬性,并與Main.storyboard中的Board關聯起來;創建一個brushes屬性,并為之賦值為:
var brushes = [PencilBrush()]
在ViewController中添加switchBrush:方法,并把Main.storyboard中的SegmentedControl的ValueChanged連接到ViewController的switchBrush:方法上,實現如下:
@IBAction func switchBrush(sender: UISegmentedControl) { assert(sender.tag < self.brushes.count, "!!!") self.board.brush = self.brushes[sender.selectedSegmentIndex] }
最后在viewDidLoad方法中做一個初始化:
self.board.brush = brushes[0]
編譯、運行,鉛筆工具可以完美運行~!
其他的工具
接下來我們把其他的繪圖工具也實現了。
其他的工具不像鉛筆工具,不需要支持連續不斷的繪圖,所以也就不用覆蓋supportedContinuousDrawing方法了。
直尺
創建一個LineBrush類,實現如下:
class LineBrush: BaseBrush { override func drawInContext(context: CGContextRef) { CGContextMoveToPoint(context, beginPoint.x, beginPoint.y) CGContextAddLineToPoint(context, endPoint.x, endPoint.y) } }
虛線
創建一個DashLineBrush類,實現如下:
class DashLineBrush: BaseBrush { override func drawInContext(context: CGContextRef) { let lengths: [CGFloat] = [self.strokeWidth * 3, self.strokeWidth * 3] CGContextSetLineDash(context, 0, lengths, 2); CGContextMoveToPoint(context, beginPoint.x, beginPoint.y) CGContextAddLineToPoint(context, endPoint.x, endPoint.y) } }
這里我們就用到了BaseBrush的strokeWidth屬性,因為我們想要創建一條動態的虛線。
矩形
創建一個RectangleBrush類,實現如下:
class RectangleBrush: BaseBrush { override func drawInContext(context: CGContextRef) { CGContextAddRect(context, CGRect(origin: CGPoint(x: min(beginPoint.x, endPoint.x), y: min(beginPoint.y, endPoint.y)), size: CGSize(width: abs(endPoint.x - beginPoint.x), height: abs(endPoint.y - beginPoint.y)))) } }
我們用到了一些計算,因為我們希望矩形的區域不是由beginPoint定死的。
圓形
創建一個EllipseBrush類,實現如下:
class EllipseBrush: BaseBrush { override func drawInContext(context: CGContextRef) { CGContextAddEllipseInRect(context, CGRect(origin: CGPoint(x: min(beginPoint.x, endPoint.x), y: min(beginPoint.y, endPoint.y)), size: CGSize(width: abs(endPoint.x - beginPoint.x), height: abs(endPoint.y - beginPoint.y)))) } }
同樣有一些計算,理由同上。
橡皮擦
從本文一開始就說過了,我們要做一個真正的橡皮擦,網上有很多的橡皮擦的實現其實就是把畫筆顏色設置為背景色,但是如果背景色可以動態設置,甚至設置為一個漸變的圖片時,這種方法就失效了,所以有些繪圖app的背景色就是固定為白色的。
其實Apple的Quartz2D框架本身就是支持橡皮擦的,只用一個方法就可以完美實現。
讓我們創建一個EraserBrush類,實現如下:
class EraserBrush: PencilBrush { override func drawInContext(context: CGContextRef) { CGContextSetBlendMode(context, kCGBlendModeClear); super.drawInContext(context) } }
注意,與其他的工具不同,橡皮擦是繼承自PencilBrush的,因為橡皮擦本身也是基于點的,而drawInContext里也只是加了一句:
CGContextSetBlendMode(context, kCGBlendModeClear);
加入這一句代碼,一個真正的橡皮擦便實現了。
再次測試
現在我們的工程結構應該類似于這樣:
我們修改下ViewController中的brushes屬性的初始值:
var brushes = [PencilBrush(), LineBrush(), DashLineBrush(), RectangleBrush(), EllipseBrush(), EraserBrush()]
編譯、運行:
除了橡皮擦擦除的范圍太小以外,一切都很完美~!
設計思路
在繼續完成剩下的功能之前,我想先對之前的代碼進行些說明。
為什么不用drawRect方法
其實我最開始也是使用drawRect方法來完成繪制,但是感覺限制很多,比如context無法保存,還是要每次重畫(雖然可以保存到一個BitMapContext里,但是這樣與保存到image里有什么區別呢?);后來用CALayer保存每一條CGPath,但是這樣仍然不能避免每次重繪,因為需要考慮到橡皮擦和畫筆屬性之類的影響,這么一來還不如采用image的方式來保存最新繪圖板。
既然定下了以image來保存繪圖板,那么drawRect就不方便了,因為不能用UIGraphicsBeginImageContext方法來創建一個ImageContext。
ViewController與Board、BaseBrush之間的關系
在ViewController、Board和BaseBrush這三者之間,雖然VC要知道另外兩個組件,但是僅限于選擇對應的工具給Board,Board本身并不知道當前的brush是哪個brush,也不需要知道其內部實現,只管調用對應的brush就行了;BaseBrush(及其子類)也并不知道自己將會被用于哪,它們只需要實現自己的算法即可。類似于這樣的圖:
實際上這里包含了兩個設計模式。
策略設計模式
策略設計模式的UML圖:
策略設計模式在iOS中也應用廣泛,如AFNetworking的AFHTTPRequestSerializer和AFHTTPResponseSerializer的設計,通過在運行時動態的改變委托對象,變換行為,使程序模塊之間解耦、提高應變能力。
以我們的繪圖板為例,輸出不同的圖形就意味著不同的算法,用戶可根據不同的需求來選擇某一種算法,即BaseBrush及其子類做具體的封裝,這樣的好處是每一個子類只關心自己的算法,達到了高聚合的原則,高級模塊(Board)不用關心具體實現。
想象一下,如果是讓Board里自身來處理這些算法,那代碼中無疑會充斥很多與算法選擇相關的邏輯,而且每增加一個算法都需要重新修改Board類,這又與代碼應該對拓展開放、對修改關閉原則有了沖突,而且每個類也應該只有一個責任。
通過采用策略模式我們實現了一個好維護、易拓展的程序(媽媽再也不用擔心工具欄不夠用了^^)。
策略模式的定義:定義一個算法群,把每一個算法分別封裝起來,讓它們之間可以互相替換,使算法的變化獨立于使用它的用戶之上。
模板方法
在傳統的策略模式中,每一個算法類都獨自完成整個算法過程,例如一個網絡解析程序,可能有一個算法用于解析JSON,有另一個算法用于解析XML等(另外一個例子是壓縮程序,用ZIP或RAR算法),獨自完成整個算法對靈活性更好,但免不了會有重復代碼,在DrawingBoard里我們做一個折中,盡量保證靈活性,又最大限度地避免重復代碼。
我們將BaseBrush的角色提升為算法的基類,并提供一些便利的屬性(如beginPoint、endPoint、strokeWidth等),然后在Board的drawingImage方法里對BaseBrush的接口進行調用,而BaseBrush不會知道自己的接口是如何聯系起來的,雖然supportedContinuousDrawing(這是一個“鉤子”)甚至影響了算法的流程(鉛筆需要實時繪圖)。
我們用drawingImage搭建了一個算法的骨架,看起來像是模板方法的UML圖:
圖中右邊的方框代表模板方法。
BaseBrush通過提供抽象方法(drawInContext)、具體方法或鉤子方法(supportedContinuousDrawing)來對應算法的每一個步驟,讓其子類可以重定義或實現這些步驟。同時,讓模板方法(即dawingImage)定義一個算法的骨架,模板方法不僅可以調用在抽象類中實現的基本方法,也可以調用在抽象類的子類中實現的基本方法,還可以調用其他對象中的方法。
除了對算法的封裝以外,模板方法還能防止“循環依賴”,即高層組件依賴低層組件,反過來低層組件也依賴高層組件。想像一下,如果既讓Board選擇具體的算法子類,又讓算法類直接調用drawingImage方法(不提供鉤子,直接把Board的事件下發下去),那到時候就熱鬧了,這些類串在一起難以理解,又不好維護。
模板方法的定義:在一個方法中定義一個算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以在不改變算法結構的情況下,重新定義算法中的某些步驟。
其實模式都很簡單,很多人在工作中會思考如何讓自己的代碼變得更好,“情不自禁”地就會慢慢實現這些原則,了解模式的設計意圖,有助于在遇到需要折中的地方更加明白如何在設計上取舍。
以上就是我設計時的思路,說完了,接下來還要完成的工作有:
-
提供對畫筆顏色、粗細的設置
-
背景設置
-
全屏繪圖(不能讓Board一直顯示不全)
先從畫筆開始,Let’s go!
畫筆設置
不管是畫筆還是背景設置,我們都要有一個能提供設置的工具欄。
設置工具欄
所以我們往Board上再蓋一個UIToolbar,與頂部的View類似:
-
拖一個UIToolbar到Board的父類上,與Board的視圖層級平級。
-
設置UIToolbar的約束:左、右、下間距為0,高為44:
-
-
往UIToolbar上拖一個UIBarButtonItem,title就寫:畫筆設置。
-
在ViewController里增加一個paintingBrushSettings方法,并把UIBarButtonItem的action連接paintingBrushSettings方法上。
-
在ViewController里增加一個toolar屬性,并把Xib中的UIToolbar連接到toolbar上。
UIToolbar配置好后,UI及視圖層級如下:
RGBColorPicker
考慮到多個頁面需要選取自定義的顏色,我們先創建一個工具類:RGBColorPicker,用于選擇RGB顏色:



這個工具類很簡單,沒有采用Auto Layout進行布局,因為layoutSubviews方法已經能很好的滿足我們的需求了。當用戶拖動任何一個UISlider的時候,我們能實時的通過colorChangedBlock回調給外部。它能展現一個這樣的視圖:
不過雖然該工具類本身沒有采用Auto Layout進行布局,但是它還是支持Auto Layout的,當它被添加到某個Auto Layout的視圖中的時候,Auto Layout布局系統可以通過intrinsicContentSize知道該視圖的尺寸信息。
最后它還有一個setCurrentColor方法從外部接收一個UIColor,可以用于初始化。
畫筆設置的UI
我打算在用戶點擊畫筆設置的時候,從底部彈出一個控制面板(就像系統的Control Center那樣),所以我們還要有一個像這樣的設置UI:
具體的,創建一個PaintingBrushSettingsView類,同時創建一個PaintingBrushSettingsView.xib文件,并把xib中view的Class設為PaintingBrushSettingsView,設置view的背景色為透明:
-
放置一個title為“畫筆粗細”的UILabel,約束設為:寬度固定為68,高度固定為21,左和上邊距為8。
-
放置一個title為“1”的UILabel,“1”與“畫筆粗細”的垂直間距為10,寬度固定為10,高度固定為21,與superview的左邊距為10。
-
放置一個UISlider,用于調節畫筆的粗細,與“1”的水平間距為5,并與“1”垂直居中,高度固定為30,寬度暫時不設,在PaintingBrushSettingsView中添加strokeWidthSlider屬性,與之連接起來。
-
放置一個title為“20”的UILabel,約束設為:寬度固定為20,高度固定為21,top與“1”相同,與superview的右間距為10。并把上一步中的UISlider的右間距設為與“20”相隔5。
-
放置一個title為“畫筆顏色”的UILabel,寬、高、left與“畫筆粗細”相同,與上面UISlider的垂直間距設為12。
-
放置一個UIView至“畫筆顏色”下方(上圖中被選中的那個UIView),寬度固定為50,高度固定為30,left與“畫筆顏色”相同,并且與“畫筆顏色”的垂直間距為5,在PaintingBrushSettingsView中添加strokeColorPreview屬性,與之連接起來。
-
放置一個UIView,把它的Class改為RGBColorPicker,約束設為:left與頂部的UISlider相同,底部與superview的間距為0,右間距為10,與上一步中的UIView的垂直間距為5。
PaintingBrushSettingsView類的完整代碼如下:
class PaintingBrushSettingsView : UIView { var strokeWidthChangedBlock: ((strokeWidth: CGFloat) -> Void)? var strokeColorChangedBlock: ((strokeColor: UIColor) -> Void)? @IBOutlet private var strokeWidthSlider: UISlider! @IBOutlet private var strokeColorPreview: UIView! @IBOutlet private var colorPicker: RGBColorPicker! override func awakeFromNib() { super.awakeFromNib() self.strokeColorPreview.layer.borderColor = UIColor.blackColor().CGColor self.strokeColorPreview.layer.borderWidth = 1 self.colorPicker.colorChangedBlock = { [unowned self] (color: UIColor) in self.strokeColorPreview.backgroundColor = color if let strokeColorChangedBlock = self.strokeColorChangedBlock { strokeColorChangedBlock(strokeColor: color) } } self.strokeWidthSlider.addTarget(self, action: "strokeWidthChanged:", forControlEvents: .ValueChanged) } func setBackgroundColor(color: UIColor) { self.strokeColorPreview.backgroundColor = color self.colorPicker.setCurrentColor(color) } func strokeWidthChanged(slider: UISlider) { if let strokeWidthChangedBlock = self.strokeWidthChangedBlock { strokeWidthChangedBlock(strokeWidth: CGFloat(slider.value)) } } }
strokeWidthChangedBlock和strokeColorChangedBlock兩個Block用于給外部傳遞狀態。setBackgroundColor用于初始化。
關于 Swift 1.2
在 Swift 1.2里,不能用 setBackgroundColor方法了,具體的,見Xcode 6.3的發布文檔:Xcode 6.3 Release Notes,下面是用didSet代替原有的setBackgroundColor方法:
override var backgroundColor: UIColor? { didSet { self.strokeColorPreview.backgroundColor = self.backgroundColor self.colorPicker.setCurrentColor(self.backgroundColor!) super.backgroundColor = oldValue } }
實現毛玻璃效果
在把PaintingBrushSettingsView顯示出來之前,我們要先想一想以何種方式展現比較好,眾所周知Control Center是有毛玻璃效果的,我們也想要這樣的效果,而且不用自己實現。那如何產生效果? 答案是用UIToolbar就行了。
UIToolbar本身就是帶有毛玻璃效果的,只要你不設置背景色,并且translucent屬性為true,“恰好”我們頁面底部就有一個UIToolbar,我們把它拉高就可以插入展現PaintingBrushSettingsView了。
只要get到了這一點,毛玻璃效果就算實現了~~
測試畫筆設置
我們在ViewController新增加幾個屬性:
var toolbarEditingItems: [UIBarButtonItem]? var currentSettingsView: UIView? @IBOutlet var toolbarConstraintHeight: NSLayoutConstraint!
toolbarConstraintHeight連接到Main.storyboard中對應的約束上就行了。toolbarEditingItems能讓我們在UIToolbar上顯示不同的items,本來還需要一個toolbarItems屬性的,因為UIViewController類本身就自帶,我們便不用單獨新增。currentSettingsView是用來保存當前展示的哪個設置頁面,考慮到我們后面會增加背景設置,這個屬性還是有必要的。
我們先寫一個往toolbar上添加約束的工具方法:
func addConstraintsToToolbarForSettingsView(view: UIView) { view.setTranslatesAutoresizingMaskIntoConstraints(false) self.toolbar.addSubview(view) self.toolbar.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[settingsView]-0-|", options: .DirectionLeadingToTrailing, metrics: nil, views: ["settingsView" : view])) self.toolbar.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[settingsView(==height)]", options: .DirectionLeadingToTrailing, metrics: ["height" : view.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height], views: ["settingsView" : view])) }
這個工具方法會把傳入進來的view添加到toolbar上,同時添加相應的約束。注意高度的約束,我是通過systemLayoutSizeFittingSize方法計算出設置視圖最佳的高度,這是為了達到更好的拓展性(背景設置與畫筆設置所需要的高度很可能會不同)。
然后再增加一個setupBrushSettingsView方法:
func setupBrushSettingsView() { let brushSettingsView = UINib(nibName: "PaintingBrushSettingsView", bundle: nil).instantiateWithOwner(nil, options: nil).first as PaintingBrushSettingsView self.addConstraintsToToolbarForSettingsView(brushSettingsView) brushSettingsView.hidden = true brushSettingsView.tag = 1 brushSettingsView.setBackgroundColor(self.board.strokeColor) brushSettingsView.strokeWidthChangedBlock = { [unowned self] (strokeWidth: CGFloat) -> Void in self.board.strokeWidth = strokeWidth } brushSettingsView.strokeColorChangedBlock = { [unowned self] (strokeColor: UIColor) -> Void in self.board.strokeColor = strokeColor } }
我們在這個方法里實例化了一個PaintingBrushSettingsView,并添加到toolbar上,增加相應的約束,以及一些初始化設置和兩個Block回調的處理。
然后修改viewDidLoad方法,增加以下行為:
//--- self.toolbarEditingItems = [ UIBarButtonItem(barButtonSystemItem:.FlexibleSpace, target: nil, action: nil), UIBarButtonItem(title: "完成", style:.Plain, target: self, action: "endSetting") ] self.toolbarItems = self.toolbar.items self.setupBrushSettingsView() //---
在paintingBrushSettings方法里響應點擊:
@IBAction func paintingBrushSettings() { self.currentSettingsView = self.toolbar.viewWithTag(1) self.currentSettingsView?.hidden = false self.updateToolbarForSettingsView() } func updateToolbarForSettingsView() { self.toolbarConstraintHeight.constant = self.currentSettingsView!.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height + 44 self.toolbar.setItems(self.toolbarEditingItems, animated: true) UIView.beginAnimations(nil, context: nil) self.toolbar.layoutIfNeeded() UIView.commitAnimations() self.toolbar.bringSubviewToFront(self.currentSettingsView!) }
updateToolbarForSettingsView也是一個工具方法,用于更新toolbar的高度。
由于我們采用了Auto Layout進行布局,動畫要通過調用layoutIfNeeded方法來實現。
響應點擊“完成”按鈕的endSetting方法:
@IBAction func endSetting() { self.toolbarConstraintHeight.constant = 44 self.toolbar.setItems(self.toolbarItems, animated: true) UIView.beginAnimations(nil, context: nil) self.toolbar.layoutIfNeeded() UIView.commitAnimations() self.currentSettingsView?.hidden = true }
這么一來畫筆設置就做完了,代碼應該還是比較好理解,編譯、運行后,應該能看到:
完成度已經很高了^^!
背景設置
整體的框架基本上已經在之前的工作中搭好了,我們快速過掉這一節。
在Main.storyboard中增加了一個title為“背景設置”的UIBarButtonItem,并將action連接到ViewController的backgroundSettings方法上,你可以選擇在插入“背景設置”之前,先插入一個FlexibleSpace的UIBarButtonItem。
創建BackgroundSettingsVC類,繼承自UIViewController,這與畫筆設置繼承于UIView不同,我們希望背景設置可以在用戶的相冊中選擇照片,而使用UIImagePickerController的前提是要實現UIImagePickerControllerDelegate、UINavigationControllerDelegate兩個接口,如果讓UIView來實現這兩個接口會很奇怪。
創建一個BackgroundSettingsVC.xib文件:
放置一個title為“從相冊中選擇背景圖”的UIButton,約束為:左、上邊距為8,寬度固定為135,高度固定為30。 放置一個RGBColorPicker,約束為:左、右邊距為8,與UIButton的垂直間距為20,底部與superview齊平。 把UIButton的Touch Up Inside事件連接到BackgroundSettingsVC的pickImage方法上;RGBColorPicker連接到BackgroundSettingsVC的colorPicker屬性上。
看上去像這樣:
BackgroundSettingsVC類的完整代碼:
class BackgroundSettingsVC : UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { var backgroundImageChangedBlock: ((backgroundImage: UIImage) -> Void)? var backgroundColorChangedBlock: ((backgroundColor: UIColor) -> Void)? @IBOutlet private var colorPicker: RGBColorPicker! lazy private var pickerController: UIImagePickerController = { [unowned self] in let pickerController = UIImagePickerController() pickerController.delegate = self return pickerController }() override func awakeFromNib() { super.awakeFromNib() self.colorPicker.colorChangedBlock = { [unowned self] (color: UIColor) in if let backgroundColorChangedBlock = self.backgroundColorChangedBlock { backgroundColorChangedBlock(backgroundColor: color) } } } func setBackgroundColor(color: UIColor) { self.colorPicker.setCurrentColor(color) } @IBAction func pickImage() { self.presentViewController(self.pickerController, animated: true, completion: nil) } // MARK: UIImagePickerControllerDelegate Methods func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [NSObject : AnyObject]) { let image = info[UIImagePickerControllerOriginalImage] as UIImage if let backgroundImageChangedBlock = self.backgroundImageChangedBlock { backgroundImageChangedBlock(backgroundImage: image) } self.dismissViewControllerAnimated(true, completion: nil) } // MARK: UINavigationControllerDelegate Methods func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) { UIApplication.sharedApplication().setStatusBarHidden(true, withAnimation: .None) } }
同樣用兩個Block進行回調;setBackgroundColor公共方法用于設置內部的RGBColorPicker的初始顏色狀態;在UINavigationControllerDelegate里隱藏系統默認顯示的狀態欄。
回到ViewController,我們對背景設置進行測試。
像setupBrushSettingsView方法一樣,我們增加一個setupBackgroundSettingsView方法:
func setupBackgroundSettingsView() { let backgroundSettingsVC = UINib(nibName: "BackgroundSettingsVC", bundle: nil).instantiateWithOwner(nil, options: nil).first as BackgroundSettingsVC self.addConstraintsToToolbarForSettingsView(backgroundSettingsVC.view) backgroundSettingsVC.view.hidden = true backgroundSettingsVC.view.tag = 2 backgroundSettingsVC.setBackgroundColor(self.board.backgroundColor!) self.addChildViewController(backgroundSettingsVC) backgroundSettingsVC.backgroundImageChangedBlock = { [unowned self] (backgroundImage: UIImage) in self.board.backgroundColor = UIColor(patternImage: backgroundImage) } backgroundSettingsVC.backgroundColorChangedBlock = { [unowned self] (backgroundColor: UIColor) in self.board.backgroundColor = backgroundColor } }
修改viewDidLoad方法:
self.toolbarEditingItems = [ UIBarButtonItem(barButtonSystemItem:.FlexibleSpace, target: nil, action: nil), UIBarButtonItem(title: "完成", style:.Plain, target: self, action: "endSetting") ] self.toolbarItems = self.toolbar.items self.setupBrushSettingsView() self.setupBackgroundSettingsView() // Added~!!!
實現backgroundSettings方法:
@IBAction func backgroundSettings() { self.currentSettingsView = self.toolbar.viewWithTag(2) self.currentSettingsView?.hidden = false self.updateToolbarForSettingsView() }
編譯、運行,現在你可以用不同的背景色(或背景圖)了!
全屏繪圖
到目前為止,Board一直顯示不全(事實上,我很早就實現了全屏繪圖,但是優先級一直被我排在最后),現在是時候來解決它了。
解決思路是這樣的:當用戶開始繪圖的時候,我們把頂部和底部兩個View隱藏;當用戶結束繪圖的時候,再讓兩個View顯示。
為了獲取用戶的繪圖狀態,我們需要在Board里加個“鉤子”:
// 增加一個Block回調 var drawingStateChangedBlock: ((state: DrawingState) -> ())? private func drawingImage() { if let brush = self.brush { // hook if let drawingStateChangedBlock = self.drawingStateChangedBlock { drawingStateChangedBlock(state: self.drawingState) } UIGraphicsBeginImageContext(self.bounds.size) // ...
這樣一來用戶繪圖的狀態就在ViewController掌握中了。
ViewController想要控制兩個View的話,還需要增加幾個屬性:
@IBOutlet var topView: UIView! @IBOutlet var topViewConstraintY: NSLayoutConstraint! @IBOutlet var toolbarConstraintBottom: NSLayoutConstraint!
然后在viewDidLoad方法里增加對“鉤子”的處理:
self.board.drawingStateChangedBlock = {(state: DrawingState) -> () in if state != .Moved { UIView.beginAnimations(nil, context: nil) if state == .Began { self.topViewConstraintY.constant = -self.topView.frame.size.height self.toolbarConstraintBottom.constant = -self.toolbar.frame.size.height self.topView.layoutIfNeeded() self.toolbar.layoutIfNeeded() } else if state == .Ended { UIView.setAnimationDelay(1.0) self.topViewConstraintY.constant = 0 self.toolbarConstraintBottom.constant = 0 self.topView.layoutIfNeeded() self.toolbar.layoutIfNeeded() } UIView.commitAnimations() } }
只有當狀態為開始或結束的時候我們才需要更新UI狀態,而且我們在結束的事件里延遲了1秒鐘,這樣用戶可以暫時預覽下全圖。
依靠Auto Layout布局系統以及我們在鉤子里對高度的處理,用戶在設置頁面繪圖時也能完美運行。
保存到圖庫
最后一個功能:保存到圖庫!
在toolbar上插入一個title為“保存到圖庫”的UIBarButtonItem,還是可以先插入一個FlexibleSpace的UIBarButtonItem,然后把action連接到ViewController的saveToAlbumy方法上:
@IBAction func saveToAlbum() { UIImageWriteToSavedPhotosAlbum(self.board.takeImage(), self, "image:didFinishSavingWithError:contextInfo:", nil) }
我為Board添加一個新的公共方法:takeImage:
func takeImage() -> UIImage { UIGraphicsBeginImageContext(self.bounds.size) self.backgroundColor?.setFill() UIRectFill(self.bounds) self.image?.drawInRect(self.bounds) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image }
然后是一個方法指針的回調:
func image(image: UIImage, didFinishSavingWithError error: NSError?, contextInfo:UnsafePointer) { if let err = error { UIAlertView(title: "錯誤", message: err.localizedDescription, delegate: nil, cancelButtonTitle: "確定").show() } else { UIAlertView(title: "提示", message: "保存成功", delegate: nil, cancelButtonTitle: "確定").show() } }
旅行到終點了~!
感謝一路的陪伴!
看了下,有些小長,文本+代碼有2w3+,全部代碼去除空行和空格有1w4+,直接貼代碼會簡單很多,但我始終覺得讓代碼完成功能并不是全部目的,代碼背后隱藏的問題定義、設計、構建更有意義,畢竟軟件開發完成“后”比完成“前”所花費的時間永遠更多(除非是一個只有10行代碼或者“一次性”的程序)。
希望與大家多多交流。