上篇文章提到了使用贝赛尔曲线实现画图板(传送门),顿时就对贝赛尔曲线兴趣大增有木有。
之所以接触贝赛尔曲线,多亏了师父。周五下班前师父给我留了个任务,让我周末回家研究研究 iPhone 手机下载 App 时的效果是怎么实现的(不知道效果的童鞋请看下图)
如果所示,下载 App 的过程效果,就是 App 图标中间有一个顺时针旋转的圆圈。当一圈走完时 App 就下载完成了。
刚给我交代这个任务的时候,顿时感觉好难有木有。。。(主要是因为那个时候我还不知道贝赛尔曲线)
抛开一切复杂内容不谈,我们今天只来说说怎么让一个圆像这样走完一圈。其实就是利用贝赛尔曲线画扇形。但在介绍怎么画扇形之前,我们先从基础开始,逐步了解 UIBezierPath。
(PS:本文内容虽然有点长,但是并不难,不要害怕,看完就能使用UIBezierPath做一些基本的操作了。在文章的最后我们讲重头戏:实现App下载时的动画效果。)
我们分直线段和曲线段两种来讲。
先来回顾一下上文(没看过也没关系)。上篇文章我们讲了怎么实现画图板,其实本质上是用直线段来实现:记录触摸的初始坐标和滑动的坐标,在二者之间连一条直线段。主要方法就是 moveToPoint: 和 addLineToPoint:
有的童鞋可能会问,那为什么能画出曲线啊?
其实不是的。你画了一条曲线可能用了0.1秒(假设),但实际上却是我每隔0.0000000001秒(假设)画了一条直线段,这样大量的微小的直线段连接起来,最后宏观上看上去就像是曲线了,但本质上、从微观角度看,它是非常多的小直线段。
额上面这段话并不重要,看不懂也没事,咱们继续说本文的重点。
知道怎么画直线了,那么正方形、矩形、多边形就都好办了。
我们先来举个多边形(比如五边形)的栗子。用现有的知识想一下,怎么实现?
很简单,只要有五个顶点的坐标,用直线段连起来就行了:
override func drawRect(rect: CGRect) { // 五边形 let color = UIColor.redColor() color.set() // 设置线条颜色 let aPath = UIBezierPath() aPath.lineWidth = 5.0 // 线条宽度 aPath.lineCapStyle = CGLineCap.Round // 线条拐角 aPath.lineJoinStyle = CGLineJoin.Round // 终点处理 // Set the starting point of the shape. aPath.moveToPoint(CGPointMake(100, 10)) // Draw the lines aPath.addLineToPoint(CGPointMake(200, 40)) aPath.addLineToPoint(CGPointMake(160, 140)) aPath.addLineToPoint(CGPointMake(40, 140)) aPath.addLineToPoint(CGPointMake(10, 40)) aPath.closePath() // 最后一条线通过调用closePath方法得到 aPath.stroke() // Draws line 根据坐标点连线,不填充 // aPath.fill() // Draws line 根据坐标点连线,填充 }有的童鞋可能注意到了:“你怎么只画了四条线啊?closePath 是干嘛的?”
画最后一条线有个简单的办法,那就是调用 closePath 方法,就会自动连上终点到起点间的线,形成一个封闭的图形。
上文我们已经使用过了 stroke(),也就是连线。那么最后一句 fill() 又是干嘛的?stroke() 只是连线,不会填充。如果想要填充效果的话,就使用 fill() 来实现:
如上图所示。左图为 stroke() 的效果,右图为 fill() 的效果。
(ps:需要重绘时不要直接调用 drawRect: 方法,调用 setNeedsDisplay 系统就会自动调用 drawRect: 方法了)
是不是很简单。想一下矩形、正方形怎么画?以画五边形的方法类推,有了四个顶点的坐标连线就行了。
嗯,这样可以。但其实有个更简单的方法,使用 UIBezierPath 类的 init(rect:CGRect) 方法。
如果传入的宽高相等,画出来的就是正方形,否则就是长方形(废话)
// 矩形 override func drawRect(rect: CGRect) { let color = UIColor.redColor() color.set() // 设置线条颜色 let aPath = UIBezierPath.init(rect: CGRectMake(40, 40, 100, 50)) // 长方形 // let aPath = UIBezierPath.init(rect: CGRectMake(40, 40, 100, 100)) // 正方形 aPath.lineWidth = 5.0 // 线条宽度 aPath.lineCapStyle = CGLineCap.Round // 线条拐角 aPath.lineJoinStyle = CGLineJoin.Round // 终点处理 aPath.stroke() // Draws line 根据坐标点连线,不填充 // aPath.fill() // Draws line 根据坐标点连线,填充 }效果如下图所示,同样分两种,左图为 stroke() ,右图为 fill() 。正方形同理。
OK,直线段就讲的差不多了,下面我们来讲讲曲线段。
怎么画一个圆/椭圆?使用 UIBezierPath 的 init(ovalInRect rect:CGRect) 方法即可。
这是什么意思呢?我们传入一个矩形的 frame,则会画出这个矩形的内切圆。
如果矩形是一个正方形,那么画出的就是内切圆。如果矩形是个长方形,那么画出的就是内切椭圆。
// 圆、椭圆 override func drawRect(rect: CGRect) { let color = UIColor.redColor() color.set() // 设置线条颜色 // 根据传人的矩形画出内切圆/椭圆 // let aPath = UIBezierPath.init(ovalInRect: CGRectMake(40, 40, 100, 100)) // 如果传入的是正方形,画出的就是内切圆 let aPath = UIBezierPath.init(ovalInRect: CGRectMake(40, 40, 100, 160)) // 如果传入的是长方形,画出的就是内切椭圆 aPath.lineWidth = 5.0 // 线条宽度 aPath.stroke() // Draws line 根据坐标点连线,不填充 // aPath.fill() // Draws line 根据坐标点连线,填充 }以椭圆形为例,同样分 stroke() 和 fill() 来演示下,圆形同理:
注意,传入的矩形的 frame 只是为了画出它的内切圆,矩形是不会画出来的。
唔,快要到重点了。在画扇形前先来说说怎么画弧线。这两可不一样哦。
如图所示,这是一个四分之一圆的弧线。它是怎么实现的呢?使用 UIBezierPath 类的 init(arcCenter center:CGPoint, radius:CGFloat, startAngle:CGFloat, endAngle:CGFloat, clockwise:Bool) 方法。
参数很多,分别来说下:center 是圆心坐标,radius 是半径长度,startAngle 是起始点,endAngle 是终点,clockwise 是 true 则为顺时针,为 false 则是逆时针。
我们来看一下官方文档:
注意到了吗?0 是右边那个点,不是上面那个点。
代码如下,以四分一圆的弧线为例:
// 弧线 override func drawRect(rect: CGRect) { let color = UIColor.redColor() color.set() // 设置线条颜色 let aPath = UIBezierPath.init(arcCenter: CGPointMake(150, 150), radius: 75, startAngle: 0, endAngle: (CGFloat)(90*M_PI/180), clockwise: true) aPath.lineWidth = 5.0 // 线条宽度 aPath.stroke() // Draws line 根据坐标点连线,不填充 // aPath.fill() // Draws line 根据坐标点连线,填充 }
并不是。我们来看一下把上述代码改成 fill() 的效果:
是不是很出乎意料。的确是填充了,但不是填充到圆心,效果和我们想要的不一样。
那扇形怎么画啊?
其实还是用 fill(),只不过我们需要先在终点和圆心之间连一条线,然后在圆心和起点之间连一条线,然后在使用 fill() 填充。
代码如下,最后一条线(圆心到起点)我们同样使用 closePath 来实现:
// 扇形 override func drawRect(rect: CGRect) { let color = UIColor.redColor() color.set() // 设置线条颜色 let aPath = UIBezierPath.init(arcCenter: CGPointMake(150, 150), radius: 75, startAngle: 0, endAngle: (CGFloat)(90*M_PI/180), clockwise: true) aPath.addLineToPoint(CGPointMake(150, 150)) aPath.closePath() aPath.lineWidth = 5.0 // 线条宽度 // aPath.stroke() // Draws line 根据坐标点连线,不填充 aPath.fill() // Draws line 根据坐标点连线,填充 }效果如下图所示:
Perfect!现在再想想文章开头提到的下载 App 时的效果,是不是就有思路了。
我们把 startAngle 设为正上方那个点,然后下载完成了多少,我们就画多大的扇形,把 endAngle 改为已下载完成的部分。
比如下载完成了10%,那我们就画一个十分之一的扇形。下载完了一半,那我们就画一个半圆。
当然这只是简单一说,具体实现起来还有其它细节。不过有了思路以后,我相信是可以实现的。
下面我们来举一个简单的栗子。假设某个应用总共用了两秒下载完,而且每 0.1 秒下载二十分之一(当然这只是假设)。
这样我们就可以每隔 0.1 秒画一个二十分之一的扇形,两秒后整个圆就全部画完了。上代码:
// 实现 App 下载时的效果 var beginAngle = M_PI*3/2 // 起点 var finishAngle = M_PI*3/2+M_PI*2/20 // 终点 override func drawRect(rect: CGRect) { let color = UIColor.whiteColor() color.set() // 设置线条颜色 let aPath = UIBezierPath.init(arcCenter: CGPointMake(150, 150), radius: 75, startAngle: (CGFloat)(beginAngle), endAngle: (CGFloat)(finishAngle), clockwise: true) aPath.addLineToPoint(CGPointMake(150, 150)) aPath.closePath() aPath.lineWidth = 5.0 // 线条宽度 aPath.fill() // Draws line 根据坐标点连线,填充 finishAngle += M_PI/20 // 更新终点 }这段代码和之前讲的几乎一样,唯一不同的就是我们每次画完以后,都更新一下终点,让它多二十分之一圆。
然后就是每隔 0.1 秒执行一次,2秒后画完,停止执行。上代码:
class ViewController: UIViewController { let myView = MyView.init(frame: CGRectZero) //我们自定义的view var timer: NSTimer! // 计时器 override func viewDidLoad() { super.viewDidLoad() myView.frame = view.bounds view.addSubview(myView) // 每隔 0.1 秒执行一次 timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: Selector("reDrawView"), userInfo: nil, repeats: true) } func reDrawView() { myView.setNeedsDisplay() // 重绘界面 // 画完一圈后停止 if myView.finishAngle > myView.beginAngle+M_PI*2 { timer.invalidate() // 停止计时器 } } }
有的童鞋可能会说,可是你这个不是半透明的,App下载时的圆圈是半透明的啊!
好办,只需要把 fill() 改成 fillWithBlendMode(CGBlendMode.Normal, alpha: 0.5) 就行了,那样画出来的线就会是半透明的了。
Good job!真正实现时,我们就根据下载的进度,修改终点即可。当然,这些都只是我的个人想法,如果有更好的做法欢迎讨论。
完整的demo请见我的GitHub:https://github.com/963239327/UIBezierPathDemo 别忘了点右上角的 star 哦~