Swift-貝賽爾曲線畫扇形、弧線、圓形、多邊形——UIBezierPath實現App下載時的動畫效果
上篇文章提到了使用貝賽爾曲線實現畫圖板(傳送門),頓時就對貝賽爾曲線興趣大增有木有。
之所以接觸貝賽爾曲線,多虧了師父。週五下班前師父給我留了個任務,讓我週末回家研究研究 iPhone 手機下載 App 時的效果是怎麼實現的(不知道效果的童鞋請看下圖)
如果所示,下載 App 的過程效果,就是 App 圖示中間有一個順時針旋轉的圓圈。當一圈走完時 App 就下載完成了。
剛給我交代這個任務的時候,頓時感覺好難有木有。。。(主要是因為那個時候我還不知道貝賽爾曲線)
拋開一切複雜內容不談,我們今天只來說說怎麼讓一個圓像這樣走完一圈。其實就是利用貝賽爾曲線畫扇形。但在介紹怎麼畫扇形之前,我們先從基礎開始,逐步瞭解 UIBezierPath。
(PS:本文內容雖然有點長,但是並不難,不要害怕,看完就能使用UIBezierPath做一些基本的操作了。在文章的最後我們講重頭戲:實現App下載時的動畫效果。)
我們分直線段和曲線段兩種來講。
先來回顧一下上文(沒看過也沒關係)。上篇文章我們講了怎麼實現畫圖板,其實本質上是用直線段來實現:記錄觸控的初始座標和滑動的座標,在二者之間連一條直線段。主要方法就是 moveToPoint: 和 addLineToPoint:
有的童鞋可能會問,那為什麼能畫出曲線啊?
其實不是的。你畫了一條曲線可能用了0.1秒(假設),但實際上卻是我每隔0.0000000001秒(假設)畫了一條直線段,這樣大量的微小的直線段連線起來,最後巨集觀上看上去就像是曲線了,但本質上、從微觀角度看,它是非常多的小直線段。
額上面這段話並不重要,看不懂也沒事,咱們繼續說本文的重點。
知道怎麼畫直線了,那麼正方形、矩形、多邊形就都好辦了。
我們先來舉個多邊形(比如五邊形)的栗子。用現有的知識想一下,怎麼實現?
很簡單,只要有五個頂點的座標,用直線段連起來就行了:
有的童鞋可能注意到了:“你怎麼只畫了四條線啊?closePath 是幹嘛的?”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 方法,就會自動連上終點到起點間的線,形成一個封閉的圖形。
上文我們已經使用過了 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!真正實現時,我們就根據下載的進度,修改終點即可。當然,這些都只是我的個人想法,如果有更好的做法歡迎討論。