<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>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                # Swift 繪圖板功能完善以及終極優化 ## 前文總結 接著這篇:[Swift 全功能的繪圖板開發](http://blog.csdn.net/zhangao0086/article/details/43836789),雖然在上一篇中我們已經完成了這些功能: * 支持鉛筆繪圖(畫點) * 支持畫直線 * 支持一些簡單的圖形(矩形、圓形等) * 做一個真正的橡皮擦 * 能設置畫筆的粗細 * 能設置畫筆的顏色 * 能設置背景色或者背景圖 但是還有一個非常重要的功能沒有實現,沒錯,那就是 Undo/Redo!我之所以把這個功能單獨放出來是有原因的,一是因為上一篇已經篇幅太長,不適合繼續往上加內容;二是因為為了實現 Undo/Redo 功能,我們需要對 DrawingBoard 進行一些重構,在這篇文章中,你能看到用另一種方式實現的繪圖板。 實現的效果:? ![](https://box.kancloud.cn/2016-01-18_569ca44da8b7b.jpg) * * * ## 更新 ViewController 先添加兩張按鈕圖:? ![](https://box.kancloud.cn/2016-01-18_569ca44dbc8d0.jpg)?![](https://box.kancloud.cn/2016-01-18_569ca44dc8500.jpg)? 黑底、50%的透明度,箭頭用白色。? (PS:這可是我自己做的,別嫌棄![](https://box.kancloud.cn/2015-11-04_5639cee14c0a0.gif))? 圖片放到 Images.xcasserts 里:? ![](https://box.kancloud.cn/2016-01-18_569ca44dd8c43.jpg)? (再次PS:圖嫌小的話,就放在2x上)? 然后在 Storyboard 里添加兩個 Button:? ![](https://box.kancloud.cn/2016-01-18_569ca44deb6a3.jpg)? 注意里面的紅框,Button 與 Board 平級,并且在 Board 的上方。? Button 的約束如下: 1. 分別為左、右的 Button 設置 Undo、Redo 的 Image 2. 左邊的 Undo 按鈕離左 10px,頂部距離父視圖 74px 3. 右邊的 Redo 按鈕離右 10px,頂部與 Undo 相同 4. 不要設置寬、高約束,應與 Image 一致 兩個按鈕的點擊事件連接到 VC 里: ~~~ @IBAction func undo(sender: UIButton) { self.board.undo() } @IBAction func redo(sneder: UIButton) { self.board.redo() } ~~~ (此時的 Board 還沒有 undo/redo 方法,你可以自行添加或者稍后再添加)? 兩個按鈕本身也連接到 VC 里: ~~~ @IBOutlet var undoButton: UIButton! @IBOutlet var redoButton: UIButton! ~~~ 更新我們原`viewDidLoad`中的動畫方法,使兩個 Button 也適時的隱藏及顯示: ~~~ ... 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() self.undoButton.alpha = 0 // 新增 self.redoButton.alpha = 0 // 新增 } else if state == .Ended { UIView.setAnimationDelay(1.0) self.topViewConstraintY.constant = 0 self.toolbarConstraintBottom.constant = 0 self.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 = lastImage self.realImage = self.image } } ~~~ 然后在每次畫新圖的時候保存下當前狀態: ~~~ private func drawingImage() { if let brush = self.brush { // hook if 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.strokeWidth brush.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 = previewImage brush.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 功能了~? ![](https://box.kancloud.cn/2016-01-18_569ca44af292a.png) * * * ## 關于內存的使用 我們很快地就加上了 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`實現也很容易,只需要改兩個地方: 1. `PaintBrush`?協議,這個協議更新后,其所有的子類同步更新下即可 2. `Board`?的`drawingImage`方法實現 我在 GitHub 里 DrawingBoard 工程里提交了這個分支:? [DrawingBoard CGPath 分支](https://github.com/zhangao0086/DrawingBoard/tree/Feature/CGPath)? 協議和`drawingImage`進行了適當的更新,繪圖是以`CGPath`來實現的,但是依然采用的是圖片棧的方式,感興趣的同學可以嘗試自己實現。 * * * ### 2\. 優化圖片所占用的內存 除了用`CGPath`來優化以外,我們還可以直接優化圖片棧,用一個緩存或Undo控制器來控制所有的一切,在這個控制器里,將直接管理圖片緩存(內存和文件)、Undo、Redo操作,使 Board 的邏輯進一步的封裝。? 不得不說,這才是我想要實現的方式,模塊之間可以達到真正的解耦,我將?`Board`的代碼去掉沒有改動的方法和屬性后貼在這里: ~~~ class Board: UIImageView { // UndoManager,用于實現 Undo 操作和維護圖片棧的內存 private class DBUndoManager { class DBImageFault: UIImage {} // 一個 Fault 對象,與 Core Data 中的 Fault 設計類似 private static let INVALID_INDEX = -1 private var images = [UIImage]() // 圖片棧 private var index = INVALID_INDEX // 一個指針,指向 images 中的某一張圖 var canUndo: Bool { get { return index != DBUndoManager.INVALID_INDEX } } var canRedo: Bool { get { return index + 1 < images.count } } func addImage(image: UIImage) { // 當往這個 Manager 中增加圖片的時候,先把指針后面的圖片全部清掉, // 這與我們之前在 drawingImage 方法中對 redoImages 的處理是一樣的 if index < images.count - 1 { images[index + 1 ... images.count - 1] = [] } images.append(image) // 更新 index 的指向 index = images.count - 1 setNeedsCache() } func imageForUndo() -> UIImage? { if self.canUndo { --index if self.canUndo == false { return nil } else { setNeedsCache() return images[index] } } else { return nil } } func imageForRedo() -> UIImage? { var image: UIImage? = nil if self.canRedo { image = images[++index] } setNeedsCache() return image } // MARK: - Cache private static let cahcesLength = 3 // 在內存中保存圖片的張數,以 index 為中心點計算:cahcesLength * 2 + 1 private func setNeedsCache() { if images.count >= DBUndoManager.cahcesLength { let location = max(0, index - DBUndoManager.cahcesLength) let length = min(images.count - 1, index + DBUndoManager.cahcesLength) for i in location ... length { autoreleasepool { var image = images[i] if i > index - DBUndoManager.cahcesLength && i < index + DBUndoManager.cahcesLength { setRealImage(image, forIndex: i) // 如果在緩存區域中,則從文件加載 } else { setFaultImage(image, forIndex: i) // 如果不在緩存區域中,則置成 Fault 對象 } } } } } private static var basePath: String = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first as! String private func setFaultImage(image: UIImage, forIndex: Int) { if !image.isKindOfClass(DBImageFault.self) { let imagePath = DBUndoManager.basePath.stringByAppendingPathComponent("\(forIndex)") UIImagePNGRepresentation(image).writeToFile(imagePath, atomically: false) images[forIndex] = DBImageFault() } } private func setRealImage(image: UIImage, forIndex: Int) { if image.isKindOfClass(DBImageFault.self) { let imagePath = DBUndoManager.basePath.stringByAppendingPathComponent("\(forIndex)") images[forIndex] = UIImage(data: NSData(contentsOfFile: imagePath)!)! } } } private var boardUndoManager = DBUndoManager() // 緩存或Undo控制器 // MARK: - Public methods var canUndo: Bool { get { return self.boardUndoManager.canUndo } } var canRedo: Bool { get { return self.boardUndoManager.canRedo } } // undo 和 redo 的邏輯都有所簡化 func undo() { if self.canUndo == false { return } self.image = self.boardUndoManager.imageForUndo() self.realImage = self.image } func redo() { if self.canRedo == false { return } self.image = self.boardUndoManager.imageForRedo() self.realImage = self.image } // MARK: - drawing private func drawingImage() { if let brush = self.brush { // hook if 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.strokeWidth brush.drawInContext(context) CGContextStrokePath(context) let previewImage = UIGraphicsGetImageFromCurrentImageContext() if self.drawingState == .Ended || brush.supportedContinuousDrawing() { self.realImage = previewImage } UIGraphicsEndImageContext() // 用 Ended 事件代替原先的 Began 事件 if self.drawingState == .Ended { self.boardUndoManager.addImage(self.image!) } self.image = previewImage brush.lastPoint = brush.endPoint } } } ~~~ 以磁盤代替了內存,這里有一些關鍵點: 1. `Board`不需要在對?`self.image`?的取值進行邏輯判斷,`DBUndoManager`會在適當的時候返回nil,這無疑簡化了邏輯 2. 不需要維護兩個圖片棧:undoImages、redoImages,`drawingImage`方法不再需要在 Began 事件里做特殊處理,直接將剛畫完的圖“扔到” UndoManager 中即可 3. undo、redo 方便得到了簡化,適時調用`UndoManager`即可 4. 由于在?`UndoManager`中只有一個圖片棧,所以需要一個額外的指針來指向當前的狀態,當前指針的取值(index)對應下圖中的 i,兩邊的箭頭分別是 undo、redo 對應的圖以及索引:? ![](https://box.kancloud.cn/2016-01-18_569ca44e14bcd.jpg) 5. Fault 對象是一種不錯的設計模式,拿來做占位符挺合適的 6. `UndoManager`會在三種情況下:addImage、undo、redo 對圖片棧進行維護,使 images 里只有 index 兩邊的元素才真正加載 image到內存中,其他的元素用 Fault 對象代替 7. 我為什么要用一個 cahcesLength 變量?這里其實還有進一步優化的余地,如只在讀取到 Fault 對象時才更新圖片棧 那么效果如何呢?我在 4s、Plus 都有進行測試,由于 4s 性能相對較差,我以 4s 為主要測試對象,在內存較少的 4s 上: ![](https://box.kancloud.cn/2016-01-18_569ca44e21af1.jpg) 在反復繪圖的情況下,內存也是毫無壓力的~!那么讀寫文件的時候是否會有卡頓呢?在 4s 上我發現遠未達到瓶頸: ![](https://box.kancloud.cn/2016-01-18_569ca44e36afd.jpg) (PS:4s 的閃存是C10級別)? cahcesLength 變量配合 index 可以進一步優化性能,在這里就不多做介紹了。 至此,DrawingBoard 就可以告一段落了。 * * * # [GitHub](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>

                              哎呀哎呀视频在线观看