貝塞爾 Layer 入門指南

原文:A Beginner’s Guide to Bezier Paths and Shape Layers

作者:GABRIEL THEODOROPOULOS

譯者:kmyhy

App 的開發往往包含了創建 UI、在屏幕上显示的各種簡單或複雜的 UIView。要繪製這些簡單的“屏幕”有幾種不同的方法:直接使用由設計師製作的圖片,用程式碼繪製 UI,用 Interface Builder,以及將這些方法中的幾種聯合起來。但是,有時候我們必須以程式碼的方式創建自定義的形狀,如果你不懂怎麼做,那就問題大了。

這個問題當然可以避免,這就需要用到 UIBezierPath 類了,根據這篇文檔 中的描述,我們可以用 UIBezierPath 來創建矢量路徑。換句話說,我們可以用這個類繪製任意形狀路徑,然後用這些路徑構成我們想要的圖形。通過它,我們可以用簡單的形狀比如舉行、方塊、橢圓或圓來繪製複雜形狀,你可以在路徑中添加直線或曲線,以及由多個點構成的折線。

要创建一条貝塞爾路徑除了要使用 UIBezierPath 类,还需要一个 Core Graphics 上下文,以便在上下文中进行绘制。获取上下文的方式有三种:

  1. 使用一個 CGContext 對象。
  2. 子類化一個 UIView 類,然後在它的 draw(_:) 方法中用默認提供的上下文繪製自定義形狀。這個方法會自動提供一個上下文。
  3. 創建一個 CAShapeLayer 對象。

我們將在本教程介紹這三種方法中的后兩個,以便將我們的注意力集中在 Core Graphics 編程上面。

本文中還會介紹 CAShapeLayer 類,我们先来聊一下它。這個類繼承自 CALayer 類,在 UIView 的默認 layer 中都會有一個 CAShapeLayer。大部分時候 CAShapeLayer 会被添加到默認的layer 上,但它也可以当做蒙版 layer 来使用。这两种情况我们都会介绍。另外,我们还会介绍 CAShapeLayer 的一些重要属性以及它们的用途。

本文的目的是教你實際操作如何創建貝塞爾路徑以及如何配合 CAShapeLayer 一起使用。我们会舉出一些短小、簡單的例子,在學習本文的過程中,你会先學習一些基本概念,然後會才會介紹更复杂的例子。在你具備一定的貝塞爾路徑和 CAShapeLayer 知識之後,我們才會介紹更新一些的知識。如果你是一个新手,請往下看。本文将介紹在 iOS 開發中的一些很有趣的內容。

預備專案

在 Xcode 中新建一個專案。和我的其他教程不同,這次沒有任何開始專案給你下載,當然也不需要擔心。專案的創建和配置只需要很少的步驟,你馬上就可以使用它了。打開 Xcode,創建一個 single view application 專案:

貝塞爾 Layer 入門指南_第1张图片

給專案命名。這裡,我會將它命名為 PathsNLayers,當然你也可以改成別的名字。填寫 team 和 organisation name,Device 選擇 iPhone。

貝塞爾 Layer 入門指南_第2张图片

最後,選擇文件保存路徑。然後,通過 File > New > File… 菜單,新建一個 Cocoa Touch Class 類:

貝塞爾 Layer 入門指南_第3张图片

確保繼承 UIView 類,類名設置為 DemoView。

貝塞爾 Layer 入門指南_第4张图片

等文件創建完并添加到專案中,你就可以在專案導航器中看到它了。

打開 ViewController.swift 文件,添加方法:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    let width: CGFloat = 240.0
    let height: CGFloat = 160.0

    let demoView = DemoView(frame: CGRect(x: self.view.frame.size.width/2 - width/2,
                                          y: self.view.frame.size.height/2 - height/2,
                                          width: width,
                                          height: height))

    self.view.addSubview(demoView)
}

然後,打開 DemoView.swift 文件,編寫類的實現為:

var path: UIBezierPath!

override init(frame: CGRect) {
    super.init(frame: frame)

    self.backgroundColor = UIColor.darkGray    
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}

準備工作完畢,接下來可以創建我們的第一個貝塞爾路徑了。

創建路徑和形狀

學游泳的最好方法是跳進水裡,學習貝塞爾路徑也同樣如此。因此閒話少說,我們來看一個如何創建、配飾和使用 UIBezierPath 的例子。實際上,我們會創建一系列路徑,每一個路徑都會繪製一個不同的形狀。請深呼吸,打開 DemoView.swift 文件。我們將在這個類中實現這些功能。

矩形

首先來一个既簡單又能够說明問題的例子:

func createRectangle() {
    // 構建路徑
    path = UIBezierPath()

    // 設置路徑的起点
    path.move(to: CGPoint(x: 0.0, y: 0.0))

    // 从起点到 UIView 的左下角之间,绘制直线
    path.addLine(to: CGPoint(x: 0.0, y: self.frame.size.height))

    // 绘制底部的線段(從左下角到右下角)
    path.addLine(to: CGPoint(x: self.frame.size.width, y: self.frame.size.height))

    // 繪製從右下角到右上角的豎線
    path.addLine(to: CGPoint(x: self.frame.size.width, y: 0.0))

    // 關閉路徑。這會自動繪製最後一段線段。
    path.close()
}

上面的程式碼在 UIView 四邊繪製了一個矩形。但這個方法暫時還無法使用,因為我們沒有給路徑的繪製提供一個上下文。等會我們再來這樣做。

首先,我們來看看這些程式碼的用途:

  • 首先,初始化一個 UIBezierPath 對象。這個類有多個擁有不同參數的初始化方法,目前我們只用到了這個最簡單初始化方法。構建出路徑之後,我們首先要做的就是制定我們要繪製的形狀的起點。起點是一個 CGPoint 結構,而 move(to:) 方法用於指定路徑的起點。在這段程式碼中,起點位於坐標(0.0, 0.0) 處,也就是指定視圖的左上角。

  • 然後,從起點繪製一條直線到指定的另一個點。我們需要提供另一個點作為參數并調用 addLine(to:) 方法,讓系統為我們繪製直線。和前面的方法系統,我們需要提供一個 CGPoint 參數,用於指定直線的另一端。第一條直線我們準備從左上角開始,到左下角結束。

  • 接下來兩條語句和前面相同,在兩點間畫線。第四行程式碼,在路徑中添加了從左下角到右下角的直線。第五行程式碼則繪製了從右下角到右上角的直線。

  • 這樣,我們就畫出了矩形的 3 條邊。如果可以,我們也可以畫出第 4 條邊來完成我們的形狀。但這沒有必要了,因為 close() 方法 (最後一行) 會自動為我們畫出剩下的這一邊。

是不是太簡單了?我們用直線連接所有端點。現在來調用這個方法,就在 DemoView 類的 draw(_:) 方法中,這個方法會為路徑提供一個上下文:

override func draw(_ rect: CGRect) {
    self.createRectangle()
}

在測試 App 之前,還有一件很重要的事情,我們需要設置填充色和筆劃的顏色。填充色用於填充整個形狀,而筆劃顏色用於形狀的邊線。在上面的代碼后加入代碼:

override func draw(_ rect: CGRect) {
    self.createRectangle()

    // 為路徑指定填充顏色
    UIColor.orange.setFill()
    path.fill()

    // 指定筆劃(描邊)顏色
    UIColor.purple.setStroke()
    path.stroke()
}

這些代碼都非常直白,我們直接運行一下看看效果。現在 App 運行效果如下:

貝塞爾 Layer 入門指南_第5张图片

恭喜你,你繪製了第一條貝塞爾路徑!當然,這有一點無聊,因為僅僅是畫出 UIView 的邊框的話,我們只需要設置 UIView 的背景色,在 layer 上加一個 border 就可以做到了,而且更簡單。現在,我們有了這些基礎,就可以進入西一階段了。

三角形

你猜到了,要創建任意形狀,就是路徑中在恰當的頂點之間添加線段。你可以創建從三角形到多邊形之間的任意形狀,只要指定適當的頂點位置即可。据此,我们可以畫出一个三角形:

func createTriangle() {
    path = UIBezierPath()
    path.move(to: CGPoint(x: self.frame.width/2, y: 0.0))
    path.addLine(to: CGPoint(x: 0.0, y: self.frame.size.height))
    path.addLine(to: CGPoint(x: self.frame.size.width, y: self.frame.size.height))
    path.close()
}

起點位於 UIView 的上一條邊的中點。上述程式碼和 createRectangle() 方法類似,僅僅是頂點和線段的數目不同而已,效果如下:

貝塞爾 Layer 入門指南_第6张图片

別忘了在 draw(_:) 方法中調用它:

override func draw(_ rect: CGRect) {
    //    self.createRectangle()
    self.createTriangle()

    // 指定填充顏色
    UIColor.orange.setFill()
    path.fill()

    // 指定筆劃顏色
    UIColor.purple.setStroke()
    path.stroke()
}

請嘗試修改起點和 addLine 參數。參數不同繪製的形狀也不同。

橢圓和圓

創建橢圓非常簡單,我們需要調用另外一個路徑初始化方法,而不用簡化的UIBezierPath() 方法。下面舉出一個簡單的例子,使用了新的初始化方法,并將 UIView 的 bound 作為 frame 參數傳遞:

override func draw(_ rect: CGRect) {
    // self.createRectangle()    
    // self.createTriangle()

    // Create an oval shape path.
    self.path = UIBezierPath(ovalIn: self.bounds)

    ...
}

效果如圖所示:

貝塞爾 Layer 入門指南_第7张图片

改變 frame 參數,我們可以繪製出不同尺寸的橢圓。當然,我們不需要一直使用 UIView 的 bounds 作為 frame 參數,你可以提供小於 UIView 大小的 frame 作為參數。

你可能奇怪怎樣將橢圓轉變成正圓。請看這裡:

override func draw(_ rect: CGRect) {
    // self.createRectangle()    
    // self.createTriangle()

    // 構建橢圓形路徑
    //self.path = UIBezierPath(ovalIn: self.bounds)
    self.path = UIBezierPath(ovalIn: CGRect(x: self.frame.size.width/2 - self.frame.size.height/2,
                                            y: 0.0,
                                            width: self.frame.size.height,
                                            height: self.frame.size.height))

    ...
}

貝塞爾 Layer 入門指南_第8张图片

注意,新的橢圓寬高是相等的(都使用了 UIView 的高),這就畫出了正圓。同時,圓心被設置為 UIView 的中心。

圓角矩形

iOS 開發者都知道的,將一個 UIView 設置為圓角只需要使用這一句:

view.layer.cornerRadius = 15.0

這是設置圓角的最簡單方法,但不幸的是,如果你在 UIView 的最上層揮著貝塞爾路徑的話,上面的方法就無效了。當然,這也有解決辦法。你需要創建一個圓角矩形的路徑,像這樣:

override func draw(_ rect: CGRect) {
    ...

    path = UIBezierPath(roundedRect: self.bounds, cornerRadius: 15.0)

    ...
}

這個初始化方法創建了一個矩形路徑,并根據你在第二個參數中指定的值設置圓角。運行 App 會是這個樣子:

貝塞爾 Layer 入門指南_第9张图片

上面的程式碼非常有用,但如果我們想只對矩形的部分角進行圓角怎麼辦?通常的辦法是創建一個自定義的 UIView ,其中有 1 個或多個角是圓角,貝塞爾路徑讓這個問題變得簡單。我們來看一個只對左上角和右下角進行圓角的例子:

override func draw(_ rect: CGRect) {
    ...

    path = UIBezierPath(roundedRect: self.bounds,
                        byRoundingCorners: [.topLeft, .bottomRight],
                        cornerRadii: CGSize(width: 15.0, height: 0.0))

    ...
}

這個新出現的初始化函式有 3 個參數:

  1. 矩形的 frame。
  2. 指定哪些角是圓角。如果只有一個角,不需要使用數組參數,如果超過一個角,就需要像上面這種寫法了。這些值都是 UIRectCorner 結構的屬性,你可以參考這裡。
  3. 圓角半徑。這個參數是 CGSize 類型,但實際上只有 width 有效,height 是無意義的。

運行 demo App,你會看到:

貝塞爾 Layer 入門指南_第10张图片

不錯,我們只需要初始化一個對象就能夠繪製部分圓角的矩形了!

對於許多開發者來說,創建弧形的貝塞爾路徑遠比前面見過的要複雜。它確實比較複雜,但如果你理解它的工作原理就不一樣了!相信我,這並不難。

首先看一個例子,我們會一步一步來。先看這個圖:

貝塞爾 Layer 入門指南_第11张图片

畫出這個弧的程式碼是個:

override func draw(_ rect: CGRect) {
    ...

    path = UIBezierPath(arcCenter: CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2),
                        radius: self.frame.size.height/2,
                        startAngle: CGFloat(180.0).toRadians(),
                        endAngle: CGFloat(0.0).toRadians(),
                        clockwise: true)

    ...
}

讓我們來一個個看看這些參數:

  1. arcCenter: 弧是不完整的圓,如果你在腦海中將缺失的部分補齊構成一個完整的圓,你會發現它有圓心。補足后的圓的圓心就是弧的圓心。上面的例子中,我們想讓整個圓都包含在 UIView 中,因此它的圓心應當位於 UIView 的中心。這個參數是一個 CGPoint。
  2. radius: 圓的半徑。因為整圓的直徑等於 UIView 的高,而半徑就是它的一半。
  3. startAngle: 弧繪製時的“起点” 。稍後我們會和 endAngle 一起細說,現在我們可以認為是弧在整圓开始绘制的位置。这个值的d單位是弧度(π),而不是度(°)。關於你看到的這段程式碼,我後面還會詳細解釋。
  4. endAngle: 弧繪製時的“終點”。和起點一樣,你可以把它想成是弧的終點在整圓上的位置。
  5. clockwise: 這是一個布爾值,表示弧繪製的方向是順時針還是反時針。這個屬性和前兩個屬性結合,才能畫出準確的弧。

再來討論一下 startAngleendAngle 的問題。要理解弧的起點和終點,請回憶一下你曾經念過的幾何課本。

在回憶的同時,順便看一眼上面的截圖,圓從 0 度開始按反時針方向進行繪製,直到 360 度形成一個完整的圓。同理,弧就不難繪製了;你可以從任意角度 A 開始繪製,到 B 角度結束。你覺得呢?我也以為应该是這樣的,但是我們還有一個訣竅,可以完全不吃這一套!

在 iOS 中,有許多東西完全顛覆我們的常見的數學理論。比如坐標系統:在 iOS 中 y 軸是向下遞增的,但在笛卡爾坐標系(數學)則是遞減的。這個問題在上面看到的度數圓中同樣存在。在 iOS 中圓從 0 度開始(上圖的右邊),但是沿順時針方向畫圓直到 360 度:

貝塞爾 Layer 入門指南_第12张图片

[ecko_alert color=”gray”]Note: The above image is courtesy of tillnagel.com[/ecko_alert]

參考上圖,你可以認為我們的弧是從 180 度開始,到 0 度結束,繪製方向為順時針,這樣就會得到屏幕截圖的結果。

為了更能夠說清問題,再來看另一個例子:

貝塞爾 Layer 入門指南_第13张图片

有兩種方法實現這個效果:

  • 參考上圖的角度坐標,要麼以順時針方向從 90 開始到 270度結束,
  • 要麼以反時針方向,從 270 度到90 度結束。

假設以第二種方式的反時針方向:

override func draw(_ rect: CGRect) {
    ...

    path = UIBezierPath(arcCenter: CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2),
                        radius: self.frame.size.height/2,
                        startAngle: CGFloat(270.0).toRadians(),
                        endAngle: CGFloat(90.0).toRadians(),
                        clockwise: false)

    ...
}

最後一個例子,能充分說明問題:

override func draw(_ rect: CGRect) {
    ...

    path = UIBezierPath(arcCenter: CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2),
                        radius: self.frame.size.height/2,
                        startAngle: CGFloat(240.0).toRadians(),
                        endAngle: CGFloat(15.0).toRadians(),
                        clockwise: false)

    ...
}

結果如下:

貝塞爾 Layer 入門指南_第14张图片

關於開始角度和截止角度,最後再嘮叨兩句。我們知道角度要麼使用度,要麼使用弧度。上面的初始化方法使用弧度,但人類更傾向於度。因此,我提供一個 CGFloat 擴展在二者之間進行轉換,在前面上個例子中都用到了:

extension CGFloat {
    func toRadians() -> CGFloat {
        return self * CGFloat(M_PI) / 180.0
    }
}

通過這個擴展,你可以輕鬆地將度轉換為弧度,就像你在上面的例子中看到的 startAngle 參數和 endAngle 參數。

貝塞爾路徑和 CAShapeLayer

正如我們在前面的例子中所見,UIView 子類的 draw(_:) 方法為貝塞爾路徑的繪製提供了一個上下文對象。但是,自定義繪製並不一定都要重寫這個方法,實際上我們應該盡量避免重寫這個方法,它會對性能造成較大的影響。一種更好的做法是使用 CAShapeLayer 對象,它的渲染速度更快而且使用更加靈活。

我們在本文的開頭說過, CAShapeLayer 是 CALayer 的子類,創建和使用這個對象實際上會在 UIView 的 layer 上添加新的 layer。它有幾個屬性可以定制 UIView 的最終顯示,這些屬性大部分是可動畫的,也就是說它們的值能夠以動畫方式改變(例如使用 CABasicAnimation 方式)。關於 layer 的動畫,內容較多,本文不會涉及,它可能需要用另外一篇單獨的教程來介紹了。

在創建 CAShapeLayer 對象時,必須指定它的形狀,也就是路徑。要設置它路徑的最簡單方式是先創建一個貝塞爾路徑,然後將它賦給 CAShapeLayer。這是最常用的方法,我們會在后面的例子中使用這種方法。

除了指定路徑,還有另幾个屬性需要設置。例如,填充的顏色、筆劃的顏色、筆劃的粗細以及位置,這些屬性我們會在後面看到。CAShapeLayer 能以兩種方式使用到 UIView 的 layer 中:sublayer (子圖層)或者 mask(遮罩層)。稍後我們會討論二者有何區別。然後,可以將多個 CAShapeLayer 添加到一個 layer 的子圖層中。當然,上面的圖層會遮在下面的圖層上,我們使用時要尤其小心它們的位置和大小。

簡單的 CAShapeLayer

我們來創建第一個 CAShapeLayer,在此之前,先將整個 draw(_:) 方法注釋掉。可以這樣創建出一個最簡單的 CAShapeLayer:

func simpleShapeLayer() {
    self.createRectangle()

    let shapeLayer = CAShapeLayer()
    shapeLayer.path = self.path.cgPath

    self.layer.addSublayer(shapeLayer)
}

首先,調用前面編寫好的 createRectangle() 方法來畫出一個矩形貝塞爾路徑。記住,這個方法所創建的路徑是保存在類的 path 屬性中的。

然後簡單地創建一個形狀圖層。首先初始化,然後將它的 path 屬性設置為對應的貝塞爾路徑。注意,path 屬性是 CGPath 類型,我們需要訪問 UIBezierPath 的 cgPath 屬性以獲得它的 CGPath。

最後,將形狀圖層添加進視圖 layer 的子圖層中。通常這是添加子圖層的最普通方式,但實際上你也可以用下列 CALayer 類中提供的方法來添加子圖層:

insertSublayer(_:at:)
insertSublayer(_:above:)
insertSublayer(_:below:)

請參考這裡、這裡 以及 這裡 。

為了看到效果,我們還需要調用這個方法(注意,我們將 UIView 的背景色由透明修改成了暗灰色):

override init(frame: CGRect) {
    super.init(frame: frame)

    self.backgroundColor = UIColor.darkGray

    simpleShapeLayer()    
}

貝塞爾 Layer 入門指南_第15张图片

看到了吧,如果我們不指定形狀圖層的填充顏色,默認將填充為黑色。讓我們來設置一下填充色,以及其它的幾個屬性:筆劃顏色和筆畫的粗細:

func simpleShapeLayer() {
    ...

    shapeLayer.fillColor = UIColor.yellow.cgColor
    shapeLayer.strokeColor = UIColor.brown.cgColor
    shapeLayer.lineWidth = 3.0

    self.layer.addSublayer(shapeLayer)
}

注意 fillColor 和 stokeColor 屬性是 CGColor 類型,所以我們需要用 cgColor 將 UIColor 轉換成 CGColor。lineWidth 屬性允許我們指定邊線的粗細。

貝塞爾 Layer 入門指南_第16张图片

遮罩層和子圖層

形狀圖層可以用兩種方式來使用:充當 UIView 的默認 layer 的遮罩層,或者子圖層(就像我們前面所做的)。兩者到底有何不同,我們到底應該使用哪一種?

再來看一個例子,添加一個新方法:

func maskVsSublayer() {
    self.createTriangle()

    let shapeLayer = CAShapeLayer()
    shapeLayer.path = path.cgPath
    shapeLayer.fillColor = UIColor.yellow.cgColor

    self.layer.addSublayer(shapeLayer)
}

這裡,為了不分散我們的注意力,我們直接調用了之前寫過的 createTriangle() 方法。 這個方法繪製一個三角形路徑,我們創建了一個新的形狀圖層,使用了這個路徑。我們還指定了填充色,然後添加到 UIView 默認 layer 的子圖層中。這一切都很熟悉。

然後我們來調用這個新方法,然後看看結果(結果和我們想象的一樣):

override init(frame: CGRect) {
    ...

    maskVsSublayer()
}

貝塞爾 Layer 入門指南_第17张图片

看到了麼,我們的新圖層是一個黃色三角,它位於 UIView 的中心,沒有被遮住的部分則顯示出暗灰色。

現在來修改一下新方法:

func maskVsSublayer() {
    ...

    // self.layer.addSublayer(shapeLayer)   

    self.layer.mask = shapeLayer
}

addSublayer(_:) 一句注釋,替換成設置 mask 屬性。 運行 App 查看效果:

貝塞爾 Layer 入門指南_第18张图片

這次只顯示了三角形,但它不是我們設定的黃色,此外,沒有該路徑覆蓋住的部分不會被顯示了!

這就是遮罩。UIView 中不屬於這個路徑中的內容會被裁切掉,UIView 只會顯示遮罩圖層以內的內容。而且在遮罩圖層中,填充顏色之類的屬性是沒有作用的,如果我們想改變三角形的顏色,我們必須修改 UIView 的背景色:

func maskVsSublayer() {
    ...

    // self.layer.addSublayer(shapeLayer)

    self.backgroundColor = UIColor.green
    self.layer.mask = shapeLayer
}

貝塞爾 Layer 入門指南_第19张图片

換句話說,如果你想讓你的 UIView 顯示成自己定義的外形,就可以用 mask 屬性,否則請創建新的形狀圖層并添加到 sublayer 中,然後修改 UIView 和 layer 的屬性實現專輯的效果。

更多的子圖層

我也說過,你可以在 UIView 的 layer 中添加不止一個形狀圖層,現在讓我們來看一下。在這一部分,我們會創建兩個子圖層,然後修改幾個屬性,以便正確地顯示在 UIView 上。首先,我們要創建兩個路徑和兩個形狀:

func twoShapes() {
    let width: CGFloat = self.frame.size.width/2
    let height: CGFloat = self.frame.size.height/2

    let path1 = UIBezierPath()
    path1.move(to: CGPoint(x: width/2, y: 0.0))
    path1.addLine(to: CGPoint(x: 0.0, y: height))
    path1.addLine(to: CGPoint(x: width, y: height))
    path1.close()

    let path2 = UIBezierPath()
    path2.move(to: CGPoint(x: width/2, y: height))
    path2.addLine(to: CGPoint(x: 0.0, y: 0.0))
    path2.addLine(to: CGPoint(x: width, y: 0.0))
    path2.close()

    let shapeLayer1 = CAShapeLayer()
    shapeLayer1.path = path1.cgPath
    shapeLayer1.fillColor = UIColor.yellow.cgColor

    let shapeLayer2 = CAShapeLayer()
    shapeLayer2.path = path2.cgPath
    shapeLayer2.fillColor = UIColor.green.cgColor

    self.layer.addSublayer(shapeLayer1)
    self.layer.addSublayer(shapeLayer2)    
}

上面語句繪製了兩個三角形的路徑,第一個的“尖角”向上,第二個的“尖角”向下。這次,我們沒有用 UIView 的整個高度和寬度,而是只用了它們的一半。創建好兩個路徑后,將它們賦給兩個 CAShapeLayer 對象,然後將形狀圖層添加到 UIView 的 sublayer 中。

然後調用這個新方法:

override init(frame: CGRect) {
    ...

    twoShapes()
}

運行 App,你會看到:

貝塞爾 Layer 入門指南_第20张图片

看到了吧,第二個形狀圖層蓋在了第一個上面,這顯然是不對的。那麼,我們該怎樣修改它呢?

func twoShapes() {
    ...

    shapeLayer2.position = CGPoint(x: 0.0, y: height)    
}

貝塞爾 Layer 入門指南_第21张图片

這就好多了!position 屬性很重要吧,它允許你“移動”形狀圖層。那麼怎樣將它們放在 UIView 正中呢?將上一句修改為:

func twoShapes() {
    ...

    // shapeLayer2.position = CGPoint(x: 0.0, y: height)

    shapeLayer2.position = CGPoint(x: width/2, y: height)
    shapeLayer1.position = CGPoint(x: width/2, y: 0.0)

}

這會產生下面的效果:

貝塞爾 Layer 入門指南_第22张图片

大部分時候,用 position 屬性移動形狀圖層就足夠了。但是,有時候我們還想修改圖層的 frame。但這時你會發現無法訪問 frame 屬性,只有一個 bounds 屬性。

根據文檔,bounds 屬性使用它自己的坐標系統來表示 sublayer 的原點和大小。要明白這是什麼意義,如何才能修改 bounds 的原點和大小,我們可以修改上面的例子:

func twoShapes() {
    ...

    shapeLayer1.bounds.origin = CGPoint(x: 0.0, y: 0.0)
    shapeLayer1.bounds.size = CGSize(width: 200.0, height: 150.0)
    shapeLayer1.backgroundColor = UIColor.magenta.cgColor
}

貝塞爾 Layer 入門指南_第23张图片

上面的代碼中,我們改變了 sublayer 的默認大小,讓它比 UIView 的可視化區域還大。路徑的大小不受任何影響,但我們改變路徑的大小后路徑的位置會移動:

func twoShapes() {
    ...

    shapeLayer1.bounds.origin = CGPoint(x: -20.0, y: -40.0)

    ...
}

貝塞爾 Layer 入門指南_第24张图片

注意:和 UIView 中常用的坐標系不同,我們要將 UIView 向右移動需要增加圓點的 x 值,向下移動需要增加 y 值,它恰恰相反。要將路徑向右、向下移,需要提供負數。正數會將路徑的原點向左向上移動。當然,大部分時候我們只需要移動 layer 就看可以了,很少會對 bounds 進行修改。

組合路徑

在本文開頭介紹路徑的時候,我們列出了幾個 UIBezierPath 類的初始化方法,通過這些初始化函數,我們可以創建多種類型的形狀。但是,我們還從來沒有看到過將幾種不同的路徑組合到一起的例子,接下來我們可以展示如何將它們組合到一起了。

addLine(to:) 方法一樣,我們已經知道如何在兩點間劃線,以及畫出其它形狀比如弧和曲線。

來看個例子:

func complexShape() {
    path = UIBezierPath()
    path.move(to: CGPoint(x: 0.0, y: 0.0))
    path.addLine(to: CGPoint(x: self.frame.size.width/2 - 50.0, y: 0.0))
    path.addArc(withCenter: CGPoint(x: self.frame.size.width/2 - 25.0, y: 25.0),
                radius: 25.0,
                startAngle: CGFloat(180.0).toRadians(),
                endAngle: CGFloat(0.0).toRadians(),
                clockwise: false)
    path.addLine(to: CGPoint(x: self.frame.size.width/2, y: 0.0))
    path.addLine(to: CGPoint(x: self.frame.size.width - 50.0, y: 0.0))
    path.addCurve(to: CGPoint(x: self.frame.size.width, y: 50.0),
                  controlPoint1: CGPoint(x: self.frame.size.width + 50.0, y: 25.0),
                  controlPoint2: CGPoint(x: self.frame.size.width - 150.0, y: 50.0))
    path.addLine(to: CGPoint(x: self.frame.size.width, y: self.frame.size.height))
    path.addLine(to: CGPoint(x: 0.0, y: self.frame.size.height))
    path.close()

    let shapeLayer = CAShapeLayer()
    shapeLayer.path = path.cgPath

    self.backgroundColor = UIColor.orange
    self.layer.mask = shapeLayer
}

然後在 init(frame:) 方法中調用上述方法:

override init(frame: CGRect) {
    ...

    complexShape()
}

運行 App,你會看到:

貝塞爾 Layer 入門指南_第25张图片

首先注意這段程式碼中將形狀圖層用做遮罩圖層而不是子圖層。這是我故意的,因為我想讓最後產生的圖形更加顯眼。當然,路徑的創建才是重點。你會看到,我們組合了不同的線條:直線、曲線和弧線。起點是左上角(0.0,0.0),按照順時針方向繪製其它部分。仔細體會每個點的位置,以及繪製路徑的每一個步驟。

上述語句中出現了一個新方法 addCurve ,它會畫出圖形中右邊的曲線。所謂的曲線是這樣一種圖形:

貝塞爾 Layer 入門指南_第26张图片

[ecko_alert color=”gray”]注意: 上圖來自https://developer.apple.com/reference/uikit/uibezierpath/1624357-addcurve.[/ecko_alert]

這個方法需要 3 個參數:曲線的終點(endpoint),以及兩個用於指定曲線曲率的控制點。你可以修改兩個控制點的值,看看曲線會如何變化。關於曲線,實話說(痛苦萬分)後面有一大套的數學理論,你可以試著讀一下這裡。

關於弧線的繪製,根本不值一提。你可以回憶一下我們講過的繪製弧的內容,我們完全是“照本宣科”而已。其它線段,也全都是你學過的內容。

補充材料: 使用 CATextLayer

還有一種經典工具我們知道的比較少,或者用得比較少,這就是 CATextLayer 類。它的主要功能是創建顯示某段文字的圖層(和 CAShapeLayer 差不多)。儘管這個類和貝塞爾路徑和形狀沒有任何關係,但我還是想在這裡介紹它,因此我把它作為補充材料列出。你可以把它看成本文的一個小小插曲。

每個開發者都在需要顯示文本的時候 99% 都會用到 UILabel 對象(針對不能修改的文本)。但有時候我們無法使用 UILabel,或者需要將多個 sublayer 添加到包含有 UILabel 的 UIView 中時很容易出錯,這時 CATextLayer 類就有用了。另外一個好處是,你可以將文字圖層添加在任何 UIView 的上面,因此我們并不只有 UILabel 一個選擇。

創建和使用 CATextlayer 的關鍵是根據文本需要顯示的方式設置一個屬性集。下面的程式碼創建了一個文本圖層,并添加到 UIView 的 layer 的 sublayer。瀏覽一下代碼,查看運行效果,按你的想法進行修改,以熟悉它。

func createTextLayer() {
    let textLayer = CATextLayer()
    textLayer.string = "WOW!\nThis is text on a layer!"
    textLayer.foregroundColor = UIColor.white.cgColor
    textLayer.font = UIFont(name: "Avenir", size: 15.0)
    textLayer.fontSize = 15.0
    textLayer.alignmentMode = kCAAlignmentCenter
    textLayer.backgroundColor = UIColor.orange.cgColor
    textLayer.frame = CGRect(x: 0.0, y: self.frame.size.height/2 - 20.0, width: self.frame.size.width, height: 40.0)
    textLayer.contentsScale = UIScreen.main.scale
    self.layer.addSublayer(textLayer)
}

有幾個地方需要注意:

  • 用 `textLayer` 對象的 string 屬性來設置要顯示的文本。
  • 我們已經用字體名和字大來獲得餓一個 UIFont,但仍然需要用 `fontSize` 屬性來指定字體大小。好像初始化 UIFont 時指定的 size 根本沒有生效。
  • Text 對齊方式可以使用 `textAlignmentMode` 屬性。你可以設置幾個指定的值,如果使用 `kCAAlignmentCenter` 表示文字居中對齊。其它值請看[這裡](https://developer.apple.com/reference/quartzcore/catextlayer/horizontal_alignment_modes)。
  • 不像 UILabel,這裡無法設置文本的行數。但是我們可以用 “\n” 符號來在我們的文本中添加換行。記住,要正確顯示文本,layer 的 frame 必須設置正確。
  • 非常重要: `contentScale` 屬性必須設置,只有參考屏幕分辨率才能正確繪製出文本。如果不設置這個屬性,可能會導致顆粒化效果。

然後調用這個方法:

override init(frame: CGRect) {
    ...

    createTextLayer()
}

運行效果如下:

貝塞爾 Layer 入門指南_第27张图片

更多細節,請參考官方文檔。

總結

本文即將結束,我希望前面的內容對大家有用。通過對這些基本內容的掌握,你可以逐漸學會將自定義視圖或自定義形狀圖層和複雜路徑結合在一起實現更高級的效果。當然,你應該知道,如何使用你在本文學習到的知識完全取決於你,因此充分發揮這些知識的威力吧!

你可能感兴趣的:(iPhone开发)