最近看了很多关于贝塞尔曲线的文章,好好总结了一番,加上自己的一点思路,做了点微小的工作。废话不多说,直接上图:
bezierPopLine主要使用到了二阶贝塞尔曲线,那么开始之前,先了解一下什么是二阶贝塞尔曲线
二阶贝塞尔曲线
首先,我们在平面内选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,连接起来就是一条直线。
原理就这些,是时候进入正文了,皮皮虾,我们走!
抽丝剥茧
bezierPopLine.gif这样看是不是清晰很多,均匀添加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)
到这里,所有点都出来了,加上手势,应该是这样
bezierPopLine.gif我们发现是没有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
}
bezierPopLine5.gif
Duang
纳尼!为什么曲线没有跟着动?
这个时候我们就要使用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
duang2.gif
网友评论