[置顶] Swift-贝赛尔曲线画扇形、弧线、圆形、多边形——UIBezierPath实现App下载时的动画效果

上篇文章提到了使用贝赛尔曲线实现画图板(传送门),顿时就对贝赛尔曲线兴趣大增有木有。

之所以接触贝赛尔曲线,多亏了师父。周五下班前师父给我留了个任务,让我周末回家研究研究 iPhone 手机下载 App 时的效果是怎么实现的(不知道效果的童鞋请看下图)

[置顶] Swift-贝赛尔曲线画扇形、弧线、圆形、多边形——UIBezierPath实现App下载时的动画效果_第1张图片

如果所示,下载 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() 来实现:

[置顶] Swift-贝赛尔曲线画扇形、弧线、圆形、多边形——UIBezierPath实现App下载时的动画效果_第2张图片[置顶] Swift-贝赛尔曲线画扇形、弧线、圆形、多边形——UIBezierPath实现App下载时的动画效果_第3张图片

如上图所示。左图为 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() 。正方形同理。

[置顶] Swift-贝赛尔曲线画扇形、弧线、圆形、多边形——UIBezierPath实现App下载时的动画效果_第4张图片[置顶] Swift-贝赛尔曲线画扇形、弧线、圆形、多边形——UIBezierPath实现App下载时的动画效果_第5张图片

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() 来演示下,圆形同理:

[置顶] Swift-贝赛尔曲线画扇形、弧线、圆形、多边形——UIBezierPath实现App下载时的动画效果_第6张图片[置顶] Swift-贝赛尔曲线画扇形、弧线、圆形、多边形——UIBezierPath实现App下载时的动画效果_第7张图片

注意,传入的矩形的 frame 只是为了画出它的内切圆,矩形是不会画出来的。


唔,快要到重点了。在画扇形前先来说说怎么画弧线。这两可不一样哦。

[置顶] Swift-贝赛尔曲线画扇形、弧线、圆形、多边形——UIBezierPath实现App下载时的动画效果_第8张图片
如图所示,这是一个四分之一圆的弧线。它是怎么实现的呢?使用 UIBezierPath 类的 
init(arcCenter center:CGPoint, radius:CGFloat, startAngle:CGFloat, endAngle:CGFloat, clockwise:Bool) 方法。

参数很多,分别来说下:center 是圆心坐标,radius 是半径长度,startAngle 是起始点,endAngle 是终点,clockwise 是 true 则为顺时针,为 false 则是逆时针。

我们来看一下官方文档:

[置顶] Swift-贝赛尔曲线画扇形、弧线、圆形、多边形——UIBezierPath实现App下载时的动画效果_第9张图片

注意到了吗?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() 的效果:

[置顶] Swift-贝赛尔曲线画扇形、弧线、圆形、多边形——UIBezierPath实现App下载时的动画效果_第10张图片

是不是很出乎意料。的确是填充了,但不是填充到圆心,效果和我们想要的不一样。


那扇形怎么画啊?

其实还是用 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 根据坐标点连线,填充
}
效果如下图所示:

[置顶] Swift-贝赛尔曲线画扇形、弧线、圆形、多边形——UIBezierPath实现App下载时的动画效果_第11张图片

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() // 停止计时器
        }
    }
}


最终效果见下面的 gif 图:

[置顶] Swift-贝赛尔曲线画扇形、弧线、圆形、多边形——UIBezierPath实现App下载时的动画效果_第12张图片

有的童鞋可能会说,可是你这个不是半透明的,App下载时的圆圈是半透明的啊!

好办,只需要把 fill() 改成 fillWithBlendMode(CGBlendMode.Normal, alpha: 0.5) 就行了,那样画出来的线就会是半透明的了。

Good job!真正实现时,我们就根据下载的进度,修改终点即可。当然,这些都只是我的个人想法,如果有更好的做法欢迎讨论。

完整的demo请见我的GitHub:https://github.com/963239327/UIBezierPathDemo 别忘了点右上角的 star 哦~







你可能感兴趣的:(github,ios,动画,swift,UIBezierPath)