貝塞爾曲線(xiàn)是指可以通過(guò)一些控制點(diǎn)去控制曲線(xiàn)的形狀并且保持曲線(xiàn)的平滑特性芥备,不會(huì)讓人感覺(jué)到突兀冬耿。在iOS開(kāi)發(fā)中,貝塞爾曲線(xiàn)的使用主要通過(guò)UIKit中的UIBezierPath類(lèi)萌壳,這個(gè)類(lèi)可以使用貝塞爾曲線(xiàn)和直線(xiàn)實(shí)現(xiàn)一些復(fù)雜的圖形結(jié)構(gòu)亦镶。本文會(huì)介紹貝塞爾曲線(xiàn)的原理和使用UIBezierPath繪制貝塞爾曲線(xiàn),文末會(huì)給兩個(gè)使用UIBezierPath實(shí)現(xiàn)的Demo袱瓮。
貝塞爾曲線(xiàn)簡(jiǎn)介
歷史沿革 貝塞爾曲線(xiàn)(Bézier curve)又稱(chēng)曲線(xiàn)或貝濟(jì)埃曲線(xiàn)缤骨,是計(jì)算機(jī)圖形學(xué)中相當(dāng)重要的參數(shù)曲線(xiàn),它包含了大量的數(shù)學(xué)證明幾何推理尺借,經(jīng)過(guò)近半個(gè)世紀(jì)發(fā)展之后绊起,終于在1962年于經(jīng)就職于雷諾的法國(guó)工程師皮埃爾·貝塞爾(Pierre Bézier)在汽車(chē)車(chē)體工業(yè)設(shè)計(jì)中的成功實(shí)踐后廣泛宣傳推廣,之后被大家稱(chēng)之為貝塞爾曲線(xiàn)燎斩,貝塞爾可以說(shuō)是理論聯(lián)系實(shí)際的踐行者虱歪。當(dāng)然,貝塞爾曲線(xiàn)最初是由Paul de Casteljau于1959年運(yùn)用de Casteljau算法開(kāi)發(fā)栅表,以穩(wěn)定數(shù)值的方法求出貝茲曲線(xiàn)笋鄙。
貝塞爾曲線(xiàn)的一個(gè)簡(jiǎn)的定義即:通過(guò)某些控制點(diǎn),去生成的復(fù)雜平滑曲線(xiàn)
數(shù)學(xué)依據(jù) 沒(méi)有復(fù)雜的數(shù)學(xué)公式怪瓶,畢竟我們不做數(shù)學(xué)研究(o)/~局装,這里只列舉了一下幾何推導(dǎo)過(guò)程。
①首先想象有兩條直線(xiàn)AB劳殖、BC相交于B點(diǎn)铐尚,AB、BC長(zhǎng)度不做限定
②分別在兩條直線(xiàn)上任取一個(gè)點(diǎn)D哆姻、F宣增,滿(mǎn)足條件AD/AB = BF/BC
③在DF上任取一點(diǎn)G,滿(mǎn)足滿(mǎn)足條件DG/DF = AD/AB = BF/BC
④從A到C畫(huà)一條曲線(xiàn)(也可以反過(guò)來(lái))矛缨,以B為控制點(diǎn)爹脾,則剛剛的G點(diǎn)一定處于A到C點(diǎn)的貝塞爾曲線(xiàn)之上帖旨。不斷變化DF位置,遞歸剛才的過(guò)程①-③的步驟灵妨,嘗試完所有點(diǎn)并連接起來(lái)解阅,我們可以得到如下一條平滑的曲線(xiàn),此曲線(xiàn)即為貝塞爾曲線(xiàn)
如果將整個(gè)遞推過(guò)程行測(cè)GIF動(dòng)畫(huà)泌霍,將會(huì)是如下效果
如果不易理解货抄,我們將GIF動(dòng)畫(huà)停止到某幀(25)的時(shí)候停一下,可得到如下圖朱转,此時(shí)下圖如同我們第④個(gè)步驟中結(jié)果圖蟹地,它同樣滿(mǎn)足條件 Q0 B / Q0 Q1 = P0Q0 / P0P1 = P1Q1/P1P2
N次貝塞爾曲線(xiàn) 在上述在貝塞爾曲線(xiàn)中,將P1稱(chēng)為控制點(diǎn)藤为,而P0和P2稱(chēng)為起點(diǎn)和終點(diǎn)怪与,由此可以推出,起點(diǎn)終點(diǎn)位置不變的情況下缅疟,調(diào)整P1的位置將導(dǎo)致曲線(xiàn)形狀發(fā)生變化分别,這也是稱(chēng)P1為控制點(diǎn)的由來(lái)。如上一個(gè)控制點(diǎn)的曲線(xiàn)我們稱(chēng)之為二次貝塞爾曲線(xiàn)存淫。當(dāng)然茎杂,該過(guò)程可以拓展至N次,使用迭代即可完成纫雁,如下圖情況是三次貝塞爾曲線(xiàn)
控制點(diǎn)為0(沒(méi)有控制點(diǎn))的曲線(xiàn)也就是一條直線(xiàn)煌往,下圖是一次貝塞爾和四次貝塞爾的動(dòng)畫(huà)示例
UIBezierPath
UIBezierPath提供了實(shí)現(xiàn)二次和三次貝塞爾曲線(xiàn)的方法,同時(shí)該類(lèi)還提供了繪制矩形轧邪、圓弧以及橢圓等其它功能刽脖,我們可以通過(guò)它實(shí)現(xiàn)某些UIKit不能提供的多邊形圖層,實(shí)現(xiàn)一些曲線(xiàn)動(dòng)畫(huà)等忌愚。由于繪制圖形需要一個(gè)上下文環(huán)境曲管,通常可以放到自定義UIView的draw(_ rect: CGRect)
中或者CALayer的draw(in ctx: CGContext)
硕糊,這兩個(gè)方法中系統(tǒng)已經(jīng)自動(dòng)配置了繪圖環(huán)境院水。如果不做任何操作,建議不要保留空方法简十,會(huì)導(dǎo)致無(wú)意義的性能開(kāi)銷(xiāo)檬某,因?yàn)橄到y(tǒng)要配置繪圖上下文環(huán)境。
繪圖上下文 可以理解為一塊畫(huà)布圖層螟蝙,一個(gè)應(yīng)用中可能有多個(gè)上下文環(huán)境即多塊畫(huà)布恢恼,存在一個(gè)畫(huà)布圖層棧stack中維護(hù),通常使用
UIGraphicsGetCurrentContext()
即可取得胰默。在UIView的draw(_ rect: CGRect)
方法中UIKit其實(shí)已經(jīng)為我們維護(hù)好了一個(gè)context场斑,無(wú)需我們創(chuàng)建既可以在這里繪制圖形漓踢。而如果我們需要在某個(gè)地方臨時(shí)時(shí)使用也可以自己創(chuàng)建,例如可以通過(guò)如下方法繪制一張圖片漏隐。CGContextRef ctx = UIGraphicsGetCurrentContext(); // 獲取當(dāng)前畫(huà)布 // draw in here... // 這里默認(rèn)ctx就是當(dāng)前的繪制上下文 UIImage *image = > > > UIGraphicsGetImageFromCurrentImageContext(); // 取得圖片 UIGraphicsEndImageContext(); // 銷(xiāo)毀畫(huà)布 ```
上下文狀態(tài)棧 在一塊畫(huà)布上畫(huà)圖可能有多種狀態(tài)喧半,比如使用的筆的顏色、粗細(xì)等青责,畫(huà)出的線(xiàn)應(yīng)該是虛線(xiàn)挺据、實(shí)線(xiàn)又或者其他特性等等,這些筆或者線(xiàn)等的狀態(tài)需要記錄爽柒,也就記錄在我們的圖形上下文環(huán)境中吴菠,這個(gè)環(huán)境中有一個(gè)狀態(tài)棧stack專(zhuān)門(mén)用來(lái)保存當(dāng)前狀態(tài)者填。棧頂中保存的狀態(tài)即為當(dāng)前狀態(tài)(當(dāng)前繪制使用的狀態(tài))浩村,亦可稱(chēng)為當(dāng)前狀態(tài)。
二次貝塞爾曲線(xiàn)Demo 自定義UIView占哟,并覆蓋draw(_ rect: CGRect)
心墅,使用UIBezierPath繪制的二次貝塞爾曲線(xiàn),如下面圖例和對(duì)應(yīng)的原理圖榨乎。
由于現(xiàn)在所處的的是UIKit配置好的context環(huán)境下怎燥,所以設(shè)置顏色、線(xiàn)寬以及路勁形狀等代碼只要在繪制提交(stroke方法調(diào)用)之前即可蜜暑。
override func draw(_ rect: CGRect) {
UIColor.green.set() // 設(shè)置顏色
let path = UIBezierPath()
path.lineWidth = 5 // 線(xiàn)寬
path.move(to: CGPoint(x: 100, y: 200)) // 起點(diǎn)
// 設(shè)置終點(diǎn)和控制點(diǎn)
path.addQuadCurve(to: CGPoint(x: 300, y: 200), controlPoint: CGPoint(x: 100, y: 0))
// 繪制路徑
path.stroke()
}
CAShapeLayer繪制圖形 除了上述使用draw(_ rect: CGRect)
方法繪制外铐姚,我們還可以將路徑計(jì)算完成后賦值給CAShapeLayer的屬性,讓CAShapeLayer去執(zhí)行繪制過(guò)程肛捍。由于draw(_ rect: CGRect)
方法中繪制由CPU計(jì)算處理隐绵,而CALayer的屬性是由GPU計(jì)算處理,因此使用CAShapeLayer方式會(huì)加快繪制速度拙毫,不過(guò)也要是CPU個(gè)GPU整體負(fù)載情況均衡拿捏铜异,通惩ǎ可能觸發(fā)離屏渲染的操作(比如同時(shí)設(shè)置cornerRadius和maskToBounds)更推薦使用CPU提前處理,然后再交給GPU。
三次貝塞爾曲線(xiàn)Demo 如果使用CAShapeLayer繪制貝塞爾曲線(xiàn)蒂破,則無(wú)需自定義View。使用UIBezierPath繪制的三次貝塞爾曲線(xiàn)培愁,如下面圖例和對(duì)應(yīng)的原理圖监氢。
使用shapeLayer后,發(fā)現(xiàn)設(shè)置線(xiàn)寬衅码、顏色等屬性對(duì)path滞欠,而shapeLayer有效,即使用shapeLayer來(lái)后更多的渲染計(jì)算工作交由CALayer來(lái)控制肆良,而這部分控制更多的話(huà)轉(zhuǎn)嫁給GPU筛璧,這樣的好處可以釋放更多的CPU資源
override func viewDidLoad() {
super.viewDidLoad()
let path = UIBezierPath()
path.move(to: CGPoint(x: 100, y: 250)) // 起點(diǎn)
// 設(shè)置終點(diǎn)和控制點(diǎn)
path.addCurve(to: CGPoint(x: 400, y: 200), controlPoint1: CGPoint(x: 240, y: 120), controlPoint2: CGPoint(x: 260, y: 280))
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.lineWidth = 5 // 線(xiàn)寬
shapeLayer.strokeColor = UIColor.green.cgColor // 線(xiàn)顏色
shapeLayer.fillColor = nil
view.layer.addSublayer(shapeLayer)
}
使用UIBezierPath實(shí)現(xiàn)一個(gè)進(jìn)度指示器
實(shí)現(xiàn)這個(gè)指示器主要考慮幾點(diǎn):圓心逸绎、半徑,開(kāi)始角度夭谤、終點(diǎn)角度棺牧。使用UIBezierPath繪扇形,繪制扇形只需要
一個(gè)圓弧
+一條圓心到終點(diǎn)的直線(xiàn)
+填充模式
即可朗儒。該案例完整的代碼如下:
class ProgressLayerView: UIView {
var progress: CGFloat
override func draw(_ rect: CGRect) {
let pathRadius = bounds.size.width / 2 - 10
let arcCenter = CGPoint(x: bounds.size.width * 0.5, y: bounds.size.height * 0.5)
let startAngle: CGFloat = CGFloat(-Double.pi * 0.5)
let endAngle: CGFloat = CGFloat(Double.pi * 2) * progress + startAngle
let placeholderPath = UIBezierPath(arcCenter: arcCenter, radius: pathRadius, startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true)
UIColor.gray.set()
placeholderPath.fill()
// 扇形
let path = UIBezierPath(arcCenter: arcCenter, radius: pathRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
// 圓心到終點(diǎn)的直線(xiàn)
path.addLine(to: arcCenter)
UIColor.white.set()
// 填充模式繪圖
path.fill()
}
override init(frame: CGRect) {
progress = 0.01
super.init(frame: frame)
backgroundColor = UIColor.black.withAlphaComponent(0.8)
layer.cornerRadius = 5
layer.masksToBounds = true
// For test
refreshProgress()
}
func refreshProgress() {
progress += 0.01
setNeedsDisplay()
if progress >= 1.0 {
progress = 0.0
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now().advanced(by: .milliseconds(100))) {
self.refreshProgress()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
UIView實(shí)現(xiàn)透明孔洞
下圖紅線(xiàn)是UIBezierPath路徑颊乘,箭頭表示路徑方向,通常情況下路徑方向沒(méi)有什么作用醉锄,不過(guò)在做路勁繪制動(dòng)畫(huà)或者實(shí)現(xiàn)以下這種透明孔洞時(shí)顯得很有用乏悄,只需要控制一下填充規(guī)則就可以實(shí)現(xiàn)這種效果。
填充規(guī)則 CAShapeLayer有一個(gè)成員fullRule
指定填充規(guī)則恳不,只適用哪一種算法判斷畫(huà)布上的某一個(gè)區(qū)域是否屬于該圖形路徑的內(nèi)部
檩小,對(duì)一個(gè)沒(méi)有交叉的線(xiàn)框如矩形,要判斷哪塊區(qū)域是內(nèi)部
比較直觀烟勋。但對(duì)于一個(gè)復(fù)查交叉路徑(比如自交或包含路徑關(guān)系)就不太明確了规求,此時(shí)就需要一定的規(guī)則才行。
fullRule
有兩個(gè)值卵惦,nonZero:非零
和 evenOdd:奇偶
非零
按該規(guī)則阻肿,要判斷一個(gè)點(diǎn)是否在圖形內(nèi),從該點(diǎn)作任意方向的一條射線(xiàn)沮尿,然后檢測(cè)射線(xiàn)與圖形路徑的交點(diǎn)情況丛塌。從0開(kāi)始計(jì)數(shù),路徑從左向右穿過(guò)射線(xiàn)則計(jì)數(shù)加1畜疾,從右向左穿過(guò)射線(xiàn)則計(jì)數(shù)減1赴邻。得出計(jì)數(shù)結(jié)果后,如果結(jié)果是0庸疾,則認(rèn)為點(diǎn)在圖形外部乍楚,否則認(rèn)為在內(nèi)部。如下圖示例:*
奇偶
按該規(guī)則届慈,要判斷一個(gè)點(diǎn)是否在圖形內(nèi)徒溪,從該點(diǎn)作任意方向的一條射線(xiàn),然后檢測(cè)射線(xiàn)與圖形路徑的交點(diǎn)的數(shù)量金顿。如果結(jié)果是奇數(shù)則認(rèn)為點(diǎn)在內(nèi)部臊泌,是偶數(shù)則認(rèn)為點(diǎn)在外部。如下圖示例:*
明確以上規(guī)則后揍拆,就容易控制圖層某些地方區(qū)域透明和不透明渠概。由于該成員默認(rèn)值是nonZero:非零
,除了通過(guò)修改fullRule的值為evenOdd:奇偶
實(shí)現(xiàn)中心透明外,還可以使用UIBezierPath的reversing()
方法改變路徑曲線(xiàn)的默認(rèn)方向來(lái)到達(dá)到相同的效果播揪。該案例的完整代碼如下:
@IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
// 背景圖片
if let path = Bundle.main.path(forResource: "1404103479048.jpg", ofType: nil) {
let image = UIImage(contentsOfFile: path)
imageView.image = image
}
// 覆蓋圖層
let maskView = UIView(frame: self.view.bounds)
maskView.backgroundColor = UIColor.black.withAlphaComponent(0.8)
view.addSubview(maskView)
// 孔洞位置(alpha透明的位置)
let width: CGFloat = 300
let height: CGFloat = width
let cornerRadius: CGFloat = width * 0.5
let pathRect = CGRect(x: (maskView.bounds.size.width - width)/2,
y: (maskView.bounds.size.height - height)/2 - 100,
width: width,
height: height)
// 先創(chuàng)建一個(gè)路徑放和原來(lái)的覆蓋圖層maskView一樣大
let path = UIBezierPath(roundedRect: maskView.frame, cornerRadius: 0)
// 然后再創(chuàng)建一個(gè)路徑時(shí)需要alpha透明的位置
path.append(UIBezierPath(roundedRect: pathRect, cornerRadius: cornerRadius))
// 創(chuàng)建shapeLayer贮喧,并將path給到shapeLayer
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
// 設(shè)置填充規(guī)則
shapeLayer.fillRule = .evenOdd
// 將shapeLayer賦值給maskView.layer.mask
// 這個(gè)mask是求兩個(gè)layer(shapeLayer和maskView.layer)的交集
// 這個(gè)交集可以理解為面積交集
maskView.layer.mask = shapeLayer
}
參考資料
維基百科
百度百科
GPU vs CPU in iOS
iOS 圖形繪制框架
必須要理解掌握的貝塞爾曲線(xiàn)(原創(chuàng))
iOS UIBezierPath貝塞爾曲線(xiàn)常用方法
iOS 利用CAShapeLayer的FillRule屬性生成一個(gè)空心遮罩的layer