1. 程式人生 > >Swift-貝賽爾曲線畫扇形、弧線、圓形、多邊形——UIBezierPath實現App下載時的動畫效果

Swift-貝賽爾曲線畫扇形、弧線、圓形、多邊形——UIBezierPath實現App下載時的動畫效果

上篇文章提到了使用貝賽爾曲線實現畫圖板(傳送門),頓時就對貝賽爾曲線興趣大增有木有。

之所以接觸貝賽爾曲線,多虧了師父。週五下班前師父給我留了個任務,讓我週末回家研究研究 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 = .Round // 線條拐角
    aPath.lineJoinStyle = .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(rect: CGRectMake(40, 40, 100, 50)) // 長方形
//    let aPath = UIBezierPath(rect: CGRectMake(40, 40, 100, 100)) // 正方形
    
    aPath.lineWidth = 5.0 // 線條寬度
    aPath.lineCapStyle = .Round // 線條拐角
    aPath.lineJoinStyle = .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(ovalInRect: CGRectMake(40, 40, 100, 100)) // 如果傳入的是正方形,畫出的就是內切圓
    let aPath = UIBezierPath(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(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(),只不過我們需要先在終點和圓心之間連一條線,然後在圓心和起點之間連一條線,然後在使用 fill() 填充。

程式碼如下,最後一條線(圓心到起點)我們同樣使用 closePath 來實現:

// 扇形
override func drawRect(rect: CGRect) {
    let color = UIColor.redColor()
    color.set() // 設定線條顏色
    
    let aPath = UIBezierPath(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(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 圖:



有的童鞋可能會說,可是你這個不是半透明的,App下載時的圓圈是半透明的啊!

好辦,只需要把 fill() 改成 fillWithBlendMode(.Normal, alpha: 0.5) 就行了,那樣畫出來的線就會是半透明的了。

Good job!真正實現時,我們就根據下載的進度,修改終點即可。當然,這些都只是我的個人想法,如果有更好的做法歡迎討論。