Core Graphic 指南:圓弧與路徑

知識小集發表於2019-03-04

| 作者:Lorenzo Boaro

| 連結:https://www.raywenderlich.com/349664-core-graphics-tutorial-arcs-and-paths

| 公眾號:https://mp.weixin.qq.com/s/hhF7hO5xQWlYpqa7cg2zfg

在本教程中,我們將學習如何繪製圓弧和路徑。特別是,我們將 Grouped TableView 的每個頁尾的底部新增整齊的弧線、線性漸變和適合弧形曲線的陰影,來美化我們的 table view。所有這些都是通過使用 Core Graphics 的強大功能實現的!

開始

在本教程中,我們將使用 LearningAgenda 示例程式,這是一個 iOS 應用程式,演示了我們要學習的內容。

首先下載初始工程(https://koenig-media.raywenderlich.com/uploads/2019/01/LearningAgenda-2.zip)。下載後,在 Xcode 中開啟LearningAgenda.xcodeproj

為了讓我們更專注於主要內容,初始專案已設定好了與圓弧和路徑無關的所有內容。

構建並執行應用程式,我們將看到以下介面:

Core Graphic 指南:圓弧與路徑

如圖所示,有一個分組的 table view,包含兩個部分,每個部分都有一個標題和三行內容。我們在這裡要做的所有工作就是在每個部分下方建立弧形頁尾。

增強頁尾

在實現功能之前,我們需要建立並設定一個自定義的頁尾,這將作為我們後續工作的基礎。

要為閃亮的新頁尾建立類,可以右鍵單擊 LearningAgenda 資料夾,然後選擇“新建檔案”。 接下來,選擇 Swift File 並將檔案命名為 CustomFooter.swift

切換到 CustomFooter.swift 檔案並使用以下程式碼替換其內容:

import UIKit

class CustomFooter: UIView {
  override init(frame: CGRect) {
    super.init(frame: frame)
    
    isOpaque = true
    backgroundColor = .clear
  }
  
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override func draw(_ rect: CGRect) {
    let context = UIGraphicsGetCurrentContext()!
    
    UIColor.red.setFill()
    context.fill(bounds)
  }
}
複製程式碼

這裡,我們重寫 init(frame:) 以設定 isOpaquetrue。我們還將背景顏色設定為 clear

注意:當檢視完全或部分透明時,不應使用 isOpaque 屬性。否則,結果可能無法預測。

我們同樣重寫 init?(coder:),因為它是必需的,但是我們不提供任何實現,因為我們不會在 Interface Builder 中使用自定義頁尾檢視。

draw(_:) 使用 Core Graphics 提供自定義 rect 的內容。我們將紅色設定為填充顏色以覆蓋頁尾本身的整個 bounds。

現在,開啟 TutorialsViewController.swift 並將以下兩個方法新增到檔案底部的 UITableViewDelegate 擴充套件中:

func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
  return 30
}
  
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
  return CustomFooter()
}
複製程式碼

上述方法組合形成 30 個 point 高度的自定義頁尾檢視。

構建並執行專案,如果一切正常,我們應該看到以下內容:

Core Graphic 指南:圓弧與路徑

回到業務

好了,既然我們已經有了一個佔位符檢視,那麼現在是時候了。但首先我們來設定一個目標。

Core Graphic 指南:圓弧與路徑

上圖有以下幾點可以注意一下:

  • 頁尾檢視底部是一個整齊的弧形;
  • 從淺灰色漸變到深灰色;
  • 陰影與弧形曲線一致

圓弧後面的數學

圓弧是表示圓的一部分的曲線。在上面這種情況下,頁尾檢視底部所需的圓弧是一個非常大的圓的頂部,具有非常大的半徑,從某個起始角度到某個結束角度。

Core Graphic 指南:圓弧與路徑

那我們如何向 Core Graphics 描述這個弧?我們將使用 CGContextaddArc(center:radius:startAngle:endAngle:clockwise:) 方法。該方法需要以下五個輸入引數:

  • 圓的中心點
  • 圓的半徑
  • 繪製線的起點,也稱為起始角度
  • 繪製線的終點,也稱為結束角度
  • 圓弧的方向

但是我們又該如何去設定這些值呢?

Core Graphic 指南:圓弧與路徑

我們需要一些簡單的數學知識,並計算出所有這些值!

我們知道的第一件事是想要繪製弧的邊界框的大小:

Core Graphic 指南:圓弧與路徑

我們知道的第二件事是一個有趣的數學定理,稱為相交和絃定理。基本上,這個定理指出,如果你在一個圓中繪製兩個交叉的和絃,第一個和絃的分段的乘積將等於第二個和絃的分段的乘積。請記住,和絃是連線圓中兩個點的線。

Core Graphic 指南:圓弧與路徑

注意:如果我們想了解其原因,請訪問 http://www.mathopenref.com/chordsintersecting.html - 它有一個很酷的小型 JavaScript 演示,我們可以直接使用。

有了這兩點知識,看看如果我們畫出如下兩個和絃時會發生什麼:

Core Graphic 指南:圓弧與路徑

因此,繪製一條線連線弧形矩形的底點和從弧形頂部向下到圓形底部的另一條線。

如果我們這樣做,知道了 abc,就可以得出 d

所以 d 的計算公式是:(a * b) / c。用它代替,會是:

// Just substituting...
let d = ((arcRectWidth / 2) * (arcRectWidth / 2)) / (arcRectHeight);
// Or more simply...
let d = pow(arcRectWidth, 2) / (4 * arcRectHeight);
複製程式碼

現在我們知道了 cd,就可以使用以下公式計算半徑:(c + d) / 2

// Just substituting...
let radius = (arcRectHeight + (pow(arcRectWidth, 2) / (4 * arcRectHeight))) / 2;
// Or more simply...
let radius = (arcRectHeight / 2) + (pow(arcRectWidth, 2) / (8 * arcRectHeight));
複製程式碼

現在我們已經知道了半徑,只需從陰影矩形的中心點減去半徑即可獲得中心:

let arcCenter = CGPoint(arcRectTopMiddleX, arcRectTopMiddleY - radius)
複製程式碼

一旦知道了中心點,半徑和圓弧矩形,就可以用一些三角函式計算起點和終點角度:

Core Graphic 指南:圓弧與路徑

我們首先計算出圖中所示的角度。如果我們還記得 SOHCAHTOA(https://en.wikipedia.org/wiki/Mnemonics_in_trigonometry#SOH-CAH-TOA),可能會想起角度的餘弦等於三角形相鄰邊緣的長度除以斜邊的長度。

換句話說,cosine(angle) = (arcRectWidth / 2) / radius。因此,為了得到角度,我們只需要取餘弦,它是餘弦的倒數:

let angle = acos((arcRectWidth / 2) / radius)
複製程式碼

現在我們知道了這個角度,獲得起點和終點角度應該相當簡單:

Core Graphic 指南:圓弧與路徑

現在我們瞭解瞭如何去做,就可以將它們寫到一個函式裡去了。

注意:順便說一句,使用 CGContext 型別中提供的 addArc(tangent1End:tangent2End:radius:) 方法,可以更簡單地繪製這樣的弧。

繪製圓弧和建立路徑

我們所做的第一件事是將度數轉換為弧度的方法。為此,將使用在 iOS 10 和 macOS 10.12 中引入的 Foundation Units 和 Measurements API。

Foundation 框架提供了一種使用和表示物理量的強大方法。除角度外,它還提供了幾種內建單元型別,如速度,持續時間等。

開啟 Extensions.swift 並將以下程式碼貼上到檔案末尾:

typealias Angle = Measurement<UnitAngle>

extension Measurement where UnitType == UnitAngle {  
  init(degrees: Double) {
    self.init(value: degrees, unit: .degrees)
  }

  func toRadians() -> Double {
    return converted(to: .radians).value
  }
}
複製程式碼

在上面的程式碼中,我們可以在 Measurement 型別上定義一個擴充套件,將其用法限制為角度單位。 init(degrees:) 僅適用於度數角度。toRadians() 允許我們將度數轉換為弧度。

注意:也可以使用公式 radians = degrees * π / 180 將度數轉換為弧度,反之亦然。

保留在 Extensions.swift 檔案中,找到 CGContext 的擴充套件塊。在最後一個花括號之前,貼上以下程式碼:

static func createArcPathFromBottom(
  of rect: CGRect, 
  arcHeight: CGFloat, 
  startAngle: Angle, 
  endAngle: Angle
) -> CGPath {
  // 1
  let arcRect = CGRect(
    x: rect.origin.x, 
    y: rect.origin.y + rect.height, 
    width: rect.width, 
    height: arcHeight)
  
  // 2
  let arcRadius = (arcRect.height / 2) + pow(arcRect.width, 2) / (8 * arcRect.height)
  let arcCenter = CGPoint(
    x: arcRect.origin.x + arcRect.width / 2, 
    y: arcRect.origin.y + arcRadius)    
  let angle = acos(arcRect.width / (2 * arcRadius))
  let startAngle = CGFloat(startAngle.toRadians()) + angle
  let endAngle = CGFloat(endAngle.toRadians()) - angle
  
  let path = CGMutablePath()
  // 3
  path.addArc(
    center: arcCenter, 
    radius: arcRadius, 
    startAngle: startAngle, 
    endAngle: endAngle, 
    clockwise: false)
  path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
  path.addLine(to: CGPoint(x: rect.minX, y: rect.minY))
  path.addLine(to: CGPoint(x: rect.minY, y: rect.maxY))
  // 4
  return path.copy()!
}
複製程式碼

到這已經進展了不少,以下是具體描述:

  • 此函式使用整個區域的矩形和弧度應該有多大的浮點數。請記住,圓弧應位於矩形的底部。我們可以根據這兩個值計算 arcRect
  • 然後,通過上面討論的數學公式計算出半徑,中心,起點和終點角度。
  • 接下來,建立路徑。路徑將由圓弧和弧上方矩形邊緣周圍的線組成。
  • 最後,返回路徑的不可變副本。我們不希望從函式外部修改路徑。

注意:與 CGContext 擴充套件中可用的其他函式不同,createArcPathFromBottom(of:arcHeight:startAngle:endAngle:) 返回 CGPath。這是因為路徑將被重複使用多次。稍後會詳細介紹。

現在我們有了一個輔助方法來繪製弧線,現在是時候用我們的新弧形替換你的矩形頁尾檢視了。

開啟 CustomFooter.swift 並使用以下程式碼替換 draw(_:)

override func draw(_ rect: CGRect) { 
  let context = UIGraphicsGetCurrentContext()!
  
  let footerRect = CGRect(
    x: bounds.origin.x, 
    y: bounds.origin.y, 
    width: bounds.width, 
    height: bounds.height)
  
  var arcRect = footerRect
  arcRect.size.height = 8
  
  context.saveGState()
  let arcPath = CGContext.createArcPathFromBottom(
    of: arcRect, 
    arcHeight: 4, 
    startAngle: Angle(degrees: 180), 
    endAngle: Angle(degrees: 360))
  context.addPath(arcPath)
  context.clip()

  context.drawLinearGradient(
    rect: footerRect, 
    startColor: .rwLightGray, 
    endColor: .rwDarkGray)
  context.restoreGState()
}
複製程式碼

在通常的 Core Graphics 設定之後,我們將為整個頁尾檢視區域和想要圓弧的區域建立一個邊界框。

然後,通過呼叫剛剛編寫的 createArcPathFromBottom(of:arcHeight:startAngle:endAngle:) 靜態方法獲得弧形路徑。然後,我們可以將路徑新增到上下文並剪下到該路徑。

後續進一步的繪圖將限於該路徑。然後,可以使用 Extensions.swift 中的 drawLinearGradient(rect:startColor:endColor:) 繪製從淺灰色到深灰色的漸變。

構建並執行應用程式。如果一切正常,我們應該看到以下介面:

Core Graphic 指南:圓弧與路徑

看起來不錯,但我們需要再完善一下。

裁剪,路徑和偶數規則

CustomFooter.swift 中,將以下內容新增到 draw(_:) 的底部:

context.addRect(footerRect)
context.addPath(arcPath)
context.clip(using: .evenOdd)
context.addPath(arcPath)
context.setShadow(
  offset: CGSize(width: 0, height: 2), 
  blur: 3, 
  color: UIColor.rwShadow.cgColor)
context.fillPath()
複製程式碼

這裡有一個新的,非常重要的概念。

要繪製陰影,請啟用陰影繪製,然後填充路徑。然後 Core Graphics 將填充路徑並在下方繪製適當的陰影。

但是我們已經使用漸變填充了路徑,因此並不希望用顏色覆蓋該區域。

嗯,這聽起來像裁剪工作!我們可以設定裁剪,以便 Core Graphics 僅繪製頁尾區域外部分。然後,我們可以告訴它填充頁尾區域並繪製陰影。由於設定了裁剪,頁尾區域填充將被忽略,但陰影將顯示。

但是我們沒有這樣一條路徑 - 唯一的路徑是頁尾區域而不是外部區域。

使用 Core Graphics 的一些功能,我們可以輕鬆地根據內部獲取外部路徑。我們只需向上下文新增多個路徑,然後使用 Core Graphics 提供的特定規則新增裁剪。

當我們向上下文新增多個路徑時,Core Graphics 需要某種方式來確定是否應該填充哪些點。例如,你可以有一個圓圈形狀,其中外部是填充但內部是空的,或者是圓環形狀,其中內部填充但外部是空的。

我們可以指定不同的演算法讓 Core Graphics 知道如何處理它。本教程中將使用的演算法是 EO,甚至是 even-odd

在 EO 中,對於任何給定點,Core Graphics 將從該點繪製一條線到繪圖區域的外部。如果該線穿過奇數個點,它將被填充。如果它穿過偶數個點,則不會被填充。

以下是 Quartz2D Programming Guide 中的圖示:

Core Graphic 指南:圓弧與路徑

因此,通過使用 EO 變體,我們告訴 Core Graphics,即使已經向上下文新增了兩條路徑,它也應該將其視為遵循 EO 規則的一條路徑。因此,外部部分,即整個頁尾矩形,應該被填充,但內部部分,即弧形路徑則不應該。我們告訴 Core Graphics 剪下到該路徑並僅在外部區域繪製。

設定裁剪區域後,新增弧的路徑,設定陰影並填充圓弧。當然,由於它被剪裁,實際上什麼都沒有被填充,但陰影仍將被繪製在外部區域!

構建並執行專案,如果一切順利,我們現在應該看到頁尾下方的陰影:

Core Graphic 指南:圓弧與路徑

恭喜!我們已使用 Core Graphics 建立了自定義 table view 的頁尾檢視!

下一步去哪?

我們可以下載專案的完整版本。

通過本教程,我們已經學習瞭如何建立圓弧和路徑。現在,可以將這些概念直接應用到應用中!

如果您想了解有關 Core Graphics 的更多資訊,請檢視 Quartz 2D Programming Guide。

關注我們

歡迎關注我們的公眾號:iOS-Tips,也歡迎加入我們的群組討論問題。可以加微信 coldlight_hh/wsy9871 進入我們的 iOS/flutter 微信群。

Core Graphic 指南:圓弧與路徑

相關文章