Swift 绘图板功能完善以及终极优化
轉載請注明出處:http://blog.csdn.net/zhangao0086/article/details/45289475。
前文總結
接著這篇:Swift 全功能的繪圖板開發,雖然在上一篇中我們已經完成了這些功能:
- 支持鉛筆繪圖(畫點)
- 支持畫直線
- 支持一些簡單的圖形(矩形、圓形等)
- 做一個真正的橡皮擦
- 能設置畫筆的粗細
- 能設置畫筆的顏色
- 能設置背景色或者背景圖
但是還有一個非常重要的功能沒有實現,沒錯,那就是 Undo/Redo!我之所以把這個功能單獨放出來是有原因的,一是因為上一篇已經篇幅太長,不適合繼續往上加內容;二是因為為了實現 Undo/Redo 功能,我們需要對 DrawingBoard 進行一些重構,在這篇文章中,你能看到用另一種方式實現的繪圖板。
實現的效果:
更新 ViewController
先添加兩張按鈕圖:
黑底、50%的透明度,箭頭用白色。
(PS:這可是我自己做的,別嫌棄)
圖片放到 Images.xcasserts 里:
(再次PS:圖嫌小的話,就放在2x上)
然后在 Storyboard 里添加兩個 Button:
注意里面的紅框,Button 與 Board 平級,并且在 Board 的上方。
Button 的約束如下:
兩個按鈕的點擊事件連接到 VC 里:
@IBAction func undo(sender: UIButton) {self.board.undo() }@IBAction func redo(sneder: UIButton) {self.board.redo() }(此時的 Board 還沒有 undo/redo 方法,你可以自行添加或者稍后再添加)
兩個按鈕本身也連接到 VC 里:
更新我們原viewDidLoad中的動畫方法,使兩個 Button 也適時的隱藏及顯示:
... self.board.drawingStateChangedBlock = {(state: DrawingState) -> () inif state != .Moved {UIView.beginAnimations(nil, context: nil)if state == .Began {self.topViewConstraintY.constant = -self.topView.frame.size.heightself.toolbarConstraintBottom.constant = -self.toolbar.frame.size.heightself.topView.layoutIfNeeded()self.toolbar.layoutIfNeeded()self.undoButton.alpha = 0 // 新增self.redoButton.alpha = 0 // 新增} else if state == .Ended {UIView.setAnimationDelay(1.0)self.topViewConstraintY.constant = 0self.toolbarConstraintBottom.constant = 0self.topView.layoutIfNeeded()self.toolbar.layoutIfNeeded()self.undoButton.alpha = 1 // 新增self.redoButton.alpha = 1 // 新增}UIView.commitAnimations()} } ...更新 Board
Undo/Redo 真正的邏輯都在Board 里面,我打算用圖片棧保存 DrawingBoard 的每一張圖,當 Undo/Redo 的時候直接把前一個狀態取出并顯示,為了分別存儲 Undo/Redo 操作所用的圖片,我們要建立兩個圖片棧:
private var undoImages = [UIImage]() private var redoImages = [UIImage]()然后加兩個工具方法:canUndo 和 canRedo :
var canUndo: Bool {get {return self.undoImages.count > 0 || self.image != nil} }var canRedo: Bool {get {return self.redoImages.count > 0} }然后是 undo/redo 這兩個主要方法:
func undo() {if self.canUndo == false {return}if self.undoImages.count > 0 {self.redoImages.append(self.image!)let lastImage = self.undoImages.removeLast()self.image = lastImage} else if self.image != nil {self.redoImages.append(self.image!)self.image = nil}self.realImage = self.image }func redo() {if self.canRedo == false {return}if self.redoImages.count > 0 {if self.image != nil {self.undoImages.append(self.image!)}let lastImage = self.redoImages.removeLast()self.image = lastImageself.realImage = self.image} }然后在每次畫新圖的時候保存下當前狀態:
private func drawingImage() {if let brush = self.brush {// hookif let drawingStateChangedBlock = self.drawingStateChangedBlock {drawingStateChangedBlock(state: self.drawingState)}UIGraphicsBeginImageContext(self.bounds.size)let context = UIGraphicsGetCurrentContext()UIColor.clearColor().setFill()UIRectFill(self.bounds)CGContextSetLineCap(context, kCGLineCapRound)CGContextSetLineWidth(context, self.strokeWidth)CGContextSetStrokeColorWithColor(context, self.strokeColor.CGColor)if let realImage = self.realImage {realImage.drawInRect(self.bounds)}brush.strokeWidth = self.strokeWidthbrush.drawInContext(context)CGContextStrokePath(context)let previewImage = UIGraphicsGetImageFromCurrentImageContext()if self.drawingState == .Ended || brush.supportedContinuousDrawing() {self.realImage = previewImage}UIGraphicsEndImageContext()// === 新增 ===if self.drawingState == .Began {self.redoImages = []if self.image != nil {self.undoImages.append(self.image!)}}// ======self.image = previewImagebrush.lastPoint = brush.endPoint} }這里面都有對 self.image 進行非空處理,其實原來不用這么麻煩,如果Swift 的數組支持插入Optional類型的話,我們直接把self.image插入到數組中,用的時候再取出來即可,因為 UIImageView 的 UIImage 是 Optional 類型的,賦一個 nil 給它沒有問題,就當是 undo 到初始化狀態了,但是偏偏 Swift的數組不支持插入Optional類型,這就導致我們不能記住 UIImageView 的初始化狀態,只能通過判斷它的image是否為 nil 來處理。
完成的邏輯很簡單:當畫圖開始的時候,保存當前 image 到 undo 棧中,并清空 redo 棧,進行 undo 操作的時候,能一直 undo,并將 undo 的 image 存進 redo 棧中,直到 self.image 為 nil。從這個邏輯可以看出兩點:redo 功能非常依賴 undo,畢竟沒有撤消就沒有重做;除此之外,當用戶開始繪制新圖的時候,我們也要清空 redo 棧,因為用戶已經“回不去”了。
完成這些工作后,就能測試 Undo/Redo 功能了~
關于內存的使用
我們很快地就加上了 Undo/Redo 功能,是吧? 通過維護兩個圖片棧,在進行相應的操作的時候,直接對 self.image 進行賦值,但是這么做有一個很明顯的弊端,就是內存使用毫無上限! 你可以很輕松地在 5s 上使內存使用達到 50M 甚至 100M,雖然我們做了一些處理,如當用戶繪制新圖時,清空 Redo 的圖片棧,但是這并不能從根本上解決問題。
要從根本上解決問題有兩種方式。
1. 用 CGPath 畫圖
假設換一種實現方式,不緩存圖片,而是保存每一步,這樣無疑會使內存使用量降低很多,取而代之的是在每次畫圖的時候需要有一個循環來重新畫每一步(可以嘗試用 clearsContextBeforeDrawing 屬性來優化),我個人覺得這種方式可能會比較惡心,因為畫的越多,性能就越差,我在前一篇里說過【為什么不用drawRect方法】:
為什么不用drawRect方法
其實我最開始也是使用drawRect方法來完成繪制,但是感覺限制很多,比如context無法保存,還是要每次重畫(雖然可以保存到一個BitMapContext里,但是這樣與保存到image里有什么區別呢?);后來用CALayer保存每一條CGPath,但是這樣仍然不能避免每次重繪,因為需要考慮到橡皮擦和畫筆屬性之類的影響,這么一來還不如采用image的方式來保存最新繪圖板。
既然定下了以image來保存繪圖板,那么drawRect就不方便了,因為不能用UIGraphicsBeginImageContext方法來創建一個ImageContext。
如果決定要用 CGPath 來畫圖的話,你除了要暴露一個CGPath和CGContext以外,你還需要用一個自定義的對象保存當前的繪圖狀態,如畫筆顏色、畫筆粗細、混合模式(Blend Mode)等(還會在后期遇到由于前期考慮不足的屬性沒有設置,然后才加上,這就破壞了“封閉-開放原則”),然后在每一個循環體中恢復當前的上下文,類似于這樣:
CGContextSaveGState... for path in paths {CGContextSetLineCap(context, kCGLineCapRound)CGContextSetLineWidth(context, self.strokeWidth)CGContextSetStrokeColorWithColor(context, self.strokeColor.CGColor)/* Add path and drawing... */CGContextRestoreGState... }從代碼上來說,想換成用CGPath實現也很容易,只需要改兩個地方:
我在 GitHub 里 DrawingBoard 工程里提交了這個分支:
DrawingBoard CGPath 分支
協議和drawingImage進行了適當的更新,繪圖是以CGPath來實現的,但是依然采用的是圖片棧的方式,感興趣的同學可以嘗試自己實現。
2. 優化圖片所占用的內存
除了用CGPath來優化以外,我們還可以直接優化圖片棧,用一個緩存或Undo控制器來控制所有的一切,在這個控制器里,將直接管理圖片緩存(內存和文件)、Undo、Redo操作,使 Board 的邏輯進一步的封裝。
不得不說,這才是我想要實現的方式,模塊之間可以達到真正的解耦,我將 Board的代碼去掉沒有改動的方法和屬性后貼在這里:
以磁盤代替了內存,這里有一些關鍵點:
那么效果如何呢?我在 4s、Plus 都有進行測試,由于 4s 性能相對較差,我以 4s 為主要測試對象,在內存較少的 4s 上:
在反復繪圖的情況下,內存也是毫無壓力的~!那么讀寫文件的時候是否會有卡頓呢?在 4s 上我發現遠未達到瓶頸:
(PS:4s 的閃存是C10級別)
cahcesLength 變量配合 index 可以進一步優化性能,在這里就不多做介紹了。
至此,DrawingBoard 就可以告一段落了。
GitHub
總結
以上是生活随笔為你收集整理的Swift 绘图板功能完善以及终极优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 十六款值得关注的NoSQL与NewSQL
- 下一篇: 随机秘钥