CoreGraphics系列一:path

Core Graphics 框架也稱爲 Quartz 2D,是基於 Quartz 的高級渲染引擎,它提供了底層輕量級 2D 渲染引擎,可以進行高保真輸出。

Quartz 2D 簡單易用,提供了強大的功能。例如,基於路徑的繪製,變換,顏色管理,離屏渲染,模式(pattern),漸變,陰影,圖像數據管理、圖像創建和mask,還可以處理 PDF 文檔的創建、渲染和解析。

這篇文章將創建一個demo,用來記錄每日喝了幾杯水,並且會用折線圖彙總顯示本週飲水量。

1. 在視圖上添加自定義繪製

創建一個名稱爲CoreGraphics-1的 iOS app。添加一個UIView,並在其中繪製自定義內容,步驟如下:

  1. 創建UIView子類。
  2. 重寫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.swiftdraw(_:)方法中,添加以下代碼:

    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()
    }

CounterViewcounter設置爲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

參考資料:

  1. Core Graphics Tutorial: Getting Started
  2. iOS Drawing Concepts
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章