Swift3.0学习实践-一个简单的画板(七色轨迹可撤销可清除带橡皮擦)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift3.0学习实践-一个简单的画板(七色轨迹可撤销可清除带橡皮擦)相关的知识,希望对你有一定的参考价值。

写着玩儿的小程序,继续学习Swift.运行效果+代码+知识点总结

运行效果:

技术分享           技术分享

代码:

Canvas类:画布,画图板状态管理、交互、处理手势
  1. class Canvas:UIView{  
  2.     //负责线条的生成、操作与管理  
  3.     let pathCreator:PathCreator  
  4.     //是否处于擦除状态  
  5.     var isInErasering:Bool  
  6.     //橡皮擦视图  
  7.     let eraserView:UIView  
  8.       
  9.     override init(frame: CGRect) {  
  10.         isInErasering = false  
  11.         pathCreator = PathCreator()  
  12.           
  13.         eraserView = UIView.init()  
  14.         eraserView.frame = CGRect(x: 0, y: 0, width: 10, height: 10)  
  15.         eraserView.backgroundColor = UIColor.white  
  16.         eraserView.alpha = 0  
  17.   
  18.         super.init(frame: frame)  
  19.           
  20.         self.backgroundColor = UIColor.black  
  21.           
  22.         self.addSubview(eraserView)  
  23.           
  24.         let revokeBut = UIButton(type: UIButtonType.system)  
  25.         revokeBut.frame = CGRect(x: 20, y: 20, width: 80, height: 30)  
  26.         revokeBut.setTitle("撤销", for: UIControlState.normal)  
  27.         revokeBut.addTarget(self, action: #selector(revokeButClick), for: UIControlEvents.touchUpInside)  
  28.         self.addSubview(revokeBut)  
  29.           
  30.         let cleanBut = UIButton(type: UIButtonType.system)  
  31.         cleanBut.frame = CGRect(x: 110, y: 20, width: 80, height: 30)  
  32.         cleanBut.setTitle("清空", for: UIControlState.normal)  
  33.         cleanBut.addTarget(self, action: #selector(cleanButClick), for: UIControlEvents.touchUpInside)  
  34.         self.addSubview(cleanBut)  
  35.       
  36.         let eraserBut = UIButton(type: UIButtonType.system)  
  37.         eraserBut.frame = CGRect(x: 200, y: 20, width:80, height: 30)  
  38.         eraserBut.setTitle("橡皮", for: UIControlState.normal)  
  39.         eraserBut.setTitle("画笔", for: UIControlState.selected)  
  40.         eraserBut.addTarget(self, action: #selector(eraserButClick(but:)), for: UIControlEvents.touchUpInside)  
  41.         self.addSubview(eraserBut)  
  42.           
  43.         let ges = UIPanGestureRecognizer(target: self, action:#selector(handleGes(ges:)))  
  44.         ges.maximumNumberOfTouches = 1  
  45.         self.addGestureRecognizer(ges)  
  46.     }  
  47.       
  48.     required public init?(coder aDecoder: NSCoder) {  
  49.         fatalError("init(coder:) has not been implemented")  
  50.     }  
  51.       
  52.     override public func layoutSubviews() {  
  53.           
  54.     }  
  55.       
  56.     @objc private func handleGes(ges:UIPanGestureRecognizer) -> Void {  
  57.         let point = ges.location(in: self)  
  58.         switch ges.state {  
  59.         case UIGestureRecognizerState.began:  
  60.             if isInErasering {  
  61.                 //擦除状态,显示出橡皮擦  
  62.                 eraserView.alpha = 1  
  63.                 eraserView.center = point  
  64.             }  
  65.             //生成新的一笔  
  66.             pathCreator.addNewPath(to: point,isEraser: isInErasering)  
  67.             self.setNeedsDisplay()  
  68.         case UIGestureRecognizerState.changed:  
  69.             if isInErasering {  
  70.                 //移动橡皮擦  
  71.                 eraserView.center = ges.location(in: self)  
  72.             }  
  73.             //更新当前笔画路径  
  74.             pathCreator.addLineForCurrentPath(to: point,isEraser:isInErasering)  
  75.             self.setNeedsDisplay()  
  76.         case UIGestureRecognizerState.ended:  
  77.             if isInErasering {  
  78.                 //擦除状态,隐藏橡皮擦  
  79.                 eraserView.alpha = 0  
  80.                 eraserView.center = ges.location(in: self)  
  81.             }  
  82.             //更新当前笔画路径  
  83.             pathCreator.addLineForCurrentPath(to: point,isEraser: isInErasering)  
  84.             self.setNeedsDisplay()  
  85.         case UIGestureRecognizerState.cancelled:  
  86.             print("cancel")  
  87.         case UIGestureRecognizerState.failed:  
  88.             print("fail")  
  89.         default:  
  90.             return  
  91.         }  
  92.     }  
  93.       
  94.     override public func draw(_ rect: CGRect) {  
  95.         //画线  
  96.         pathCreator.drawPaths()  
  97.     }  
  98.       
  99.     @objc private func revokeButClick()->Void{  
  100.         //撤销操作  
  101.         pathCreator.revoke()  
  102.         self.setNeedsDisplay()  
  103.     }  
  104.       
  105.     @objc private func cleanButClick()->Void{  
  106.         //清空操作  
  107.         pathCreator.clean()  
  108.         self.setNeedsDisplay()  
  109.     }  
  110.       
  111.     @objc private func eraserButClick(but:UIButton)->Void{  
  112.         //切换画图与擦除状态  
  113.         if but.isSelected {  
  114.             but.isSelected = false  
  115.             isInErasering = false  
  116.         }else{  
  117.             but.isSelected = true  
  118.             isInErasering = true  
  119.         }  
  120.     }  
  121. }  

PathCreator:具体线条绘制、管理

 

  1. //每条子线段信息  
  2. struct BezierInfo{  
  3.     let path:UIBezierPath//具体线段  
  4.     let color:UIColor//线段对应颜色  
  5.     init(path:UIBezierPath,color:UIColor){  
  6.         self.path = path  
  7.         self.color = color  
  8.     }  
  9. }  
  10.   
  11. class PathCreator{  
  12.     //所有笔画  
  13.     private var paths:[NSMutableArray]?  
  14.     //笔画内当前子线段  
  15.     private var currentBezierPathInfo:BezierInfo?  
  16.     //当前笔画的所有子线段  
  17.     private var currentPath:NSMutableArray?  
  18.     //当前笔画已经采集处理了几个触摸点  
  19.     private var pointCountInOnePath = 0  
  20.       
  21.     static let colors = [UIColor.red,UIColor.orange,UIColor.yellow,UIColor.green,UIColor.blue,UIColor.gray,UIColor.purple]  
  22.     init() {  
  23.         paths = []  
  24.     }  
  25.     //添加新笔画  
  26.     func addNewPath(to:CGPoint,isEraser:Bool)->Void{  
  27.         //创建起始线段  
  28.         let path = UIBezierPath()  
  29.         path.lineWidth = 5  
  30.         path.move(to: to)  
  31.         path.lineJoinStyle = CGLineJoin.round  
  32.         path.lineCapStyle = CGLineCap.round  
  33.         if !isEraser {  
  34.             //绑定线段与颜色信息  
  35.             currentBezierPathInfo = BezierInfo(path: path, color: PathCreator.colors[0])  
  36.         }else{  
  37.             //处于擦除模式,颜色与画板背景色相同  
  38.             currentBezierPathInfo = BezierInfo(path: path, color: UIColor.black)  
  39.         }  
  40.         //新建一个笔画  
  41.         currentPath = NSMutableArray.init()  
  42.         //将起始线段加入当前笔画  
  43.         currentPath!.add(currentBezierPathInfo)  
  44.         pointCountInOnePath = 0  
  45.         //将当前笔画加入笔画数组  
  46.         paths!.append(currentPath!)  
  47.     }  
  48.     //添加新的点,更新当前笔画路径  
  49.     func addLineForCurrentPath(to:CGPoint,isEraser:Bool) -> Void {  
  50.         pointCountInOnePath += 1//同一笔画内,每7个点换一次颜色  
  51.         if pointCountInOnePath % 7 == 0{//换颜色  
  52.             if let currentBezierPathInfo = currentBezierPathInfo{  
  53.                 //将当前点加入当前子线段,更新当前子线段路径  
  54.                 currentBezierPathInfo.path.addLine(to: to)  
  55.             }  
  56.             //生成新的子线段  
  57.             let path = UIBezierPath()  
  58.             path.lineWidth = 5  
  59.             path.move(to: to)  
  60.             path.lineJoinStyle = CGLineJoin.round  
  61.             path.lineCapStyle = CGLineCap.round  
  62.             if !isEraser{  
  63.                 //给当前子线段设置下一个颜色  
  64.                 currentBezierPathInfo = BezierInfo(path: path, color: PathCreator.colors[currentPath!.count % 7])  
  65.             }else{  
  66.                 //处于擦除模式,颜色与画板背景色相同  
  67.                 currentBezierPathInfo = BezierInfo(path: path, color: UIColor.black)  
  68.             }  
  69.             //将当前子线段加入当前笔画  
  70.             currentPath!.add(currentBezierPathInfo)  
  71.         }else{  
  72.             if let currentBezierPathInfo = currentBezierPathInfo{  
  73.                 //将当前点加入当前子线段,更新当前子线段路径  
  74.                 currentBezierPathInfo.path.addLine(to: to)  
  75.             }  
  76.         }  
  77.     }  
  78.       
  79.     func drawPaths()->Void{  
  80.         //画线  
  81.         let pathCount = paths!.count  
  82.         for i in 0..<pathCount{  
  83.             //取出所有笔画  
  84.             let onePath = paths![i]  
  85.             let onePathCount = onePath.count  
  86.             for j in 0..<onePathCount{  
  87.                 //绘制每条笔画内每个子线段  
  88.                 let pathInfo = onePath.object(at: j) as! BezierInfo  
  89.                 pathInfo.color.set()  
  90.                 pathInfo.path.stroke()  
  91.             }  
  92.         }  
  93.     }  
  94.       
  95.     func revoke()->Void{  
  96.         //移走上一笔画  
  97.         if paths!.count > 0 {  
  98.             paths!.removeLast()  
  99.         }  
  100.     }  
  101.       
  102.     func clean()->Void{  
  103.         //移走所有笔画  
  104.         paths!.removeAll()  
  105.     }  
  106. }  

知识点总结:

1.结构体是值传递

一个基础概念,但开始使用时还是给忘了。数组[]在swift中是结构体(struct)实现,值传递。最开始把currentPath声明为了[],添加到paths[]中后,后续再去往currentPath中添加元素,paths中的对应的currentpath对象内容并未随之发生改变,后将currentPath改为了NSMutableArray(引用传递).

2.selector、@objc、private

(纯)swift与oc采用了不同的运行机制,swift不再采用与oc一样的运行时(runtime)与消息分发机制,selector作为oc运行机制的产物,swift中也对其进行了保留与支持。

@objc修饰符的作用是将swift定义的类、方法等暴露给oc。

于是,下列selector中指定的方法,都要使用@objc进行修饰

  1. cleanBut.addTarget(self, action: #selector(cleanButClick), for: UIControlEvents.touchUpInside)  
  1. let ges = UIPanGestureRecognizer(target: self, action:#selector(handleGes(ges:)))  
如果一个swift类继承自NSObject,swift会默认给该类的非private属性或方法加上@objc修饰。
因为Canvas类(->UIView->UIResponder->NSObject)继承自NSObject,所以其属性或方法(非private)都会被自动加上@objc修饰
但是因为我代码中的这几个selector指向的方法都声明为了private,所以还是需要手动去做@objc修饰(如果是非private的,可以不写@objc)
  1. @objc private func handleGes(ges:UIPanGestureRecognizer) -> Void  

3.required的构造函数

required用于修饰构造方法,用于要求子类必需实现对应的构造方法
如果子类中没有实现任何构造方法,则不必去显式的实现父类要求的required构造方法;而当子类中有定义实现构造方法时,则必需显式的去实现父类要求的required构造方法,同时还要保留required修饰.
当实现一个类Canvas继承自UIView时,我们可以看到编译器强制要求我们实现构造方法
  1. public init?(coder aDecoder: NSCoder)  
通过xcode找到该方法是在NSCoding协议中被定义的
  1. public protocol NSCoding {  
  2.     public func encode(with aCoder: NSCoder)  
  3.     public init?(coder aDecoder: NSCoder) // NS_DESIGNATED_INITIALIZER  
  4. }  
可以看到,此处并没有进行requird修饰,为什么还要求强制实现该构造方法呢?
因为在协议中规定的构造方法,不用显式进行requird修饰,实现协议的对应类默认必需要去实现协议中规定的构造方法,且加上requird修饰

4.as

  1. let x:UInt16 = 100  
  2. let y:UInt8 = 10  
  3. //x + y会报错,不自动类型转换,更安全  
  4. let n = UInt8(x) + y  
上面例子中,当我们进行值类型之间的类型转换(UInt16->UInt8)时,其实借助的是UInt8的构造方法
  1. /// Create an instance initialized to `value`.  
  2.     public init(integerLiteral value: UInt8)  
而当引用类型之间需要进行强制转换时,则需要借助as操作符
因为转换可能失败(两个不相关的类之间进行转换),所以需要使用as?,转换结果为一个可选型,不成功时,可选型值为nil
当然,如果可以肯定转换是成功的,则可以使用as!进行转换,结果为目标类型的对象。
另外,看下面这个例子
  1. var people:People?  
  2. let man:Man = Man()  
  3. people = man  
  4. print(people)//可选型变量  
  5. let beMan = people as! Man  
  6. print (beMan)//强制转化后beMan不是可选型  
  1. var people:People?  
  2. let man:Man = Man()  
  3. people = man  
  4. print(people)//可选型变量  
  5. let beMan = people as! Man?  
  6. print (beMan)//强制转化后beMan为可选型  

转换后的结果类型完全由as!后面的目标类型决定,即便原对象在转换之前是可选型对象,但如果转换的目标类型不是可选型,则转换后得到的也就不是一个可选型了





以上是关于Swift3.0学习实践-一个简单的画板(七色轨迹可撤销可清除带橡皮擦)的主要内容,如果未能解决你的问题,请参考以下文章

Swift3.0学习实践-一个简单的画板(七色轨迹可撤销可清除带橡皮擦)

Canvas在线画图—简单制作一个画板

如何用几何画板平移抛物线

使用Canvas和JavaScript做一个画板

内存恶鬼drawRect

圆球沿着椭圆轨迹做动画