转载请注明出处:http://blog.csdn.net/zhangao0086/article/details/43836789。
要做一个全功能的绘图板,至少要支持以下这些功能:
- 支持铅笔绘图(画点)
- 支持画直线
- 支持一些简单的图形(矩形、圆形等)
- 做一个真正的橡皮擦
- 能设置画笔的粗细
- 能设置画笔的颜色
- 能设置背景色或者背景图
- 能支持撤消与重做
- …
我们先做一些基础性的工作,比如创建工程。
工程搭建
先创建一个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个选项,分别是:- 铅笔
- 直尺
- 虚线
- 矩形
- 圆形
- 橡皮擦
- 给这个SegmentedControl添加约束:
垂直居中,两边各留20,高度固定为28。
完整的UI及结构看起来像这样:
ImageView将会作为实际的绘制区域,顶部的SegmentedControl提供工具的选择。 到目前为止我们还没有写下一行代码,至此要开始编码了。
你可能会注意到Board有一部分被挡住了,这只是暂时的~
施工…
Board
我们创建一个Board
类,继承自UIImageView
,同时把这个类设置为Main.storyboard
中ImageView
的Class,这样当app启动的时候就会自动创建一个Board的实例了。
增加两个属性以及初始化方法:
var strokeWidth: CGFloat
var strokeColor: UIColor
override 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
顾名思义,BaseBrush
将会作为一个绘图的基类而存在,我们会在它的基础上创建一系列的子类,以达到弹性的设计目的。为此,我们创建一个BaseBrush
类,并实现一个PaintBrush
接口:
import CoreGraphics
protocol 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
声明了两个方法:
- 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
添加两个新的属性:
var brush: BaseBrush?
private var realImage: UIImage?
brush
对应到具体的画笔类,realImage
保存当前的图形,重新修改touches方法,以便增加对brush
属性的处理,完整的touches方法实现如下:
// MARK: - touches methods
override 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