Swift 全功能的绘图板开发
转载请注明出处:http://www.52php.cn/article/p-htigfqxi-ber.html。 要做一个全功能的绘图板,至少要支持以下这些功能:
我们先做一些基础性的工作,比如创建工程。 工程搭建先创建一个
完整的UI及结构看起来像这样:
施工…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的状态,比如 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顾名思义, 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.")
}
}
只要是实现了
这么一来,子类也可以很方便的获取到当前的状态,并作一些深度定制的绘图方法。
回到Board我们实现了一个画笔的基类之后,就可以重新回到 var brush: BaseBrush?
private var realImage: UIImage?
// 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()
}
}
我们需要防止 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
}
}
步骤解析:
这些工作完成以后,我们就可以开始写第一个工具了:铅笔工具。 铅笔工具铅笔工具应该支持连续不断的绘图(不断的保存到realImage中),这也是我们给 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画线。 测试到目前为止,我们的 var brushes = [PencilBrush()]
在 @IBAction func switchBrush(sender: UISegmentedControl) {
assert(sender.tag < self.brushes.count,"!!!")
self.board.brush = self.brushes[sender.selectedSegmentIndex]
}
最后在 其他的工具接下来我们把其他的绘图工具也实现了。 直尺创建一个 class LineBrush: BaseBrush { override func drawInContext(context: CGContextRef) { CGContextMoveToPoint(context,beginPoint.x,beginPoint.y) CGContextAddLineToPoint(context,endPoint.x,endPoint.y) } }
虚线创建一个 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) } }
这里我们就用到了 矩形创建一个 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定死的。 圆形创建一个 class EllipseBrush: BaseBrush { override func drawInContext(context: CGContextRef) { CGContextAddEllipseInRect(context,height: abs(endPoint.y - beginPoint.y)))) } }
同样有一些计算,理由同上。 橡皮擦从本文一开始就说过了,我们要做一个真正的橡皮擦,网上有很多的橡皮擦的实现其实就是把画笔颜色设置为背景色,但是如果背景色可以动态设置,甚至设置为一个渐变的图片时,这种方法就失效了,所以有些绘图app的背景色就是固定为白色的。 class EraserBrush: PencilBrush { override func drawInContext(context: CGContextRef) { CGContextSetBlendMode(context,kCGBlendModeClear); super.drawInContext(context) } }
注意,与其他的工具不同,橡皮擦是继承自 CGContextSetBlendMode(context,kCGBlendModeClear);
加入这一句代码,一个真正的橡皮擦便实现了。 再次测试现在我们的工程结构应该类似于这样: var brushes = [PencilBrush(),LineBrush(),DashLineBrush(),RectangleBrush(),EllipseBrush(),EraserBrush()]
编译、运行: 设计思路在继续完成剩下的功能之前,我想先对之前的代码进行些说明。 为什么不用drawRect方法其实我最开始也是使用drawRect方法来完成绘制,但是感觉限制很多,比如context无法保存,还是要每次重画(虽然可以保存到一个BitMapContext里,但是这样与保存到image里有什么区别呢?);后来用CALayer保存每一条CGPath,但是这样仍然不能避免每次重绘,因为需要考虑到橡皮擦和画笔属性之类的影响,这么一来还不如采用image的方式来保存最新绘图板。 ViewController与Board、BaseBrush之间的关系在 策略设计模式
策略模式的定义:定义一个算法群,把每一个算法分别封装起来,让它们之间可以互相替换,使算法的变化独立于使用它的用户之上。 模板方法在传统的策略模式中,每一个算法类都独自完成整个算法过程,例如一个网络解析程序,可能有一个算法用于解析
模板方法的定义:在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
以上就是我设计时的思路,说完了,接下来还要完成的工作有:
先从画笔开始,Let’s go! 画笔设置不管是画笔还是背景设置,我们都要有一个能提供设置的工具栏。 设置工具栏所以我们往
UIToolbar配置好后,UI及视图层级如下: RGBColorPicker考虑到多个页面需要选取自定义的颜色,我们先创建一个工具类: 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进行布局,因为 画笔设置的UI我打算在用户点击
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))
}
}
}
关于 Swift 1.2在 Swift 1.2里,不能用 override var backgroundColor: UIColor? {
didSet {
self.strokeColorPreview.backgroundColor = self.backgroundColor
self.colorPicker.setCurrentColor(self.backgroundColor!)
super.backgroundColor = oldValue
}
}
实现毛玻璃效果在把 测试画笔设置我们在ViewController新增加几个属性: var toolbarEditingItems: [UIBarButtonItem]?
var currentSettingsView: UIView?
@IBOutlet var toolbarConstraintHeight: NSLayoutConstraint!
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上,同时添加相应的约束。注意高度的约束,我是通过 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
}
}
我们在这个方法里实例化了一个 //---
self.toolbarEditingItems = [
UIBarButtonItem(barButtonSystemItem:.FlexibleSpace,target: nil,action: nil),UIBarButtonItem(title: "完成",style:.Plain,target: self,action: "endSetting")
]
self.toolbarItems = self.toolbar.items
self.setupBrushSettingsView()
//---
在 @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!)
}
@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
}
这么一来画笔设置就做完了,代码应该还是比较好理解,编译、运行后,应该能看到: 背景设置整体的框架基本上已经在之前的工作中搭好了,我们快速过掉这一节。
看上去像这样:
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进行回调; 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.items
self.setupBrushSettingsView()
self.setupBackgroundSettingsView() // Added~!!!
实现 @IBAction func backgroundSettings() {
self.currentSettingsView = self.toolbar.viewWithTag(2)
self.currentSettingsView?.hidden = false
self.updateToolbarForSettingsView()
}
编译、运行,现在你可以用不同的背景色(或背景图)了! 全屏绘图到目前为止, // 增加一个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掌握中了。 @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秒钟,这样用户可以暂时预览下全图。
保存到图库最后一个功能:保存到图库! @IBAction func saveToAlbum() {
UIImageWriteToSavedPhotosAlbum(self.board.takeImage(),self,"image:didFinishSavingWithError:contextInfo:",nil)
}
我为 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行代码或者“一次性”的程序)。
更新——撤消与重做功能Swift 绘图板功能完善以及终极优化 GitHub地址DrawingBoard (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |