转载请注明出处:http://www.jb51.cc/article/p-htigfqxi-ber.html。
要做一个全功能的绘图板,至少要支持以下这些功能:
支持铅笔绘图(画点) 支持画直线 支持一些简单的图形(矩形、圆形等) 做一个真正的橡皮擦 能设置画笔的粗细 能设置画笔的颜色 能设置背景色或者背景图 能支持撤消与重做 …我们先做一些基础性的工作,比如创建工程。
先创建一个Single VIEw Application
工程:
语言选择Swift
:
为了最大程度的利用屏幕区域,我们完全隐藏掉状态栏,在Info.pList
里修改或添加这两个参数:
然后进入到Main.storyboard
,开始搭建我们的UI。
我们给已存在的VIEwController
的VIEw
添加一个UIImageVIEw
的子视图,背景色设为light Gray
,然后添加4个约束,由于要做一个全屏的画板,必须要让Constraint to margins
保持没有选中的状态,否则左右两边会留下苹果建议的空白区域,最后把User Interaction Enabled
打开:
然后我们回到VIEwController
的VIEw
上:
UIVIEw
,为该VIEw设置约束: 同样的不要选择
Contraint to margins
。 在该VIEw里添加一个UISegmentedControl
,并给SegmentedControl设置6个选项,分别是:
垂直居中,两边各留20,高度固定为28。
完整的UI及结构看起来像这样:
ImageVIEw将会作为实际的绘制区域,顶部的SegmentedControl提供工具的选择。 到目前为止我们还没有写下一行代码,至此要开始编码了。
施工… Board你可能会注意到Board有一部分被挡住了,这只是暂时的~
我们创建一个Board
类,继承自UIImageVIEw
,同时把这个类设置为Main.storyboard
中ImageVIEw
的Class,这样当app启动的时候就会自动创建一个Board的实例了。
增加两个属性以及初始化方法:
var strokeWIDth: CGfloatvar strokecolor: UIcoloroverrIDe 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
将会作为一个绘图的基类而存在,我们会在它的基础上创建一系列的子类,以达到d性的设计目的。为此,我们创建一个BaseBrush
类,并实现一个PaintBrush
接口:
import CoreGraphicsprotocol 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
声明了两个方法:
只要是实现了PaintBrush
接口的类,我们就当作是一个绘图工具(如铅笔、直尺等),而BaseBrush
除了实现PaintBrush
接口以外,我们还为它增加了四个便利属性:
这么一来,子类也可以很方便的获取到当前的状态,并作一些深度定制的绘图方法。
回到BoardlastPoint的意义:beginPoint和endPoint很好理解,beginPoint是手势刚识别时的点,只要手势不结束,那么beginPoint在手势识别期间是不会变的;endPoint总是表示手势最后识别的点;除了铅笔以外,其他的图形用这两个属性就够了,但是用铅笔在移动的时候,不能每次从beginPoint画到endPoint,如果是那样的话就是画直线了,而是应该从上一次画的位置(lastPoint)画到endPoint,这样才是连贯的线。
我们实现了一个画笔的基类之后,就可以重新回到Board
类了,毕竟我们之前的工作还没有做完,现在是时候完善Board
类了。
我们用Board
实际 *** 纵BaseBrush
,先为Board
添加两个新的属性:
var brush: BaseBrush?private var realimage: UIImage?
brush
对应到具体的画笔类,realimage
保存当前的图形,重新修改touches方法,以便增加对brush
属性的处理,完整的touches方法实现如下:
// MARK: - touches methodsoverrIDe 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 }}
步骤解析:
开启一个新的ImageContext,为保存每次的绘图状态作准备。 初始化context,进行基本设置(画笔宽度、画笔颜色、画笔的圆润度等)。 把之前保存的图片绘制进context中。 设置brush
的基本属性,以便子类更方便的绘图;调用具体的绘图方法,并最终添加到context中。 从当前的context中,得到Image,如果是ended
状态或者需要支持连续不断的绘图,则将Image保存到realimage
中。 实时显示当前的绘制状态,并记录绘制的最后一个点。 这些工作完成以后,我们就可以开始写第一个工具了:铅笔工具。
铅笔工具应该支持连续不断的绘图(不断的保存到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.y) } } overrIDe func supportedContinuousDrawing() -> Bool { return true }}
如果lastPoint为nil,则基于beginPoint画线,反之则基于lastPoint画线。
这样一来,一个铅笔工具就完成了,怎么样,很简单吧。
到目前为止,我们的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]
编译、运行,铅笔工具可以完美运行~!
接下来我们把其他的绘图工具也实现了。
其他的工具不像铅笔工具,不需要支持连续不断的绘图,所以也就不用覆盖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,lengths,2); CGContextMovetoPoint(context,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,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);
加入这一句代码,一个真正的橡皮擦便实现了。
再次测试现在我们的工程结构应该类似于这样:
我们修改下VIEwController
中的brushes
属性的初始值:
var brushes = [PencilBrush(),lineBrush(),DashlineBrush(),RectangleBrush(),EllipseBrush(),EraserBrush()]
编译、运行:
除了橡皮擦擦除的范围太小以外,一切都很完美~!
在继续完成剩下的功能之前,我想先对之前的代码进行些说明。
为什么不用drawRect方法其实我最开始也是使用drawRect方法来完成绘制,但是感觉限制很多,比如context无法保存,还是要每次重画(虽然可以保存到一个BitMapContext里,但是这样与保存到image里有什么区别呢?);后来用CALayer保存每一条CGPath,但是这样仍然不能避免每次重绘,因为需要考虑到橡皮擦和画笔属性之类的影响,这么一来还不如采用image的方式来保存最新绘图板。
既然定下了以image来保存绘图板,那么drawRect就不方便了,因为不能用UIGraphicsBeginImageContext
方法来创建一个ImageContext。
在VIEwController
、Board
和BaseBrush
这三者之间,虽然VC要知道另外两个组件,但是仅限于选择对应的工具给Board,Board本身并不知道当前的brush是哪个brush,也不需要知道其内部实现,只管调用对应的brush就行了;BaseBrush(及其子类)也并不知道自己将会被用于哪,它们只需要实现自己的算法即可。类似于这样的图:
实际上这里包含了两个设计模式。
策略设计模式
的UML图:
策略设计模式
在iOS中也应用广泛,如AFNetworking
的AFhttpRequestSerializer
和AFhttpResponseSerializer
的设计,通过在运行时动态的改变委托对象,变换行为,使程序模块之间解耦、提高应变能力。
以我们的绘图板为例,输出不同的图形就意味着不同的算法,用户可根据不同的需求来选择某一种算法,即BaseBrush及其子类做具体的封装,这样的好处是每一个子类只关心自己的算法,达到了高聚合的原则,高级模块(Board)不用关心具体实现。
想象一下,如果是让Board里自身来处理这些算法,那代码中无疑会充斥很多与算法选择相关的逻辑,而且每增加一个算法都需要重新修改Board类,这又与代码应该对拓展开放、对修改关闭原则有了冲突,而且每个类也应该只有一个责任。
通过采用策略模式我们实现了一个好维护、易拓展的程序(妈妈再也不用担心工具栏不够用了^^)。
策略模式的定义:定义一个算法群,把每一个算法分别封装起来,让它们之间可以互相替换,使算法的变化独立于使用它的用户之上。
模板方法在传统的策略模式中,每一个算法类都独自完成整个算法过程,例如一个网络解析程序,可能有一个算法用于解析JsON
,有另一个算法用于解析XML
等(另外一个例子是压缩程序,用ZIP
或RAR
算法),独自完成整个算法对灵活性更好,但免不了会有重复代码,在DrawingBoard
里我们做一个折中,尽量保证灵活性,又最大限度地避免重复代码。
我们将BaseBrush
的角色提升为算法的基类,并提供一些便利的属性(如beginPoint
、endPoint
、strokeWIDth
等),然后在Board
的drawingImage
方法里对BaseBrush
的接口进行调用,而BaseBrush
不会知道自己的接口是如何联系起来的,虽然supportedContinuousDrawing
(这是一个“钩子”)甚至影响了算法的流程(铅笔需要实时绘图)。
我们用drawingImage
搭建了一个算法的骨架,看起来像是模板方法的UML图:
图中右边的方框代表模板方法。
BaseBrush
通过提供抽象方法(drawInContext
)、具体方法或钩子方法(supportedContinuousDrawing
)来对应算法的每一个步骤,让其子类可以重定义或实现这些步骤。同时,让模板方法(即dawingImage
)定义一个算法的骨架,模板方法不仅可以调用在抽象类中实现的基本方法,也可以调用在抽象类的子类中实现的基本方法,还可以调用其他对象中的方法。
除了对算法的封装以外,模板方法还能防止“循环依赖”,即高层组件依赖低层组件,反过来低层组件也依赖高层组件。想像一下,如果既让Board选择具体的算法子类,又让算法类直接调用drawingImage方法(不提供钩子,直接把Board的事件下发下去),那到时候就热闹了,这些类串在一起难以理解,又不好维护。
模板方法的定义:在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
其实模式都很简单,很多人在工作中会思考如何让自己的代码变得更好,“情不自禁”地就会慢慢实现这些原则,了解模式的设计意图,有助于在遇到需要折中的地方更加明白如何在设计上取舍。
以上就是我设计时的思路,说完了,接下来还要完成的工作有:
提供对画笔颜色、粗细的设置 背景设置 全屏绘图(不能让Board一直显示不全)先从画笔开始,Let’s go!
画笔设置不管是画笔还是背景设置,我们都要有一个能提供设置的工具栏。
@H_419_1059@设置工具栏所以我们往Board
上再盖一个UIToolbar
,与顶部的VIEw类似:
UIToolbar
到Board
的父类上,与Board
的视图层级平级。 设置UIToolbar
的约束:左、右、下间距为0,高为44: 往
UIToolbar
上拖一个UIbarbuttonItem
,Title
就写:画笔设置。 在VIEwController
里增加一个paintingBrushSettings
方法,并把UIbarbuttonItem
的action
连接paintingBrushSettings
方法上。 在VIEwController
里增加一个toolar
属性,并把Xib中的UIToolbar
连接到toolbar
上。 UIToolbar配置好后,UI及视图层级如下:
考虑到多个页面需要选取自定义的颜色,我们先创建一个工具类: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
回调给外部。它能展现一个这样的视图:
不过虽然该工具类本身没有采用auto Layout进行布局,但是它还是支持auto Layout的,当它被添加到某个auto Layout的视图中的时候,auto Layout布局系统可以通过intrinsicContentSize
知道该视图的尺寸信息。
最后它还有一个setCurrentcolor
方法从外部接收一个UIcolor,可以用于初始化。
我打算在用户点击画笔设置
的时候,从底部d出一个控制面板(就像系统的Control Center
那样),所以我们还要有一个像这样的设置UI:
具体的,创建一个PaintingBrushSettingsVIEw
类,同时创建一个PaintingBrushSettingsVIEw.xib
文件,并把xib中vIEw的Class
设为PaintingBrushSettingsVIEw
,设置vIEw的背景色为透明:
UILabel
,约束设为:宽度固定为68,高度固定为21,左和上边距为8。 放置一个Title为“1”的UILabel
,“1”与“画笔粗细”的垂直间距为10,宽度固定为10,高度固定为21,与supervIEw
的左边距为10。 放置一个UiSlider
,用于调节画笔的粗细,与“1”的水平间距为5,并与“1”垂直居中,高度固定为30,宽度暂时不设,在PaintingBrushSettingsVIEw
中添加strokeWIDthSlIDer
属性,与之连接起来。 放置一个Title为“20”的UILabel
,约束设为:宽度固定为20,高度固定为21,top与“1”相同,与supervIEw
的右间距为10。并把上一步中的UiSlider
的右间距设为与“20”相隔5。 放置一个Title为“画笔颜色”的UILabel
,宽、高、left与“画笔粗细”相同,与上面UiSlider
的垂直间距设为12。 放置一个UIVIEw
至“画笔颜色”下方(上图中被选中的那个UIVIEw),宽度固定为50,高度固定为30,left与“画笔颜色”相同,并且与“画笔颜色”的垂直间距为5,在PaintingBrushSettingsVIEw
中添加strokecolorPrevIEw
属性,与之连接起来。 放置一个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里,不能用 setBackgroundcolor
方法了,具体的,见Xcode 6.3的发布文档:Xcode 6.3 Release Notes,下面是用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到了这一点,毛玻璃效果就算实现了~~
我们在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)]",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.itemsself.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}
这么一来画笔设置就做完了,代码应该还是比较好理解,编译、运行后,应该能看到:
完成度已经很高了^^!
整体的框架基本上已经在之前的工作中搭好了,我们快速过掉这一节。
在Main.storyboard
中增加了一个Title为“背景设置”的UIbarbuttonItem
,并将action连接到VIEwController
的backgroundSettings
方法上,你可以选择在插入“背景设置”之前,先插入一个FlexibleSpace
的UIbarbuttonItem
。
创建BackgroundSettingsVC
类,继承自UIVIEwController
,这与画笔设置继承于UIVIEw
不同,我们希望背景设置可以在用户的相册中选择照片,而使用UIImagePickerController
的前提是要实现UIImagePickerControllerDelegate
、UINavigationControllerDelegate
两个接口,如果让UIVIEw来实现这两个接口会很奇怪。
创建一个BackgroundSettingsVC.xib
文件:
touch Up InsIDe
事件连接到BackgroundSettingsVC
的pickImage
方法上;RGBcolorPicker连接到BackgroundSettingsVC
的colorPicker
属性上。 看上去像这样:
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",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,action: "endSetting")]self.toolbaritems = self.toolbar.itemsself.setupBrushSettingsVIEw()self.setupBackgroundSettingsVIEw() // Added~!!!
实现backgroundSettings
方法:
@IBAction func backgroundSettings() { self.currentSettingsVIEw = self.toolbar.vIEwWithTag(2) self.currentSettingsVIEw?.hIDden = false self.updatetoolbarForSettingsVIEw()}
编译、运行,现在你可以用不同的背景色(或背景图)了!
到目前为止,Board
一直显示不全(事实上,我很早就实现了全屏绘图,但是优先级一直被我排在最后),现在是时候来解决它了。
解决思路是这样的:当用户开始绘图的时候,我们把顶部和底部两个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: "保存成功",cancelbuttonTitle: "确定").show() }}
旅行到终点了~!
看了下,有些小长,文本+代码有2w3+,全部代码去除空行和空格有1w4+,直接贴代码会简单很多,但我始终觉得让代码完成功能并不是全部目的,代码背后隐藏的问题定义、设计、构建更有意义,毕竟软件开发完成“后”比完成“前”所花费的时间永远更多(除非是一个只有10行代码或者“一次性”的程序)。
希望与大家多多交流。
更新——撤消与重做功能最后吐槽下CSDN新的Markdown编辑器,代码样式丑且不能自定义,而且有些代码高亮都无法识别。不过感觉草稿箱比以前更方便,问题主要还是集中在样式上,希望以后能不断改进,会一如既往的支持。
Swift 绘图板功能完善以及终极优化
@H_419_2321@GitHub地址DrawingBoard
总结以上是内存溢出为你收集整理的Swift 全功能的绘图板开发全部内容,希望文章能够帮你解决Swift 全功能的绘图板开发所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)