最近看了很多關于貝塞爾曲線的文章,好好總結了一番,加上自己的一點思路,做了點微小的工作。廢話不多說,直接上圖:
主要使用到了二階貝塞爾曲線,那么開始之前,先了解一下什么是二階貝塞爾曲線
二階貝塞爾曲線
首先,我們在平面內選3個不同線的點并且依次用線段連接。如下所示
接著,我們在AB和BC線段上找出點D和點E,使得AD/AB = BE/BC。
再接著,連接DE,并在DE上找出一點F,使得DF/DE = AD/AB = BE/BC。
然后,讓選取的點D在第一條線段上從起點A,移動到終點B,找出所有點F,并將它們連起來。最后得到了一條非常光滑的曲線,這條就是傳說中的。。。二階貝塞爾曲線。
看這里,看這里,看這里:
仔細觀察會發現,起始點P0,結束點P2,和曲線是相切的關系。
所以,如果要使兩條貝塞爾曲線光滑連接,只要保證第一條貝塞爾曲線的結束點和第二條貝塞爾曲線相切就行。
如果使貝塞爾A的結束點A2與貝塞爾B的起始點B0重合,那么,貝塞爾A的控制點A1,結束點A2,貝塞爾B的起始點B0(即A2),貝塞爾B的控制點B1,連接起來就是一條直線。
原理就這些,是時候進入正文了,皮皮蝦,我們走!
抽絲剝繭
這樣看是不是清晰很多,均勻添加7個View作為關鍵點,然后由這些點畫出3條二階貝塞爾曲線。
BezierPath明明只需要CGPoint就行了,為什么這里設置了7個View來作為貝塞爾曲線的關鍵點,而不是7個CGPoint,這個后面說
關鍵代碼:
bezierDotCount = 7
for i in 0...bezierDotCount-1 {
let view = UIView(frame: CGRect(x: Int(self.frame.width)*i/(bezierDotCount-1), y: 0, width: 10, height: 10))
view.center = CGPoint(x: Int(self.frame.width)*i/(bezierDotCount-1), y: 0)
self.addSubview(view)
view.backgroundColor = UIColor.red
view.layer.cornerRadius = 5
viewArray.append(view)
}
shapeLayer = CAShapeLayer()
shapeLayer?.strokeColor = UIColor.white.cgColor
shapeLayer?.fillColor = UIColor.clear.cgColor
shapeLayer?.path = currentPath()
self.layer.addSublayer(shapeLayer!)
生成曲線:
func currentPath() -> CGPath {
let width = self.bounds.size.width
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: viewArray[0].center.y))
for i in stride(from: 1, to: bezierDotCount-1, by: 2) {
path.addQuadCurve(to: (viewArray[i+1].center), controlPoint: (viewArray[i].center))
}
path.addLine(to: CGPoint(x: width, y: self.frame.height))
path.close()
return path.cgPath
}
再往下剝一點。曲線是由點畫出來了,但是這些點在移動中又是如何確定的呢?
如果依次把點命名為L3,L2,L1,C,R1,R2,R3,那么:
第一條貝塞爾曲線以L3為起始點,L2為控制點,L1為結束點,
第二條曲線以L1為起始點,C為控制點,R1為結束點,
第三條曲線以R1為起始點,R2為控制點,R3為結束點。
有圖有真相:
是不是更清晰了。
在使用手勢操作時,我們需要一個控制點跟隨手指來控制整個曲線的運動,顯然中點C是最好的選擇。
那么其他點應該如何移動呢?
就如前面說的,為了使連接的曲線平滑,我們得保證兩個控制點和起始點(結束點)是一直線,所以L2,L1,C得保證是一條直線,C,R1,R2也是一條直線。
在移動中要保證3點一直線,就要讓他們按比例來移動。
我們也別想得太復雜了。就把L3到C的距離三等分
用初中數學可以算出比例
下面就可以算出關鍵點坐標了
let additionalHeight = max(gesture.translation(in: self).y, 0)
let waveHeight = min(additionalHeight*2/3, 100)
let baseHeight = additionalHeight-waveHeight
let locationX = gesture.location(in: self).x
let width = self.bounds.size.width
let minLeftX = CGFloat(0)
let maxRightX = width
let leftPartWidth = locationX - minLeftX
let rightPartWidth = maxRightX - locationX
viewArray[0].center = CGPoint(x: minLeftX, y: baseHeight)
viewArray[1].center = CGPoint(x: minLeftX+leftPartWidth/3, y: baseHeight)
viewArray[2].center = CGPoint(x: minLeftX+leftPartWidth*2/3, y: baseHeight+waveHeight*2/3)
viewArray[3].center = CGPoint(x: locationX, y: baseHeight+waveHeight*4/3)
viewArray[4].center = CGPoint(x: maxRightX-rightPartWidth*2/3, y: baseHeight+waveHeight*2/3)
viewArray[5].center = CGPoint(x: maxRightX-(rightPartWidth/3), y: baseHeight)
viewArray[6].center = CGPoint(x: maxRightX, y: baseHeight)
到這里,所有點都出來了,加上手勢,應該是這樣
我們發現是沒有DuangDuangDuang~的特效
聰明的你應該想到了可以給關鍵點view加上彈簧動畫
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: UIViewAnimationOptions.curveEaseIn, animations: {
for i in 0...self.bezierDotCount-1 {
self.viewArray[i].center = CGPoint(x: Int(self.frame.width)*i/(self.bezierDotCount-1), y: 0)
}
self.shapeLayer?.path = self.currentPath()
}, completion: {[weak self] (finish) -> Void in
}
納尼!為什么曲線沒有跟著動?
這個時候我們就要使用Presentation Layer,可以實時獲取 Layer 屬性的當前值。
這就是為什么一開始使用view作為關鍵點,而不是CGPoint,這里可以通過關鍵點view的Presentation Layer,生成一個新的view,然后放到CADisplayLink實時獲取彈簧動畫中的位置,最后重新繪制曲線。
千萬記得在彈簧動畫結束后,將CADisplayLink設置invalidate
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: UIViewAnimationOptions.curveEaseIn, animations: {
for i in 0...self.bezierDotCount-1 {
self.viewArray[i].center = CGPoint(x: Int(self.frame.width)*i/(self.bezierDotCount-1), y: 0)
}
self.displaylink = CADisplayLink(target: self, selector: #selector(self.displayLinkAction))
self.displaylink?.add(to: RunLoop.main, forMode: RunLoopMode.commonModes)
}, completion: {[weak self] (finish) -> Void in
self?.displaylink?.invalidate()
}
)
func displayLinkAction(dis:CADisplayLink) {
let rectViewArray:Array = viewArray.map({
(view) -> UIView in
let layer = view.layer.presentation()
let rect:CGRect = layer?.value(forKey: "frame") as! CGRect
let rectView = UIView(frame: rect)
return rectView
})
let width = self.bounds.size.width
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: (rectViewArray[0].center.y)))
for i in stride(from: 1, to: rectViewArray.count-1, by: 2) {
path.addQuadCurve(to: (rectViewArray[i+1].center), controlPoint: (rectViewArray[i].center))
}
path.addLine(to: CGPoint(x: width, y: self.frame.height))
path.close()
shapeLayer?.path = path.cgPath
}
到這里就完成啦。
GitHub源碼,記得點星星哦
稍微修改一下也能做出不錯的特效菜單哦DuangDuangDuang