Swift 绘图板功能完善以及终极优化
转载请注明出处:http://www.52php.cn/article/p-yoqsxumy-ber.html。 前文总结接着这篇:Swift 全功能的绘图板开发,虽然在上一篇中我们已经完成了这些功能:
但是还有一个非常重要的功能没有实现,没错,那就是 Undo/Redo!我之所以把这个功能单独放出来是有原因的,一是因为上一篇已经篇幅太长,不适合继续往上加内容;二是因为为了实现 Undo/Redo 功能,我们需要对 DrawingBoard 进行一些重构,在这篇文章中,你能看到用另一种方式实现的绘图板。 实现的效果: 更新 ViewController先添加两张按钮图:
两个按钮的点击事件连接到 VC 里: @IBAction func undo(sender: UIButton) {
self.board.undo()
}
@IBAction func redo(sneder: UIButton) {
self.board.redo()
}
(此时的 Board 还没有 undo/redo 方法,你可以自行添加或者稍后再添加) @IBOutlet var undoButton: UIButton!
@IBOutlet var redoButton: UIButton!
更新我们原 ...
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()
}
}
...
更新 BoardUndo/Redo 真正的逻辑都在 private var undoImages = [UIImage]()
private var redoImages = [UIImage]()
然后加两个工具方法: 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
}
}
这里面都有对 完成的逻辑很简单:当画图开始的时候,保存当前 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 画图假设换一种实现方式,不缓存图片,而是保存每一步,这样无疑会使内存使用量降低很多,取而代之的是在每次画图的时候需要有一个循环来重新画每一步(可以尝试用
如果决定要用 CGContextSaveGState...
for path in paths {
CGContextSetLineCap(context,kCGLineCapRound)
CGContextSetLineWidth(context,self.strokeWidth)
CGContextSetStrokeColorWithColor(context,self.strokeColor.CGColor)
/* Add path and drawing... */
CGContextRestoreGState...
}
从代码上来说,想换成用
我在 GitHub 里 DrawingBoard 工程里提交了这个分支: 2. 优化图片所占用的内存除了用 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
}
}
}
以磁盘代替了内存,这里有一些关键点:
那么效果如何呢?我在 4s、Plus 都有进行测试,由于 4s 性能相对较差,我以 4s 为主要测试对象,在内存较少的 4s 上: 在反复绘图的情况下,内存也是毫无压力的~!那么读写文件的时候是否会有卡顿呢?在 4s 上我发现远未达到瓶颈: (PS:4s 的闪存是C10级别) 至此,DrawingBoard 就可以告一段落了。 GitHub(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |