Swift全功能的绘图板开发
2015-11-26 17:10:14 | 来源:玩转帮会 | 投稿:佚名 | 编辑:小柯

原标题:Swift全功能的绘图板开发

要做一个全功能的绘图板,至少要支持以下这些功能:

  • 支持铅笔绘图(画点)

  • 支持画直线

  • 支持一些简单的图形(矩形、圆形等)

  • 做一个真正的橡皮擦

  • 能设置画笔的粗细

  • 能设置画笔的颜色

  • 能设置背景色或者背景图

  • 能支持撤消与重做

我们先做一些基础性的工作,比如创建工程。

工程搭建

先创建一个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个选项,分别是:

  1. 铅笔

  2. 直尺

  3. 虚线

  4. 矩形

  5. 圆形

  6. 橡皮擦

  • 给这个SegmentedControl添加约束:

垂直居中,两边各留20,高度固定为28。

完整的UI及结构看起来像这样:

ImageView将会作为实际的绘制区域,顶部的SegmentedControl提供工具的选择。 到目前为止我们还没有写下一行代码,至此要开始编码了。

你可能会注意到Board有一部分被挡住了,这只是暂时的~

施工…

Board

我们创建一个Board类,继承自UIImageView,同时把这个类设置为Main.storyboard中ImageView的Class,这样当app启动的时候就会自动创建一个Board的实例了。

增加两个属性以及初始化方法:

varstrokeWidth:CGFloat
varstrokeColor:UIColor
overrideinit(){
self.strokeColor=UIColor.blackColor()
self.strokeWidth=1
super.init()
}
requiredinit(coderaDecoder:NSCoder){
self.strokeColor=UIColor.blackColor()
self.strokeWidth=1
super.init(coder:aDecoder)
}

由于我们是依赖于touches方法来完成绘图过程,我们需要记录下每次touch的状态,比如began、moved、ended等,为此我们创建一个枚举,在touches方法中进行记录,并调用私有的绘图方法drawingImage:

enumDrawingState{
caseBegan,Moved,Ended
}
classBoard:UIImageView{
privatevardrawingState:DrawingState!
//此处省略init方法与另外两个属性
//MARK:-touchesmethods
overridefunctouchesBegan(touches:NSSet,withEventevent:UIEvent){
self.drawingState=.Began
self.drawingImage()
}
overridefunctouchesMoved(touches:NSSet,withEventevent:UIEvent){
self.drawingState=.Moved
self.drawingImage()
}
overridefunctouchesEnded(touches:NSSet,withEventevent:UIEvent){
self.drawingState=.Ended
self.drawingImage()
}
//MARK:-drawing
privatefuncdrawingImage(){
//暂时为空实现
}
}

在我们实现drawingImage方法之前,我们先创建另外一个重要的组件:BaseBrush。

BaseBrush

顾名思义,BaseBrush将会作为一个绘图的基类而存在,我们会在它的基础上创建一系列的子类,以达到弹性的设计目的。为此,我们创建一个BaseBrush类,并实现一个PaintBrush接口:

importCoreGraphics
protocolPaintBrush{
funcsupportedContinuousDrawing()->Bool;
funcdrawInContext(context:CGContextRef)
}
classBaseBrush:NSObject,PaintBrush{
varbeginPoint:CGPoint!
varendPoint:CGPoint!
varlastPoint:CGPoint?
varstrokeWidth:CGFloat!
funcsupportedContinuousDrawing()->Bool{
returnfalse
}
funcdrawInContext(context:CGContextRef){
assert(false,"mustimplementsinsubclass.")
}
}

BaseBrush实现了PaintBrush接口,PaintBrush声明了两个方法:

  • supportedContinuousDrawing,表示是否是连续不断的绘图

  • drawInContext,基于Context的绘图方法,子类必须实现具体的绘图

只要是实现了PaintBrush接口的类,我们就当作是一个绘图工具(如铅笔、直尺等),而BaseBrush除了实现PaintBrush接口以外,我们还为它增加了四个便利属性:

  • beginPoint,开始点的位置

  • endPoint,结束点的位置

  • lastPoint,最后一个点的位置(也可以称作是上一个点的位置)

  • strokeWidth,画笔的宽度

这么一来,子类也可以很方便的获取到当前的状态,并作一些深度定制的绘图方法。

lastPoint的意义:beginPoint和endPoint很好理解,beginPoint是手势刚识别时的点,只要手势不结束,那么beginPoint在手势识别期间是不会变的;endPoint总是表示手势最后识别的点;除了铅笔以外,其他的图形用这两个属性就够了,但是用铅笔在移动的时候,不能每次从beginPoint画到endPoint,如果是那样的话就是画直线了,而是应该从上一次画的位置(lastPoint)画到endPoint,这样才是连贯的线。

回到Board

我们实现了一个画笔的基类之后,就可以重新回到Board类了,毕竟我们之前的工作还没有做完,现在是时候完善Board类了。

我们用Board实际操纵BaseBrush,先为Board添加两个新的属性:

varbrush:BaseBrush?
privatevarrealImage:UIImage?

brush对应到具体的画笔类,realImage保存当前的图形,重新修改touches方法,以便增加对brush属性的处理,完整的touches方法实现如下:

//MARK:-touchesmethods
overridefunctouchesBegan(touches:NSSet,withEventevent:UIEvent){
ifletbrush=self.brush{
brush.lastPoint=nil
brush.beginPoint=touches.anyObject()!.locationInView(self)
brush.endPoint=brush.beginPoint
self.drawingState=.Began
self.drawingImage()
}
}
overridefunctouchesMoved(touches:NSSet,withEventevent:UIEvent){
ifletbrush=self.brush{
brush.endPoint=touches.anyObject()!.locationInView(self)
self.drawingState=.Moved
self.drawingImage()
}
}
overridefunctouchesCancelled(touches:NSSet!,withEventevent:UIEvent!){
ifletbrush=self.brush{
brush.endPoint=nil
}
}
overridefunctouchesEnded(touches:NSSet,withEventevent:UIEvent){
ifletbrush=self.brush{
brush.endPoint=touches.anyObject()!.locationInView(self)
self.drawingState=.Ended
self.drawingImage()
}
}

我们需要防止brush为nil的情况,以及为brush设置好beginPoint和endPoint,之后我们就可以完善drawingImage方法了,实现如下:

privatefuncdrawingImage(){
ifletbrush=self.brush{
//1.
UIGraphicsBeginImageContext(self.bounds.size)
//2.
letcontext=UIGraphicsGetCurrentContext()
UIColor.clearColor().setFill()
UIRectFill(self.bounds)
CGContextSetLineCap(context,kCGLineCapRound)
CGContextSetLineWidth(context,self.strokeWidth)
CGContextSetStrokeColorWithColor(context,self.strokeColor.CGColor)
//3.
ifletrealImage=self.realImage{
realImage.drawInRect(self.bounds)
}
//4.
brush.strokeWidth=self.strokeWidth
brush.drawInContext(context);
CGContextStrokePath(context)
//5.
letpreviewImage=UIGraphicsGetImageFromCurrentImageContext()
ifself.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类,实现如下:

classPencilBrush:BaseBrush{
overridefuncdrawInContext(context:CGContextRef){
ifletlastPoint=self.lastPoint{
CGContextMoveToPoint(context,lastPoint.x,lastPoint.y)
CGContextAddLineToPoint(context,endPoint.x,endPoint.y)
}else{
CGContextMoveToPoint(context,beginPoint.x,beginPoint.y)
CGContextAddLineToPoint(context,endPoint.x,endPoint.y)
}
}
overridefuncsupportedContinuousDrawing()->Bool{
returntrue
}
}

如果lastPoint为nil,则基于beginPoint画线,反之则基于lastPoint画线。

这样一来,一个铅笔工具就完成了,怎么样,很简单吧。

测试

到目前为止,我们的ViewController还保持着默认的状态,是时候先为铅笔工具写一些测试代码了。

在ViewController添加board属性,并与Main.storyboard中的Board关联起来;创建一个brushes属性,并为之赋值为:

varbrushes=[PencilBrush()]

在ViewController中添加switchBrush:方法,并把Main.storyboard中的SegmentedControl的ValueChanged连接到ViewController的switchBrush:方法上,实现如下:

@IBActionfuncswitchBrush(sender:UISegmentedControl){
assert(sender.tag<self.brushes.count,"!!!")
self.board.brush=self.brushes[sender.selectedSegmentIndex]
}

最后在viewDidLoad方法中做一个初始化:

self.board.brush = brushes[0]

编译、运行,铅笔工具可以完美运行~!

其他的工具

接下来我们把其他的绘图工具也实现了。

其他的工具不像铅笔工具,不需要支持连续不断的绘图,所以也就不用覆盖supportedContinuousDrawing方法了。

直尺

创建一个LineBrush类,实现如下:

classLineBrush:BaseBrush{
overridefuncdrawInContext(context:CGContextRef){
CGContextMoveToPoint(context,beginPoint.x,beginPoint.y)
CGContextAddLineToPoint(context,endPoint.x,endPoint.y)
}
}

虚线

创建一个DashLineBrush类,实现如下:

classDashLineBrush:BaseBrush{
overridefuncdrawInContext(context:CGContextRef){
letlengths:[CGFloat]=[self.strokeWidth*3,self.strokeWidth*3]
CGContextSetLineDash(context,0,lengths,2);
CGContextMoveToPoint(context,beginPoint.x,beginPoint.y)
CGContextAddLineToPoint(context,endPoint.x,endPoint.y)
}
}

这里我们就用到了BaseBrush的strokeWidth属性,因为我们想要创建一条动态的虚线。

矩形

创建一个RectangleBrush类,实现如下:

classRectangleBrush:BaseBrush{
overridefuncdrawInContext(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类,实现如下:

classEllipseBrush:BaseBrush{
overridefuncdrawInContext(context:CGContextRef){
CGContextAddEllipseInRect(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))))
}
}

同样有一些计算,理由同上。

橡皮擦

从本文一开始就说过了,我们要做一个真正的橡皮擦,网上有很多的橡皮擦的实现其实就是把画笔颜色设置为背景色,但是如果背景色可以动态设置,甚至设置为一个渐变的图片时,这种方法就失效了,所以有些绘图app的背景色就是固定为白色的。

其实Apple的Quartz2D框架本身就是支持橡皮擦的,只用一个方法就可以完美实现。

让我们创建一个EraserBrush类,实现如下:

classEraserBrush:PencilBrush{
overridefuncdrawInContext(context:CGContextRef){
CGContextSetBlendMode(context,kCGBlendModeClear);
super.drawInContext(context)
}
}

注意,与其他的工具不同,橡皮擦是继承自PencilBrush的,因为橡皮擦本身也是基于点的,而drawInContext里也只是加了一句:

CGContextSetBlendMode(context,kCGBlendModeClear);

加入这一句代码,一个真正的橡皮擦便实现了。

再次测试

现在我们的工程结构应该类似于这样:

我们修改下ViewController中的brushes属性的初始值:

varbrushes=[PencilBrush(),LineBrush(),DashLineBrush(),RectangleBrush(),EllipseBrush(),EraserBrush()]

编译、运行:

除了橡皮擦擦除的范围太小以外,一切都很完美~!

设计思路

在继续完成剩下的功能之前,我想先对之前的代码进行些说明。

为什么不用drawRect方法

其实我最开始也是使用drawRect方法来完成绘制,但是感觉限制很多,比如context无法保存,还是要每次重画(虽然可以保存到一个BitMapContext里,但是这样与保存到image里有什么区别呢?);后来用CALayer保存每一条CGPath,但是这样仍然不能避免每次重绘,因为需要考虑到橡皮擦和画笔属性之类的影响,这么一来还不如采用image的方式来保存最新绘图板。

既然定下了以image来保存绘图板,那么drawRect就不方便了,因为不能用UIGraphicsBeginImageContext方法来创建一个ImageContext。

ViewController与Board、BaseBrush之间的关系

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

画笔设置

不管是画笔还是背景设置,我们都要有一个能提供设置的工具栏。

设置工具栏

所以我们往Board上再盖一个UIToolbar,与顶部的View类似:

  1. 拖一个UIToolbar到Board的父类上,与Board的视图层级平级。

  2. 设置UIToolbar的约束:左、右、下间距为0,高为44:

  3. 往UIToolbar上拖一个UIBarButtonItem,title就写:画笔设置。

  4. 在ViewController里增加一个paintingBrushSettings方法,并把UIBarButtonItem的action连接paintingBrushSettings方法上。

  5. 在ViewController里增加一个toolar属性,并把Xib中的UIToolbar连接到toolbar上。

UIToolbar配置好后,UI及视图层级如下:

RGBColorPicker

考虑到多个页面需要选取自定义的颜色,我们先创建一个工具类:RGBColorPicker,用于选择RGB颜色:

这个工具类很简单,没有采用Auto Layout进行布局,因为layoutSubviews方法已经能很好的满足我们的需求了。当用户拖动任何一个UISlider的时候,我们能实时的通过colorChangedBlock回调给外部。它能展现一个这样的视图:

不过虽然该工具类本身没有采用Auto Layout进行布局,但是它还是支持Auto Layout的,当它被添加到某个Auto Layout的视图中的时候,Auto Layout布局系统可以通过intrinsicContentSize知道该视图的尺寸信息。

最后它还有一个setCurrentColor方法从外部接收一个UIColor,可以用于初始化。

画笔设置的UI

我打算在用户点击画笔设置的时候,从底部弹出一个控制面板(就像系统的Control Center那样),所以我们还要有一个像这样的设置UI:

具体的,创建一个PaintingBrushSettingsView类,同时创建一个PaintingBrushSettingsView.xib文件,并把xib中view的Class设为PaintingBrushSettingsView,设置view的背景色为透明:

  1. 放置一个title为“画笔粗细”的UILabel,约束设为:宽度固定为68,高度固定为21,左和上边距为8。

  2. 放置一个title为“1”的UILabel,“1”与“画笔粗细”的垂直间距为10,宽度固定为10,高度固定为21,与superview的左边距为10。

  3. 放置一个UISlider,用于调节画笔的粗细,与“1”的水平间距为5,并与“1”垂直居中,高度固定为30,宽度暂时不设,在PaintingBrushSettingsView中添加strokeWidthSlider属性,与之连接起来。

  4. 放置一个title为“20”的UILabel,约束设为:宽度固定为20,高度固定为21,top与“1”相同,与superview的右间距为10。并把上一步中的UISlider的右间距设为与“20”相隔5。

  5. 放置一个title为“画笔颜色”的UILabel,宽、高、left与“画笔粗细”相同,与上面UISlider的垂直间距设为12。

  6. 放置一个UIView至“画笔颜色”下方(上图中被选中的那个UIView),宽度固定为50,高度固定为30,left与“画笔颜色”相同,并且与“画笔颜色”的垂直间距为5,在PaintingBrushSettingsView中添加strokeColorPreview属性,与之连接起来。

  7. 放置一个UIView,把它的Class改为RGBColorPicker,约束设为:left与顶部的UISlider相同,底部与superview的间距为0,右间距为10,与上一步中的UIView的垂直间距为5。

PaintingBrushSettingsView类的完整代码如下:

classPaintingBrushSettingsView:UIView{
varstrokeWidthChangedBlock:((strokeWidth:CGFloat)->Void)?
varstrokeColorChangedBlock:((strokeColor:UIColor)->Void)?
@IBOutletprivatevarstrokeWidthSlider:UISlider!
@IBOutletprivatevarstrokeColorPreview:UIView!
@IBOutletprivatevarcolorPicker:RGBColorPicker!
overridefuncawakeFromNib(){
super.awakeFromNib()
self.strokeColorPreview.layer.borderColor=UIColor.blackColor().CGColor
self.strokeColorPreview.layer.borderWidth=1
self.colorPicker.colorChangedBlock={
[unownedself](color:UIColor)in
self.strokeColorPreview.backgroundColor=color
ifletstrokeColorChangedBlock=self.strokeColorChangedBlock{
strokeColorChangedBlock(strokeColor:color)
}
}
self.strokeWidthSlider.addTarget(self,action:"strokeWidthChanged:",forControlEvents:.ValueChanged)
}
funcsetBackgroundColor(color:UIColor){
self.strokeColorPreview.backgroundColor=color
self.colorPicker.setCurrentColor(color)
}
funcstrokeWidthChanged(slider:UISlider){
ifletstrokeWidthChangedBlock=self.strokeWidthChangedBlock{
strokeWidthChangedBlock(strokeWidth:CGFloat(slider.value))
}
}
}

strokeWidthChangedBlock和strokeColorChangedBlock两个Block用于给外部传递状态。setBackgroundColor用于初始化。

关于 Swift 1.2

在 Swift 1.2里,不能用 setBackgroundColor方法了,具体的,见Xcode 6.3的发布文档:Xcode 6.3 Release Notes,下面是用didSet代替原有的setBackgroundColor方法:

overridevarbackgroundColor: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新增加几个属性:

vartoolbarEditingItems:[UIBarButtonItem]?
varcurrentSettingsView:UIView?
@IBOutletvartoolbarConstraintHeight:NSLayoutConstraint!

toolbarConstraintHeight连接到Main.storyboard中对应的约束上就行了。toolbarEditingItems能让我们在UIToolbar上显示不同的items,本来还需要一个toolbarItems属性的,因为UIViewController类本身就自带,我们便不用单独新增。currentSettingsView是用来保存当前展示的哪个设置页面,考虑到我们后面会增加背景设置,这个属性还是有必要的。

我们先写一个往toolbar上添加约束的工具方法:

funcaddConstraintsToToolbarForSettingsView(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)]",
options:.DirectionLeadingToTrailing,
metrics:["height":view.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height],
views:["settingsView":view]))
}

这个工具方法会把传入进来的view添加到toolbar上,同时添加相应的约束。注意高度的约束,我是通过systemLayoutSizeFittingSize方法计算出设置视图最佳的高度,这是为了达到更好的拓展性(背景设置与画笔设置所需要的高度很可能会不同)。

然后再增加一个setupBrushSettingsView方法:

funcsetupBrushSettingsView(){
letbrushSettingsView=UINib(nibName:"PaintingBrushSettingsView",bundle:nil).instantiateWithOwner(nil,options:nil).firstasPaintingBrushSettingsView
self.addConstraintsToToolbarForSettingsView(brushSettingsView)
brushSettingsView.hidden=true
brushSettingsView.tag=1
brushSettingsView.setBackgroundColor(self.board.strokeColor)
brushSettingsView.strokeWidthChangedBlock={
[unownedself](strokeWidth:CGFloat)->Voidin
self.board.strokeWidth=strokeWidth
}
brushSettingsView.strokeColorChangedBlock={
[unownedself](strokeColor:UIColor)->Voidin
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.items
self.setupBrushSettingsView()
//---

在paintingBrushSettings方法里响应点击:

@IBActionfuncpaintingBrushSettings(){
self.currentSettingsView=self.toolbar.viewWithTag(1)
self.currentSettingsView?.hidden=false
self.updateToolbarForSettingsView()
}
funcupdateToolbarForSettingsView(){
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方法:

@IBActionfuncendSetting(){
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文件:

放置一个title为“从相册中选择背景图”的UIButton,约束为:左、上边距为8,宽度固定为135,高度固定为30。
放置一个RGBColorPicker,约束为:左、右边距为8,与UIButton的垂直间距为20,底部与superview齐平。
把UIButton的TouchUpInside事件连接到BackgroundSettingsVC的pickImage方法上;RGBColorPicker连接到BackgroundSettingsVC的colorPicker属性上。

看上去像这样:

BackgroundSettingsVC类的完整代码:

classBackgroundSettingsVC:UIViewController,UIImagePickerControllerDelegate,UINavigationControllerDelegate{
varbackgroundImageChangedBlock:((backgroundImage:UIImage)->Void)?
varbackgroundColorChangedBlock:((backgroundColor:UIColor)->Void)?
@IBOutletprivatevarcolorPicker:RGBColorPicker!
lazyprivatevarpickerController:UIImagePickerController={
[unownedself]in
letpickerController=UIImagePickerController()
pickerController.delegate=self
returnpickerController
}()
overridefuncawakeFromNib(){
super.awakeFromNib()
self.colorPicker.colorChangedBlock={
[unownedself](color:UIColor)in
ifletbackgroundColorChangedBlock=self.backgroundColorChangedBlock{
backgroundColorChangedBlock(backgroundColor:color)
}
}
}
funcsetBackgroundColor(color:UIColor){
self.colorPicker.setCurrentColor(color)
}
@IBActionfuncpickImage(){
self.presentViewController(self.pickerController,animated:true,completion:nil)
}
//MARK:UIImagePickerControllerDelegateMethods
funcimagePickerController(picker:UIImagePickerController,didFinishPickingMediaWithInfoinfo:[NSObject:AnyObject]){
letimage=info[UIImagePickerControllerOriginalImage]asUIImage
ifletbackgroundImageChangedBlock=self.backgroundImageChangedBlock{
backgroundImageChangedBlock(backgroundImage:image)
}
self.dismissViewControllerAnimated(true,completion:nil)
}
//MARK:UINavigationControllerDelegateMethods
funcnavigationController(navigationController:UINavigationController,willShowViewControllerviewController:UIViewController,animated:Bool){
UIApplication.sharedApplication().setStatusBarHidden(true,withAnimation:.None)
}
}

同样用两个Block进行回调;setBackgroundColor公共方法用于设置内部的RGBColorPicker的初始颜色状态;在UINavigationControllerDelegate里隐藏系统默认显示的状态栏。

回到ViewController,我们对背景设置进行测试。

像setupBrushSettingsView方法一样,我们增加一个setupBackgroundSettingsView方法:

funcsetupBackgroundSettingsView(){
letbackgroundSettingsVC=UINib(nibName:"BackgroundSettingsVC",bundle:nil).instantiateWithOwner(nil,options:nil).firstasBackgroundSettingsVC
self.addConstraintsToToolbarForSettingsView(backgroundSettingsVC.view)
backgroundSettingsVC.view.hidden=true
backgroundSettingsVC.view.tag=2
backgroundSettingsVC.setBackgroundColor(self.board.backgroundColor!)
self.addChildViewController(backgroundSettingsVC)
backgroundSettingsVC.backgroundImageChangedBlock={
[unownedself](backgroundImage:UIImage)in
self.board.backgroundColor=UIColor(patternImage:backgroundImage)
}
backgroundSettingsVC.backgroundColorChangedBlock={
[unownedself](backgroundColor:UIColor)in
self.board.backgroundColor=backgroundColor
}
}

修改viewDidLoad方法:

self.toolbarEditingItems=[
UIBarButtonItem(barButtonSystemItem:.FlexibleSpace,target:nil,action:nil),
UIBarButtonItem(title:"完成",style:.Plain,target:self,action:"endSetting")
]
self.toolbarItems=self.toolbar.items
self.setupBrushSettingsView()
self.setupBackgroundSettingsView()//Added~!!!

实现backgroundSettings方法:

@IBActionfuncbackgroundSettings(){
self.currentSettingsView=self.toolbar.viewWithTag(2)
self.currentSettingsView?.hidden=false
self.updateToolbarForSettingsView()
}

编译、运行,现在你可以用不同的背景色(或背景图)了!

全屏绘图
到目前为止,Board一直显示不全(事实上,我很早就实现了全屏绘图,但是优先级一直被我排在最后),现在是时候来解决它了。

解决思路是这样的:当用户开始绘图的时候,我们把顶部和底部两个View隐藏;当用户结束绘图的时候,再让两个View显示。

为了获取用户的绘图状态,我们需要在Board里加个“钩子”:

//增加一个Block回调
vardrawingStateChangedBlock:((state:DrawingState)->())?
privatefuncdrawingImage(){
ifletbrush=self.brush{
//hook
ifletdrawingStateChangedBlock=self.drawingStateChangedBlock{
drawingStateChangedBlock(state:self.drawingState)
}
UIGraphicsBeginImageContext(self.bounds.size)
//...

这样一来用户绘图的状态就在ViewController掌握中了。

ViewController想要控制两个View的话,还需要增加几个属性:

@IBOutletvartopView:UIView!
@IBOutletvartopViewConstraintY:NSLayoutConstraint!
@IBOutletvartoolbarConstraintBottom:NSLayoutConstraint!

然后在viewDidLoad方法里增加对“钩子”的处理:

self.board.drawingStateChangedBlock={(state:DrawingState)->()in
ifstate!=.Moved{
UIView.beginAnimations(nil,context:nil)
ifstate==.Began{
self.topViewConstraintY.constant=-self.topView.frame.size.height
self.toolbarConstraintBottom.constant=-self.toolbar.frame.size.height
self.topView.layoutIfNeeded()
self.toolbar.layoutIfNeeded()
}elseifstate==.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方法上:

@IBActionfuncsaveToAlbum(){
UIImageWriteToSavedPhotosAlbum(self.board.takeImage(),self,"image:didFinishSavingWithError:contextInfo:",nil)
}

我为Board添加一个新的公共方法:takeImage:

functakeImage()->UIImage{
UIGraphicsBeginImageContext(self.bounds.size)
self.backgroundColor?.setFill()
UIRectFill(self.bounds)
self.image?.drawInRect(self.bounds)
letimage=UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
returnimage
}

然后是一个方法指针的回调:

funcimage(image:UIImage,didFinishSavingWithErrorerror:NSError?,contextInfo:UnsafePointer){
ifleterr=error{
UIAlertView(title:"错误",message:err.localizedDescription,delegate:nil,cancelButtonTitle:"确定").show()
}else{
UIAlertView(title:"提示",message:"保存成功",delegate:nil,cancelButtonTitle:"确定").show()
}
}

旅行到终点了~!

感谢一路的陪伴!

看了下,有些小长,文本+代码有2w3+,全部代码去除空行和空格有1w4+,直接贴代码会简单很多,但我始终觉得让代码完成功能并不是全部目的,代码背后隐藏的问题定义、设计、构建更有意义,毕竟软件开发完成“后”比完成“前”所花费的时间永远更多(除非是一个只有10行代码或者“一次性”的程序)。

希望与大家多多交流。

tags:

上一篇  下一篇

相关:

不停止MySQL服务增加从库的两种方式

现在生产环境MySQL数据库是一主一从,由于业务量访问不断增大,故再增加一台从库。前提是不能影响线上业务使

大多数人在寻找快乐,但他想让你思考沮丧和忧郁

是作还是够深刻——荷兰设计师 Nel Verbeke 认为,现代人主动追求幸福和快乐,却被动接受悲伤和忧郁。久而

这个衣服有型、装备好用的日本登山品牌,也在纽约开店了

如果你是野营达人,热爱户外运动,也对高品质装备有所研究,相信你对日本的 Snow Peak 品牌并不陌生。最近

这是一份好礼物清单,全球最活跃的设计博客编辑推荐

本文由 Coolhunting 授权《好奇心日报》发布,即使我们允许了也不许转载。 Cool Hunting 团队致力于传播趣

英国运行了50年的全民免费医疗,成效不低,但手术排队好几个月

伦敦奥运会开幕式,特地表现了英国的 NHS 全民免费医疗系统,国家名片妥妥的。NHS 是西方国家第一个全民免费

「你好,我是一名天文学家,我的最新研究项目是麻辣烫」

先从 MALATANG 说起,这是 JCMT (James Clerk Maxwell Telescope)属下的大型观测项目之一,全称是 MAppin

养了一只害羞的小刺猬,哼唧

—图还蛮多的预警—哼唧是我最近开始养的一只小刺猬,英文名是 Edward(男朋友叫 Alphonse,你们能不能 get

华为小米终极PK:谁能笑到最后?

腾讯科技 郭晓峰 11月26日报道十年前,国产品牌曾有过短暂的春天,然而在经历诺基亚、三星等洋品牌的冲击之

为什么计算机能读懂1和0?

关于计算机为什么能读懂 0 和 1, @萧井陌 同学的答案中已经回答地相当仔细了,相信有钻研精神的朋友读完他

隔壁老王长相、气质不如你,为什么大家对他的评价竟然更高

下面这样的情况在我们身上或多或少地发生过(肯定发生过啦,不要装了):虽然有个人长相、气质不如你(对,

站长推荐: