運用 UIBezierPath 繪製各種形狀
開發 iOS App 時,我們可以利用 UIBezierPath 繪製路徑,然後搭配 CAShapeLayer 做出任意形狀的 view。接下來我們先從基本的三角形開始,然後再一步步挑戰像是上弦月之類較為複雜的圖形吧。
三角形
1 利用 UIBezierPath 繪製三角形的路徑。
2 生成三角形的 CAShapeLayer 。
let triangleLayer = CAShapeLayer()
triangleLayer.path = path.cgPath
CAShapeLayer 可繪製特定的形狀,我們透過它的 path 設定形狀。然而 triangleLayer.path 的型別是 CGPath,所以我們無法指定剛剛生成的 UIBezierPath ,而須利用 path.cgPath 讀取 CGPath 型別的三角形路徑後再存入 triangleLayer.path。
3 生成 100 * 100 的紅色正方形 View。
待會我們即將把它變成三角形。
let frame = CGRect(x: 0, y: 0, width: 100, height: 100)
let view = UIView(frame: frame)
view.backgroundColor = UIColor(red: 1, green: 0, blue: 0, alpha: 1)
4 利用 CAShapeLayer 和 mask 將 view 變成三角形。
view.layer.mask = triangleLayer
mask 中文是遮罩的意思,經由設定 CALayer 元件的 mask,我們將讓 view 只顯示 mask 裡非透明的區塊,其它部分被遮起來。在我們的例子,非透明的區塊即為剛剛 (0, 0),(100, 0),(100,100) 繪製的三角形。
mask 的相關說明。
The layer’s alpha channel determines how much of the layer’s content and background shows through. Fully or partially opaque pixels allow the underlying content to show through but fully transparent pixels block that content.
如果從 view.layer.mask 看預覽,只會顯示 NSObject,因此我們還差最後一步。
5 從 view 看預覽。
view
記得要從 view 才能看到它的模樣,從 layer 是看不到的,因此我們在最後一行輸入 view,查看 view 的模樣。
完整程式
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 100, y: 0))
path.addLine(to: CGPoint(x: 100, y: 100))
path.close()
let triangleLayer = CAShapeLayer()
triangleLayer.path = path.cgPath
let frame = CGRect(x: 0, y: 0, width: 100, height: 100)
let view = UIView(frame: frame)
view.backgroundColor = UIColor(red: 1, green: 0, blue: 0, alpha: 1)
view.layer.mask = triangleLayer
view
剛剛將 view 變成特定形狀的程式,基本上可分成以下三個步驟
- 利用 UIBezierPath 繪製形狀的路徑。
- 產生 CAShapeLayer,透過 path 將它變成 UIBezierPath 繪製的形狀。
- 產生 UIView,透過 mask 將它變成 CAShapeLayer 設定的形狀。
小潘飛刀
利用 UIBezierPath 的 addLine 畫線,我們已可將每一邊描繪出來,繪製出大部分的圖形,就算有一百邊也不是問題。比方以下彼得潘創作的小潘飛刀。(ps: 接下來的程式範例將只包含 UIBezierPath 生成路徑的段落,請記得最後要結合 CAShapeLayer 設為 mask。)
let path = UIBezierPath()
var point = CGPoint(x: 0, y: 20)
path.move(to: point)
point = CGPoint(x: 100, y: 0)
path.addLine(to: point)
point = CGPoint(x: 100, y: 100)
path.addLine(to: point)
point = CGPoint(x: 75, y: 40)
path.addLine(to: point)
path.close()
運用 UIBezierPath 繪製其它形狀
長方形
若想繪製長方形,並不需要傻傻地用 UIBezierPath 的 addLine 畫四條線,因為建立 UIBezierPath 時即可傳入長方形的位置和大小。
let path = UIBezierPath(rect: CGRect(x: 10, y: 10, width: 30, height: 50))
圓角的長方形
利用 UIBezierPath 的 init(roundedRect:cornerRadius:) 繪製有圓角的長方形。
let path = UIBezierPath(roundedRect: CGRect(x: 10, y: 10, width: 80, height: 50), cornerRadius: 5)
如果不想四個頂點都有圓角,比方只有左下和右下有圓角,則可利用 UIBezierPath 的 init(roundedRect:byRoundingCorners:cornerRadii:)。
let path = UIBezierPath(roundedRect: CGRect(x: 10, y: 10, width: 80, height: 50), byRoundingCorners: [.bottomLeft, .bottomRight], cornerRadii: CGSize(width: 5, height: 0))
參數說明:
byRoundingCorners: 型別為 UIRectCorner,控制長方形圓角的位置。
cornerRadii: 控制圓角的程度,在此我們傳入的 CGSize 只要指定寬度即可,高度可設為 0。
圓形
利用 UIBezierPath 的 init(ovalIn:) 繪製橢圓,參數 ovalIn 的長方形決定橢圓的寬高。
let path = UIBezierPath(ovalIn: CGRect(x: 0, y: 10, width: 100, height: 50))
想繪製圓形也不是問題,只要參數 ovalIn 傳入正方形即可。
let path = UIBezierPath(ovalIn: CGRect(x: 0, y: 10, width: 60, height: 60))
介紹完如何畫直線和圓形,接著來挑戰進階一點的主題吧,讓我們來繪製彎曲的曲線。
繪製剛好在圓上的圓弧
利用 UIBezierPath 的 init(arcCenter:radius:startAngle:endAngle:clockwise:) 繪製圓弧。
比方以下程式可畫出只能看不能吃的西瓜。
let aDegree = CGFloat.pi / 180
let path = UIBezierPath(arcCenter: CGPoint(x: 50, y: 50), radius: 40, startAngle: aDegree * 0, endAngle: aDegree * 180, clockwise: true)
參數說明:
arcCenter: 圓心座標。
radius: 半徑。
startAngle & endAngle: 圓弧開始和結束的角度。圓形有 360 度,如下圖所示,在程式裡我們以 2 pi 表示 360 度,1 pi 表示 180 度。
我們都知道 pi 是 3.1415926… ,可是我們只記得前面幾位,不記得後面的數字。沒關係,iOS SDK 已經幫我們定義好,透過 CGFloat.pi 即可取得。關於從型別 + . 讀取屬性的語法,相關說明可參考以下連結。
因此 1 度是 pi / 180,我們利用 let aDegree = CGFloat.pi / 180
算出一度的大小,存在常數 aDegree。而角度 0 度的位置在右邊,180 度在左邊。startAngle 傳入 aDegree * 0,endAngle 傳入 aDegree * 180時,表示從 0 度畫到 180 度。不過由於這個例子剛好整除,所以也可以簡化成傳入 0 & CGFloat.pi。
let path = UIBezierPath(arcCenter: CGPoint(x: 50, y: 50), radius: 40, startAngle: 0, endAngle: CGFloat.pi, clockwise: true)
clockwise: 是否為順時針。畫圓時可以順時針,也可以逆時針。剛剛的例子, 參數 clockwise 傳入 true,因此我們順時針從 0 度畫到 180 度,產生西瓜形狀的下半圓。
其它例子:
逆時針從 10 度畫到 150 度。
let path = UIBezierPath(arcCenter: CGPoint(x: 50, y: 50), radius: 40, startAngle: aDegree * 10, endAngle: aDegree * 150, clockwise: false)
值得注意的,剛剛方法繪製的形狀填滿顏色時將以圓弧和圓弧兩端點連線包含的區塊,若想畫出從圓心連到圓弧兩端點的扇形,則須先用 move 移動到圓心,然後再呼叫 addArc(withCenter:radius:startAngle:endAngle:clockwise:) 加入圓弧,例如以下例子:
let path = UIBezierPath()
path.move(to: CGPoint(x: 50, y: 50))
path.addArc(withCenter: CGPoint(x: 50, y: 50), radius: 40, startAngle: aDegree * 10, endAngle: aDegree * 150, clockwise: false)
其它例子:
// 畫出比較大的外圓弧,從 (0, 50) 到 (100, 50)
let path = UIBezierPath(arcCenter: CGPoint(x: 50, y: 50), radius: 50, startAngle: aDegree * 180, endAngle: aDegree * 0, clockwise: true)
// 畫出右下方的橫線,從 (100, 50) 連到 (75, 50)
path.addLine(to: CGPoint(x: 75, y: 50))
// 畫出比較小的內圓弧,從 (75, 50) 到 (25, 50)
path.addArc(withCenter: CGPoint(x: 50, y: 50), radius: 25, startAngle: aDegree * 0, endAngle: aDegree * 180, clockwise: false)
path.close()
繪製特別的曲線
剛剛介紹的方法,只能畫出剛好在圓上的圓弧,但若想畫出特別弧度的曲線,則須運用以下兩個方法,addQuadCurve & addQuadCurve。
let path = UIBezierPath(arcCenter: CGPoint(x: 50, y: 50), radius: 50, startAngle: aDegree * 270, endAngle: aDegree * 90, clockwise: true)
path.addQuadCurve(to: CGPoint(x: 50, y: 0), controlPoint: CGPoint(x: 80, y: 50))
UIBezierPath(arcCenter: CGPoint(x: 50, y: 50), radius: 50, startAngle: aDegree * 270, endAngle: aDegree * 90, clockwise: true)
將從 (50, 0) 到 (50, 100) 畫出圓弧,接著呼叫 addQuadCurve 時,利用參數 controlPoint 控制從 (50, 100) 到 (50, 0) 的曲線弧度,即可畫出可愛的上弦月。
如下圖所示,controlPoint 控制曲線的弧度。剛剛 addQuadCurve(to: CGPoint(x: 50, y: 0), controlPoint: CGPoint(x: 80, y: 50))
的參數 to 對應下圖的 C 點,controlPoint 對應 B 點,那麼 A 點呢 ? A 點是在呼叫 addQuadCurve 前畫到的位置,因此是從 (50, 100) 開始。
以下為 controlPoint 改成 (20,50) & (120, 50) 的例子。
let path = UIBezierPath(arcCenter: CGPoint(x: 50, y: 50), radius: 50, startAngle: aDegree * 180, endAngle: aDegree * 0, clockwise: true)
path.addCurve(to: CGPoint(x: 0, y: 50), controlPoint1: CGPoint(x: 50, y: 80), controlPoint2: CGPoint(x: 50, y: 20))
比剛剛複雜一點, addCurve(to:controlPoint1:controlPoint2:) 的控制點有兩個,利用 controlPoint1 & controlPoint2 控制從 (100, 50) 到 (0, 50) 的曲線弧度。
關於曲線繪製的更多說明,也可以參考 Jaba 同學的文章。
組合路徑
剛剛介紹的都是單一路徑,其實我們也可將多個路徑結合,例如以下同時顯示圓形跟三角形的例子。
let trianglePath = UIBezierPath()
var point = CGPoint(x: 0, y: 0)
trianglePath.move(to: point)
point = CGPoint(x: 100, y: 0)
trianglePath.addLine(to: point)
point = CGPoint(x: 100, y: 100)
trianglePath.addLine(to: point)
trianglePath.close()
let rect = CGRect(x: 0, y: 40, width: 40, height: 40)
let circlePath = UIBezierPath(roundedRect: rect, cornerRadius: 20)
trianglePath.append(circlePath)
說明:
結合多個路徑繪製的形狀。
trianglePath.append(circlePath)
我們可將多個 UIBezierPath 結合,透過 append 加入另一個 UIBezierPath。