Swift 绘图板功能完善以及终极优化
轉(zhuǎn)載請(qǐng)注明出處:http://blog.csdn.net/zhangao0086/article/details/45289475。
前文總結(jié)
接著這篇:Swift 全功能的繪圖板開(kāi)發(fā),雖然在上一篇中我們已經(jīng)完成了這些功能:
- 支持鉛筆繪圖(畫(huà)點(diǎn))
- 支持畫(huà)直線(xiàn)
- 支持一些簡(jiǎn)單的圖形(矩形、圓形等)
- 做一個(gè)真正的橡皮擦
- 能設(shè)置畫(huà)筆的粗細(xì)
- 能設(shè)置畫(huà)筆的顏色
- 能設(shè)置背景色或者背景圖
但是還有一個(gè)非常重要的功能沒(méi)有實(shí)現(xiàn),沒(méi)錯(cuò),那就是 Undo/Redo!我之所以把這個(gè)功能單獨(dú)放出來(lái)是有原因的,一是因?yàn)樯弦黄呀?jīng)篇幅太長(zhǎng),不適合繼續(xù)往上加內(nèi)容;二是因?yàn)闉榱藢?shí)現(xiàn) Undo/Redo 功能,我們需要對(duì) DrawingBoard 進(jìn)行一些重構(gòu),在這篇文章中,你能看到用另一種方式實(shí)現(xiàn)的繪圖板。
實(shí)現(xiàn)的效果:
更新 ViewController
先添加兩張按鈕圖:
黑底、50%的透明度,箭頭用白色。
(PS:這可是我自己做的,別嫌棄)
圖片放到 Images.xcasserts 里:
(再次PS:圖嫌小的話(huà),就放在2x上)
然后在 Storyboard 里添加兩個(gè) Button:
注意里面的紅框,Button 與 Board 平級(jí),并且在 Board 的上方。
Button 的約束如下:
兩個(gè)按鈕的點(diǎn)擊事件連接到 VC 里:
@IBAction func undo(sender: UIButton) {self.board.undo() }@IBAction func redo(sneder: UIButton) {self.board.redo() }(此時(shí)的 Board 還沒(méi)有 undo/redo 方法,你可以自行添加或者稍后再添加)
兩個(gè)按鈕本身也連接到 VC 里:
更新我們?cè)璿iewDidLoad中的動(dòng)畫(huà)方法,使兩個(gè) Button 也適時(shí)的隱藏及顯示:
... 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 的每一張圖,當(dāng) Undo/Redo 的時(shí)候直接把前一個(gè)狀態(tài)取出并顯示,為了分別存儲(chǔ) Undo/Redo 操作所用的圖片,我們要建立兩個(gè)圖片棧:
private var undoImages = [UIImage]() private var redoImages = [UIImage]()然后加兩個(gè)工具方法: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 這兩個(gè)主要方法:
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} }然后在每次畫(huà)新圖的時(shí)候保存下當(dāng)前狀態(tài):
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} }這里面都有對(duì) self.image 進(jìn)行非空處理,其實(shí)原來(lái)不用這么麻煩,如果Swift 的數(shù)組支持插入Optional類(lèi)型的話(huà),我們直接把self.image插入到數(shù)組中,用的時(shí)候再取出來(lái)即可,因?yàn)?UIImageView 的 UIImage 是 Optional 類(lèi)型的,賦一個(gè) nil 給它沒(méi)有問(wèn)題,就當(dāng)是 undo 到初始化狀態(tài)了,但是偏偏 Swift的數(shù)組不支持插入Optional類(lèi)型,這就導(dǎo)致我們不能記住 UIImageView 的初始化狀態(tài),只能通過(guò)判斷它的image是否為 nil 來(lái)處理。
完成的邏輯很簡(jiǎn)單:當(dāng)畫(huà)圖開(kāi)始的時(shí)候,保存當(dāng)前 image 到 undo 棧中,并清空 redo 棧,進(jìn)行 undo 操作的時(shí)候,能一直 undo,并將 undo 的 image 存進(jìn) redo 棧中,直到 self.image 為 nil。從這個(gè)邏輯可以看出兩點(diǎn):redo 功能非常依賴(lài) undo,畢竟沒(méi)有撤消就沒(méi)有重做;除此之外,當(dāng)用戶(hù)開(kāi)始繪制新圖的時(shí)候,我們也要清空 redo 棧,因?yàn)橛脩?hù)已經(jīng)“回不去”了。
完成這些工作后,就能測(cè)試 Undo/Redo 功能了~
關(guān)于內(nèi)存的使用
我們很快地就加上了 Undo/Redo 功能,是吧? 通過(guò)維護(hù)兩個(gè)圖片棧,在進(jìn)行相應(yīng)的操作的時(shí)候,直接對(duì) self.image 進(jìn)行賦值,但是這么做有一個(gè)很明顯的弊端,就是內(nèi)存使用毫無(wú)上限! 你可以很輕松地在 5s 上使內(nèi)存使用達(dá)到 50M 甚至 100M,雖然我們做了一些處理,如當(dāng)用戶(hù)繪制新圖時(shí),清空 Redo 的圖片棧,但是這并不能從根本上解決問(wèn)題。
要從根本上解決問(wèn)題有兩種方式。
1. 用 CGPath 畫(huà)圖
假設(shè)換一種實(shí)現(xiàn)方式,不緩存圖片,而是保存每一步,這樣無(wú)疑會(huì)使內(nèi)存使用量降低很多,取而代之的是在每次畫(huà)圖的時(shí)候需要有一個(gè)循環(huán)來(lái)重新畫(huà)每一步(可以嘗試用 clearsContextBeforeDrawing 屬性來(lái)優(yōu)化),我個(gè)人覺(jué)得這種方式可能會(huì)比較惡心,因?yàn)楫?huà)的越多,性能就越差,我在前一篇里說(shuō)過(guò)【為什么不用drawRect方法】:
為什么不用drawRect方法
其實(shí)我最開(kāi)始也是使用drawRect方法來(lái)完成繪制,但是感覺(jué)限制很多,比如context無(wú)法保存,還是要每次重畫(huà)(雖然可以保存到一個(gè)BitMapContext里,但是這樣與保存到image里有什么區(qū)別呢?);后來(lái)用CALayer保存每一條CGPath,但是這樣仍然不能避免每次重繪,因?yàn)樾枰紤]到橡皮擦和畫(huà)筆屬性之類(lèi)的影響,這么一來(lái)還不如采用image的方式來(lái)保存最新繪圖板。
既然定下了以image來(lái)保存繪圖板,那么drawRect就不方便了,因?yàn)椴荒苡肬IGraphicsBeginImageContext方法來(lái)創(chuàng)建一個(gè)ImageContext。
如果決定要用 CGPath 來(lái)畫(huà)圖的話(huà),你除了要暴露一個(gè)CGPath和CGContext以外,你還需要用一個(gè)自定義的對(duì)象保存當(dāng)前的繪圖狀態(tài),如畫(huà)筆顏色、畫(huà)筆粗細(xì)、混合模式(Blend Mode)等(還會(huì)在后期遇到由于前期考慮不足的屬性沒(méi)有設(shè)置,然后才加上,這就破壞了“封閉-開(kāi)放原則”),然后在每一個(gè)循環(huán)體中恢復(fù)當(dāng)前的上下文,類(lèi)似于這樣:
CGContextSaveGState... for path in paths {CGContextSetLineCap(context, kCGLineCapRound)CGContextSetLineWidth(context, self.strokeWidth)CGContextSetStrokeColorWithColor(context, self.strokeColor.CGColor)/* Add path and drawing... */CGContextRestoreGState... }從代碼上來(lái)說(shuō),想換成用CGPath實(shí)現(xiàn)也很容易,只需要改兩個(gè)地方:
我在 GitHub 里 DrawingBoard 工程里提交了這個(gè)分支:
DrawingBoard CGPath 分支
協(xié)議和drawingImage進(jìn)行了適當(dāng)?shù)母?#xff0c;繪圖是以CGPath來(lái)實(shí)現(xiàn)的,但是依然采用的是圖片棧的方式,感興趣的同學(xué)可以嘗試自己實(shí)現(xiàn)。
2. 優(yōu)化圖片所占用的內(nèi)存
除了用CGPath來(lái)優(yōu)化以外,我們還可以直接優(yōu)化圖片棧,用一個(gè)緩存或Undo控制器來(lái)控制所有的一切,在這個(gè)控制器里,將直接管理圖片緩存(內(nèi)存和文件)、Undo、Redo操作,使 Board 的邏輯進(jìn)一步的封裝。
不得不說(shuō),這才是我想要實(shí)現(xiàn)的方式,模塊之間可以達(dá)到真正的解耦,我將 Board的代碼去掉沒(méi)有改動(dòng)的方法和屬性后貼在這里:
以磁盤(pán)代替了內(nèi)存,這里有一些關(guān)鍵點(diǎn):
那么效果如何呢?我在 4s、Plus 都有進(jìn)行測(cè)試,由于 4s 性能相對(duì)較差,我以 4s 為主要測(cè)試對(duì)象,在內(nèi)存較少的 4s 上:
在反復(fù)繪圖的情況下,內(nèi)存也是毫無(wú)壓力的~!那么讀寫(xiě)文件的時(shí)候是否會(huì)有卡頓呢?在 4s 上我發(fā)現(xiàn)遠(yuǎn)未達(dá)到瓶頸:
(PS:4s 的閃存是C10級(jí)別)
cahcesLength 變量配合 index 可以進(jìn)一步優(yōu)化性能,在這里就不多做介紹了。
至此,DrawingBoard 就可以告一段落了。
GitHub
總結(jié)
以上是生活随笔為你收集整理的Swift 绘图板功能完善以及终极优化的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 十六款值得关注的NoSQL与NewSQL
- 下一篇: Windows下配置Java开发环境