Core Graphics 框架也稱爲 Quartz 2D,是基於 Quartz 的高級渲染引擎,它提供了底層輕量級 2D 渲染引擎,可以進行高保真輸出。
Quartz 2D 簡單易用,提供了強大的功能。例如,基於路徑的繪製,變換,顏色管理,離屏渲染,模式(pattern),漸變,陰影,圖像數據管理、圖像創建和mask,還可以處理 PDF 文檔的創建、渲染和解析。
這篇文章將創建一個demo,用來記錄每日喝了幾杯水,並且會用折線圖彙總顯示本週飲水量。
1. 在視圖上添加自定義繪製
創建一個名稱爲CoreGraphics-1
的 iOS app。添加一個UIView
,並在其中繪製自定義內容,步驟如下:
- 創建
UIView
子類。 - 重寫
draw(_:)
方法,添加 Core Graphics 繪製代碼。
先繪製一個如下的按鈕:
創建名稱爲PushButton
的類,其繼承自UIButton
。
這篇文章只介紹CoreGraphics部分,將忽略與此無關的部分。如果在此過程中遇到問題,可以在文章底部獲取源碼查看。
2. 繪製 Button
要在 Core Graphics 中繪製圖形,需定義一條路徑(path),用以告訴 Core Graphics 如何跟蹤繪製。例如,兩條垂直的線畫加號,填充圓畫圓形。
path有以下三個基礎知識:
- path可以被描邊(stroke)和填充(fill)。
- stroke使用當前的顏色勾勒出path。
- fill使用當前顏色填充閉合path。
使用UIBezierPath
可以很方便的創建 Core Graphics path。UIBezierPath
提供的 API 簡單易用,可以基於直線、曲線、矩形,或一系列點繪製 path。
首先,使用UIBezierPath
創建一個橢圓,並用綠色填充。打開PushButton.swift
文件,添加以下代碼:
override func draw(_ rect: CGRect) {
let path = UIBezierPath(ovalIn: rect)
UIColor.green.setFill()
path.fill()
}
UIBezierPath(ovalIn:)
根據參數矩形的大小創建橢圓。由於 path 不會自動繪製任何內容,因此可以在沒有繪製上下文(drawing context)的地方定義 path。想要繪製 path,在當前 context 設置 fill color 並填充 path。
運行後如下:
每個UIView
都有 graphics context。在傳輸到屏幕中之前,視圖所有繪製操作都在 graphics 上下文中進行。
當視圖需要重繪時,系統調用draw(_:)
方法。例如:
- 將視圖添加到屏幕中。
- 移動視圖上子視圖位置。
- 視圖的
isHidden
屬性發生改變。 - 視圖滑動出屏幕後,再次滑動到屏幕上。
- 手動調用了
setNeedsDisplay()
、setNeedsDisplayInRect()
。
系統提供的視圖會自動進行重繪。自定義的視圖需重寫draw(_:)
方法,在該方法中執行所有繪製代碼。視圖首次顯示時,傳遞給draw(_:)
方法的rect
參數爲視圖的所有可見區域。後續調用draw(_:)
方法時,只傳遞需重繪的rect。爲優化性能,應只重繪受影響區域。
調用draw(_:)
方法後,view會被標記爲已更新,等待新的修改操作,然後觸發下一個更新循環。如果想更新視圖內容,需調用setNeedsDisplay()
或setNeedsDisplay(_:)
方法觸發更新循環。draw(_:)
方法只能在重繪時,由系統調用。其它時間,graphics context 是不存在的。因此,不能手動調用draw(_:)
方法。
draw(_:)
中的所有繪製操作都會進入視圖的繪製上下文中。在draw(_:)
之外繪製,需單獨創建 graphics context。
UIKit
對 Core Graphics 的部分 API 進行了封裝。例如,UIBezierPath
是對CGMutablePath
的封裝。因此,到目前爲止還沒有涉及到 Core Graphics 相關 API。
3. 畫家模型 The Painter's Model
Core Graphics 使用一種被稱之爲「畫家模型」的方式工作。在畫家模型中,每個連續的繪圖操作將一個圖層繪製應用到畫布,通常被稱爲 page。通過添加額外的繪製可以覆蓋原來繪製的內容,達到修改原來繪製內容的目的。通過使用畫家模型,基於少量的基礎操作可以構建複雜的圖像。
下圖顯示了畫家模型如何工作。圖片中的上半部分,先繪製左側圖形、後繪製右側實心區域。右側圖形會覆蓋左側圖形,遮擋了左側圖形周邊以外的區域。下半部分以相反順序繪製,最終結果有所不同。在畫家模型中,繪製順序很重要。
Page 會根據輸出設備而變。如果輸出設備是打印機,則page是真正的紙;如果輸出設備是 PDF 文件,則 page 是虛擬的紙;如果顯示到屏幕中,則 page 可能是位圖。page 隨當前的 context 而變。
我們需要繪製的圖形加號位於藍色圓之上。因此,需要先繪製藍色圓,後繪製加號。
在PushButton
中添加以下常量:
private struct Constants {
static let plusLineWidth: CGFloat = 3.0
static let plusButtonScale: CGFloat = 0.6
static let halfPointShift: CGFloat = 0.5
}
private var halfWidth: CGFloat {
return bounds.width / 2
}
private var halfHeight: CGFloat {
return bounds.height / 2
}
在draw(_:)
添加以下代碼,繪製加號中的橫線:
override func draw(_ rect: CGRect) {
...
// Set up the width and height variables for the horizontal stroke
let plusWidth = min(bounds.width, bounds.height) * Constants.plusButtonScale
let halfPlusWidth = plusWidth / 2
// Create the path
let plusPath = UIBezierPath()
// Set the path's line width to the height of the stroke
plusPath.lineWidth = Constants.plusLineWidth
// Move the initial point of the path to the start of the horizontal stroke
plusPath.move(to: CGPoint(x: halfWidth - halfPlusWidth, y: halfHeight))
// Add a point to the path at the end of the stroke
plusPath.addLine(to: CGPoint(x: halfWidth + halfPlusWidth, y: halfHeight))
// Set the stroke color
UIColor.white.setStroke()
// Draw the stroke
plusPath.stroke()
}
上述代碼創建了一個UIBezierPath
,設置其在圓上的起點、終點,最後使用白色描邊。效果如下:
在iPad 2、iPhone 8 Plus模擬器中運行demo,可以看到該橫線不是很清晰,有一條淡藍色的線圍繞它。如下所示:
4. Points VS Pixels
初代iPhone發佈時,points和pixels佔據同樣空間,大小一致。Retain 屏iPhone發佈後,一個point不再佔據一pixels。
下圖是12*12像素,point使用灰色、白色顯示的表格,iPad 2使用1x圖,即1point佔據1pixel;iPhone 8使用2x圖,即1point佔據2pixel;iPhone 8 Plus使用3x圖,即1point佔據3pixel。
繪製線時從path的中心開始。繪製線高度爲3point時,則每側有1.5point。可以看到,1x、3x顯示屏渲染時,會有半像素需要渲染的情況。顯然,屏幕無法將一個像素渲染爲兩種顏色。iOS 的抗鋸齒化會將該像素渲染爲兩種顏色的中間值。最終,顏色邊界變得模糊。
開發過程中,retain 顯示屏3x擁有超高分辨率,不太容易注意到抗鋸齒產生的模糊。但如果app需支持1x屏,抗鋸齒會很明顯,需格外注意。
path位置需爲整數加減0.5,以防止抗鋸齒。正如上圖中看到的,0.5point在1x屏幕中向上移動0.5pixel,在2x屏幕中移動1.0pixel,在3x屏幕中移動1.5pixel。
更新draw(_:)
中的move(to:)
和addLine(to:)
如下:
// Move the initial point of the path to the start of the horizontal stroke
plusPath.move(to: CGPoint(x: halfWidth - halfPlusWidth + Constants.halfPointShift, y: halfHeight + Constants.halfPointShift))
// Add a point to the path at the end of the stroke
plusPath.addLine(to: CGPoint(x: halfWidth + halfPlusWidth + Constants.halfPointShift, y: halfHeight + Constants.halfPointShift))
因爲path偏移了0.5point,其在三種不同屏幕上都不會產生抗鋸齒。
在上述代碼後,stroke前添加以下代碼,繪製豎線:
// Vertical line
plusPath.move(to: CGPoint(x: halfWidth + Constants.halfPointShift, y: halfHeight - halfPlusWidth + Constants.halfPointShift))
plusPath.addLine(to: CGPoint(x: halfWidth + Constants.halfPointShift, y: halfHeight + halfPlusWidth + Constants.halfPointShift))
用戶有時可能誤操作,點擊兩次加號。做爲開發者,應該提供減少次數的功能。 你可以複用PushButton
,繪製一個減號按鈕。遇到問題可以下載源碼查看。最終效果如下圖:
[圖片上傳失敗...(image-5433f7-1636042273902)]
5. 弧線
這一部分將繪製如下的圖像:
創建CounterView
,其繼承自UIView
。添加以下代碼,稍後用於繪製:
class CounterView: UIView {
private struct Constants {
static let numberOfGlasses = 8
static let lineWidth: CGFloat = 5.0
static let arcWidth: CGFloat = 76
static var halfOfLineWidth: CGFloat {
lineWidth / 2
}
}
var counter = 5
var outlineColor = UIColor.blue
var counterColor = UIColor.orange
}
使用Auto Layout佈局CounterView,設置其寬高230point,中點位於水平中心,底部距離pushButton
頂部40point。添加約束後,運行如下:
在CounterView.swift
的draw(_:)
方法中,添加以下代碼:
override func draw(_ rect: CGRect) {
// 弧線的center
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
// 根據視圖最大尺寸計算半徑
let radius = max(bounds.width, bounds.height)
// 弧線起始弧度
let startAngle: CGFloat = 3 * .pi / 4
let endAngle: CGFloat = .pi / 4
// 根據center、radius、angle創建貝塞爾曲線
let path = UIBezierPath(arcCenter: center, radius: radius / 2 - Constants.arcWidth / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
// 設置path寬度、顏色,最後stroke path
path.lineWidth = Constants.arcWidth
counterColor.setStroke()
path.stroke()
}
可以將上面繪製圓弧的方式想象成圓規畫圓。將帶有鋼芯的腳放到center,兩隻腳的距離爲半徑,旋轉繪製圖形即可。使用 Core Graphics 繪製時,圓規的鋼芯是 center,圓規兩腳的距離減去筆寬的一半是 radius。筆的寬度是圓弧的寬度。
運行後效果如下:
6. 勾畫弧線
使用弧線標誌喝了幾杯水。弧線包含一條外線,一條內線,以及連接它們的線。
在CounterView.swift
文件的draw(_:)
方法中添加以下代碼:
override func draw(_ rect: CGRect) {
...
// 繪製外邊緣
// 計算弧度,確保其爲正值。
let angleDifference: CGFloat = 2 * .pi - startAngle + endAngle
// 每杯水對應弧度
let arcLengthPerGlass = angleDifference / CGFloat(Constants.numberOfGlasses)
// 弧線終點弧度
let outlineEndAngle = arcLengthPerGlass * CGFloat(counter) + startAngle
// 繪製外邊緣
let outerArcRadius = bounds.width / 2 - Constants.halfOfLineWidth
let outlinePath = UIBezierPath(arcCenter: center, radius: outerArcRadius, startAngle: startAngle, endAngle: outlineEndAngle, clockwise: true)
// 繪製內邊緣
let innerArcRadius = bounds.width / 2 - Constants.arcWidth + Constants.halfOfLineWidth
outlinePath.addArc(withCenter: center, radius: innerArcRadius, startAngle: outlineEndAngle, endAngle: startAngle, clockwise: false)
// 關閉path
outlinePath.close()
outlineColor.setStroke()
outlinePath.lineWidth = Constants.lineWidth
outlinePath.stroke()
}
CounterView
的counter
設置爲5時,效果如下:
最後,在
CounterView
中心添加UILabel
,顯示當前喝了幾杯水。爲加減按鈕添加點擊事件。因爲,只有在自身isHidden
變化、子視圖移動,首次添加到屏幕中,纔會調用draw(_:)
。因此,需要重繪時,需調用setNeedsDisplay()
、setNeedsDisplayInRect()
方法。
如果遇到問題,可以下載源碼查看。運行後效果如下:
總結
這篇文章介紹了基礎的繪圖操作,可以用來繪製各種形狀的圖案。
下一篇文章CoreGraphics系列二:gradient和context將進一步介紹 Core Graphics 的 context,繪製一個折線圖。
Demo名稱:CoreGraphics1
源碼地址:https://github.com/pro648/BasicDemos-iOS/tree/master/CoreGraphics-1
參考資料: