<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                # Swift 全功能的繪圖板開發 要做一個全功能的繪圖板,至少要支持以下這些功能: * 支持鉛筆繪圖(畫點) * 支持畫直線 * 支持一些簡單的圖形(矩形、圓形等) * 做一真正的橡皮擦 * 能設置畫筆的粗細 * 能設置畫筆的顏色 * 能設置背景色或者背景圖 * 能支持撤消與重做 * … 我們先做一些基礎性的工作,比如創建工程。? ![](https://box.kancloud.cn/2016-01-18_569ca44a3d6f4.png) * * * ## 工程搭建 先創建一個`Single View Application`?工程:? ![](https://box.kancloud.cn/2016-01-18_569ca44a4d02c.jpg)? 語言選擇`Swift`:? ![](https://box.kancloud.cn/2016-01-18_569ca44a67d64.jpg)? 為了最大程度的利用屏幕區域,我們完全隱藏掉狀態欄,在`Info.plist`里修改或添加這兩個參數:? ![](https://box.kancloud.cn/2016-01-18_569ca44a81d53.jpg)? 然后進入到`Main.storyboard`,開始搭建我們的UI。? 我們給已存在的`ViewController`的`View`添加一個`UIImageView`的子視圖,背景色設為`Light Gray`,然后添加4個約束,由于要做一個全屏的畫板,必須要讓`Constraint to margins`保持沒有選中的狀態,否則左右兩邊會留下蘋果建議的空白區域,最后把`User Interaction Enabled`打開:? ![](https://box.kancloud.cn/2016-01-18_569ca44a8fdfc.jpg)? ![](https://box.kancloud.cn/2016-01-18_569ca44aa3e47.jpg)? 然后我們回到`ViewController`的`View`上: * 添加一個放工具欄的容器:`UIView`,為該View設置約束:? ![](https://box.kancloud.cn/2016-01-18_569ca44ab9be3.jpg)? 同樣的不要選擇`Contraint to margins`。 * 在該View里添加一個`UISegmentedControl`,并給SegmentedControl設置6個選項,分別是: 1. 鉛筆 2. 直尺 3. 虛線 4. 矩形 5. 圓形 6. 橡皮擦 * 給這個SegmentedControl添加約束:? ![](https://box.kancloud.cn/2016-01-18_569ca44acc7d6.jpg)? 垂直居中,兩邊各留20,高度固定為28。 完整的UI及結構看起來像這樣:? ![](https://box.kancloud.cn/2016-01-18_569ca44adcdd8.jpg)? ImageView將會作為實際的繪制區域,頂部的SegmentedControl提供工具的選擇。 到目前為止我們還沒有寫下一行代碼,至此要開始編碼了。? ![](https://box.kancloud.cn/2016-01-18_569ca44af292a.png) > 你可能會注意到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 } } ~~~ 步驟解析: 1. 開啟一個新的ImageContext,為保存每次的繪圖狀態作準備。 2. 初始化context,進行基本設置(畫筆寬度、畫筆顏色、畫筆的圓潤度等)。 3. 把之前保存的圖片繪制進context中。 4. 設置`brush`的基本屬性,以便子類更方便的繪圖;調用具體的繪圖方法,并最終添加到context中。 5. 從當前的context中,得到Image,如果是`ended`狀態或者需要支持連續不斷的繪圖,則將Image保存到`realImage`中。 6. 實時顯示當前的繪制狀態,并記錄繪制的最后一個點。 這些工作完成以后,我們就可以開始寫第一個工具了:鉛筆工具。? ![](https://box.kancloud.cn/2016-01-18_569ca44b0c445.png) ### 鉛筆工具 鉛筆工具應該支持連續不斷的繪圖(不斷的保存到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畫線。? 這樣一來,一個鉛筆工具就完成了,怎么樣,很簡單吧。? ![](https://box.kancloud.cn/2016-01-18_569ca44b2fe50.png) * * * ## 測試 到目前為止,我們的`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]`? 編譯、運行,鉛筆工具可以完美運行~!? ![](https://box.kancloud.cn/2016-01-18_569ca44b42845.jpg) * * * ## 其他的工具 接下來我們把其他的繪圖工具也實現了。? 其他的工具不像鉛筆工具,不需要支持連續不斷的繪圖,所以也就不用覆蓋`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); ~~~ 加入這一句代碼,一個真正的橡皮擦便實現了。 * * * ## 再次測試 現在我們的工程結構應該類似于這樣:? ![](https://box.kancloud.cn/2016-01-18_569ca44b550f4.jpg)? 我們修改下`ViewController`中的`brushes`屬性的初始值: ~~~ var brushes = [PencilBrush(), LineBrush(), DashLineBrush(), RectangleBrush(), EllipseBrush(), EraserBrush()] ~~~ 編譯、運行:? ![](https://box.kancloud.cn/2016-01-18_569ca44b6abd9.jpg)? 除了橡皮擦擦除的范圍太小以外,一切都很完美~!? ![](https://box.kancloud.cn/2016-01-18_569ca44b812ad.png) * * * ## 設計思路 在繼續完成剩下的功能之前,我想先對之前的代碼進行些說明。 ### 為什么不用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(及其子類)也并不知道自己將會被用于哪,它們只需要實現自己的算法即可。類似于這樣的圖:? ![](https://box.kancloud.cn/2016-01-18_569ca44b99834.jpg)? 實際上這里包含了兩個設計模式。 #### 策略設計模式 `策略設計模式`的UML圖:? ![](https://box.kancloud.cn/2016-01-18_569ca44bd1665.jpg)? `策略設計模式`在iOS中也應用廣泛,如`AFNetworking`的`AFHTTPRequestSerializer`和`AFHTTPResponseSerializer`的設計,通過在運行時動態的改變委托對象,變換行為,使程序模塊之間解耦、提高應變能力。? 以我們的繪圖板為例,輸出不同的圖形就意味著不同的算法,用戶可根據不同的需求來選擇某一種算法,即BaseBrush及其子類做具體的封裝,這樣的好處是每一個子類只關心自己的算法,達到了高聚合的原則,高級模塊(Board)不用關心具體實現。? 想象一下,如果是讓Board里自身來處理這些算法,那代碼中無疑會充斥很多與算法選擇相關的邏輯,而且每增加一個算法都需要重新修改Board類,這又與代碼應該對拓展開放、對修改關閉原則有了沖突,而且每個類也應該只有一個責任。? 通過采用策略模式我們實現了一個好維護、易拓展的程序(媽媽再也不用擔心工具欄不夠用了^^)。 策略模式的定義:***定義一個算法群,把每一個算法分別封裝起來,讓它們之間可以互相替換,使算法的變化獨立于使用它的用戶之上。*** #### 模板方法 在傳統的策略模式中,每一個算法類都獨自完成整個算法過程,例如一個網絡解析程序,可能有一個算法用于解析`JSON`,有另一個算法用于解析`XML`等(另外一個例子是壓縮程序,用`ZIP`或`RAR`算法),獨自完成整個算法對靈活性更好,但免不了會有重復代碼,在`DrawingBoard`里我們做一個折中,盡量保證靈活性,又最大限度地避免重復代碼。? 我們將`BaseBrush`的角色提升為算法的基類,并提供一些便利的屬性(如`beginPoint`、`endPoint`、`strokeWidth`等),然后在`Board`的`drawingImage`方法里對`BaseBrush`的接口進行調用,而`BaseBrush`不會知道自己的接口是如何聯系起來的,雖然`supportedContinuousDrawing`(這是一個“鉤子”)甚至影響了算法的流程(鉛筆需要實時繪圖)。? 我們用`drawingImage`搭建了一個算法的骨架,看起來像是模板方法的UML圖:? ![](https://box.kancloud.cn/2016-01-18_569ca44be3724.jpg) > 圖中右邊的方框代表模板方法。 `BaseBrush`通過提供抽象方法(`drawInContext`)、具體方法或鉤子方法(`supportedContinuousDrawing`)來對應算法的每一個步驟,讓其子類可以重定義或實現這些步驟。同時,讓模板方法(即`dawingImage`)定義一個算法的骨架,模板方法不僅可以調用在抽象類中實現的基本方法,也可以調用在抽象類的子類中實現的基本方法,還可以調用其他對象中的方法。? 除了對算法的封裝以外,模板方法還能防止“循環依賴”,即高層組件依賴低層組件,反過來低層組件也依賴高層組件。想像一下,如果既讓Board選擇具體的算法子類,又讓算法類直接調用drawingImage方法(不提供鉤子,直接把Board的事件下發下去),那到時候就熱鬧了,這些類串在一起難以理解,又不好維護。 模板方法的定義:***在一個方法中定義一個算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以在不改變算法結構的情況下,重新定義算法中的某些步驟。*** > 其實模式都很簡單,很多人在工作中會思考如何讓自己的代碼變得更好,“情不自禁”地就會慢慢實現這些原則,了解模式的設計意圖,有助于在遇到需要折中的地方更加明白如何在設計上取舍。 以上就是我設計時的思路,說完了,接下來還要完成的工作有: * 提供對畫筆顏色、粗細的設置 * 背景設置 * 全屏繪圖(不能讓Board一直顯示不全) 先從畫筆開始,*Let’s go!* * * * ## 畫筆設置 不管是畫筆還是背景設置,我們都要有一個能提供設置的工具欄。 ### 設置工具欄 所以我們往`Board`上再蓋一個`UIToolbar`,與頂部的View類似: 1. 拖一個`UIToolbar`到`Board`的父類上,與`Board`的視圖層級平級。 2. 設置`UIToolbar`的約束:左、右、下間距為0,高為44:? ![](https://box.kancloud.cn/2016-01-18_569ca44c01956.jpg) 3. 往`UIToolbar`上拖一個`UIBarButtonItem`,`title`就寫:畫筆設置。 4. 在`ViewController`里增加一個`paintingBrushSettings`方法,并把`UIBarButtonItem`的`action`連接`paintingBrushSettings`方法上。 5. 在`ViewController`里增加一個`toolar`屬性,并把Xib中的`UIToolbar`連接到`toolbar`上。 UIToolbar配置好后,UI及視圖層級如下:? ![](https://box.kancloud.cn/2016-01-18_569ca44c12d73.jpg) ### RGBColorPicker 考慮到多個頁面需要選取自定義的顏色,我們先創建一個工具類:`RGBColorPicker`,用于選擇RGB顏色: ~~~ class RGBColorPicker: UIView { var colorChangedBlock: ((color: UIColor) -> Void)? private var sliders = [UISlider]() private var labels = [UILabel]() override init(frame: CGRect) { super.init(frame: frame) self.initial() } required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder) self.initial() } private func initial() { self.backgroundColor = UIColor.clearColor() let trackColors = [UIColor.redColor(), UIColor.greenColor(), UIColor.blueColor()] for index in 1...3 { let slider = UISlider() slider.minimumValue = 0 slider.value = 0 slider.maximumValue = 255 slider.minimumTrackTintColor = trackColors[index - 1] slider.addTarget(self, action: "colorChanged:", forControlEvents: .ValueChanged) self.addSubview(slider) self.sliders.append(slider) let label = UILabel() label.text = "0" self.addSubview(label) self.labels.append(label) } } override func layoutSubviews() { super.layoutSubviews() let sliderHeight = CGFloat(31) let labelWidth = CGFloat(29) let yHeight = self.bounds.size.height / CGFloat(sliders.count) for index in 0..<self.sliders.count { let slider = self.sliders[index] slider.frame = CGRect(x: 0, y: CGFloat(index) * yHeight, width: self.bounds.size.width - labelWidth - 5.0, height: sliderHeight) let label = self.labels[index] label.frame = CGRect(x: CGRectGetMaxX(slider.frame) + 5, y: slider.frame.origin.y, width: labelWidth, height: sliderHeight) } } override func intrinsicContentSize() -> CGSize { return CGSize(width: UIViewNoIntrinsicMetric, height: 107) } @IBAction private func colorChanged(slider: UISlider) { let color = UIColor( red: CGFloat(self.sliders[0].value / 255.0), green: CGFloat(self.sliders[1].value / 255.0), blue: CGFloat(self.sliders[2].value / 255.0), alpha: 1) let label = self.labels[find(self.sliders, slider)!] label.text = NSString(format: "%.0f", slider.value) if let colorChangedBlock = self.colorChangedBlock { colorChangedBlock(color: color) } } func setCurrentColor(color: UIColor) { var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0 color.getRed(&red, green: &green, blue: &blue, alpha: nil) let colors = [red, green, blue] for index in 0..<self.sliders.count { let slider = self.sliders[index] slider.value = Float(colors[index]) * 255 let label = self.labels[index] label.text = NSString(format: "%.0f", slider.value) } } } ~~~ 這個工具類很簡單,沒有采用Auto Layout進行布局,因為`layoutSubviews`方法已經能很好的滿足我們的需求了。當用戶拖動任何一個`UISlider`的時候,我們能實時的通過`colorChangedBlock`回調給外部。它能展現一個這樣的視圖:? ![](https://box.kancloud.cn/2016-01-18_569ca44c2ddc8.jpg)? 不過雖然該工具類本身沒有采用Auto Layout進行布局,但是它還是支持Auto Layout的,當它被添加到某個Auto Layout的視圖中的時候,Auto Layout布局系統可以通過`intrinsicContentSize`知道該視圖的尺寸信息。? 最后它還有一個`setCurrentColor`方法從外部接收一個UIColor,可以用于初始化。 ### 畫筆設置的UI 我打算在用戶點擊`畫筆設置`的時候,從底部彈出一個控制面板(就像系統的`Control Center`那樣),所以我們還要有一個像這樣的設置UI:? ![](https://box.kancloud.cn/2016-01-18_569ca44c3ba1b.jpg)? 具體的,創建一個`PaintingBrushSettingsView`類,同時創建一個`PaintingBrushSettingsView.xib`文件,并把xib中view的`Class`設為`PaintingBrushSettingsView`,設置view的背景色為透明: 1. 放置一個title為“畫筆粗細”的`UILabel`,約束設為:寬度固定為68,高度固定為21,左和上邊距為8。 2. 放置一個title為“1”的`UILabel`,“1”與“畫筆粗細”的垂直間距為10,寬度固定為10,高度固定為21,與`superview`的左邊距為10。 3. 放置一個`UISlider`,用于調節畫筆的粗細,與“1”的水平間距為5,并與“1”垂直居中,高度固定為30,寬度暫時不設,在`PaintingBrushSettingsView`中添加`strokeWidthSlider`屬性,與之連接起來。 4. 放置一個title為“20”的`UILabel`,約束設為:寬度固定為20,高度固定為21,top與“1”相同,與`superview`的右間距為10。并把上一步中的`UISlider`的右間距設為與“20”相隔5。 5. 放置一個title為“畫筆顏色”的`UILabel`,寬、高、left與“畫筆粗細”相同,與上面`UISlider`的垂直間距設為12。 6. 放置一個`UIView`至“畫筆顏色”下方(上圖中被選中的那個UIView),寬度固定為50,高度固定為30,left與“畫筆顏色”相同,并且與“畫筆顏色”的垂直間距為5,在`PaintingBrushSettingsView`中添加`strokeColorPreview`屬性,與之連接起來。 7. 放置一個`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](https://developer.apple.com/library/ios/releasenotes/DeveloperTools/RN-Xcode/Chapters/xc6_release_notes.html#//apple_ref/doc/uid/TP40001051-CH4-DontLinkElementID_23),下面是用`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*到了這一點,毛玻璃效果就算實現了~~? ![](https://box.kancloud.cn/2016-01-18_569ca44c4da62.png) ### 測試畫筆設置 我們在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 } ~~~ 這么一來畫筆設置就做完了,代碼應該還是比較好理解,編譯、運行后,應該能看到:? ![](https://box.kancloud.cn/2016-01-18_569ca44c6035c.jpg)![](https://box.kancloud.cn/2016-01-18_569ca44c7340b.jpg)![](https://box.kancloud.cn/2016-01-18_569ca44c83fe7.jpg)? 完成度已經很高了^^!? ![](https://box.kancloud.cn/2016-01-18_569ca44c9709b.png) * * * ## 背景設置 整體的框架基本上已經在之前的工作中搭好了,我們快速過掉這一節。? 在`Main.storyboard`中增加了一個title為“背景設置”的`UIBarButtonItem`,并將action連接到`ViewController`的`backgroundSettings`方法上,你可以選擇在插入“背景設置”之前,先插入一個`FlexibleSpace`的`UIBarButtonItem`。? 創建`BackgroundSettingsVC`類,繼承自`UIViewController`,這與畫筆設置繼承于`UIView`不同,我們希望背景設置可以在用戶的相冊中選擇照片,而使用`UIImagePickerController`的前提是要實現`UIImagePickerControllerDelegate`、`UINavigationControllerDelegate`兩個接口,如果讓UIView來實現這兩個接口會很奇怪。? 創建一個`BackgroundSettingsVC.xib`文件: 1. 放置一個title為“從相冊中選擇背景圖”的UIButton,約束為:左、上邊距為8,寬度固定為135,高度固定為30。 2. 放置一個RGBColorPicker,約束為:左、右邊距為8,與UIButton的垂直間距為20,底部與superview齊平。 3. 把UIButton的`Touch Up Inside`事件連接到`BackgroundSettingsVC`的`pickImage`方法上;RGBColorPicker連接到`BackgroundSettingsVC`的`colorPicker`屬性上。 看上去像這樣:? ![](https://box.kancloud.cn/2016-01-18_569ca44ca347a.jpg) `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() } ~~~ 編譯、運行,現在你可以用不同的背景色(或背景圖)了!? ![](https://box.kancloud.cn/2016-01-18_569ca44cb5d9b.jpg)![](https://box.kancloud.cn/2016-01-18_569ca44cc9e83.jpg)![](https://box.kancloud.cn/2016-01-18_569ca44ce8a0d.jpg) * * * ## 全屏繪圖 到目前為止,`Board`一直顯示不全(事實上,我很早就實現了全屏繪圖,但是優先級一直被我排在最后![](https://box.kancloud.cn/2015-11-04_5639cee14c0a0.gif)),現在是時候來解決它了。? 解決思路是這樣的:當用戶開始繪圖的時候,我們把頂部和底部兩個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<Void>) { if let err = error { UIAlertView(title: "錯誤", message: err.localizedDescription, delegate: nil, cancelButtonTitle: "確定").show() } else { UIAlertView(title: "提示", message: "保存成功", delegate: nil, cancelButtonTitle: "確定").show() } } ~~~ ![](https://box.kancloud.cn/2016-01-18_569ca44d10882.jpg)? 旅行到終點了~!? ![](https://box.kancloud.cn/2016-01-18_569ca44d67c3d.png) * * * ## 感謝一路的陪伴! 看了下,有些小長,文本+代碼有2w3+,全部代碼去除空行和空格有1w4+,直接貼代碼會簡單很多,但我始終覺得讓代碼完成功能并不是全部目的,代碼背后隱藏的問題定義、設計、構建更有意義,畢竟軟件開發完成“后”比完成“前”所花費的時間永遠更多(除非是一個只有10行代碼或者“一次性”的程序)。? 希望與大家多多交流。 > 最后吐槽下CSDN新的Markdown編輯器,代碼樣式丑且不能自定義,而且有些代碼高亮都無法識別。不過感覺草稿箱比以前更方便,問題主要還是集中在樣式上,希望以后能不斷改進,會一如既往的支持。 * * * ## 更新——撤消與重做功能 [Swift 繪圖板功能完善以及終極優化](http://blog.csdn.net/zhangao0086/article/details/45289475) * * * ## GitHub地址 [DrawingBoard](https://github.com/zhangao0086/DrawingBoard)
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看