本文翻譯自 raywenderlich.com 的 Core Graphics on macOS Tutorial蛤奢,已咨詢對(duì)方網(wǎng)站它褪,可至多翻譯 10 篇文章饵骨。
希望各位有英語(yǔ)閱讀能力的話,還是先打賞然后去閱讀英文原吧茫打,畢竟無(wú)論是 Xcode居触,抑或是官方的文檔,還是各種最前沿的資訊都只有英文版本老赤。
綜上轮洋,此翻譯版本僅供參考,謝絕轉(zhuǎn)載抬旺。也歡迎你點(diǎn)擊我的頭像查看我翻譯的其他 macOS 開發(fā)教程??
更新于 2016-9-22:此教程已更新至 Xcode 8 和 Swift 3弊予。
你肯定見過(guò)許多擁有漂亮的界面和華麗的自定義視圖的 app,它們肯定在你的心里留下了深刻的印象开财,因?yàn)樗鼈兪?strong>那汉柒!么误褪!好!看碾褂!
Core Graphics 是 Apple 提供的 2D 繪圖引擎振坚,也幾乎是 macOS 和 iOS 所有框架中最酷的了。它可以用來(lái)繪制你能想到的所有圖形斋扰,從簡(jiǎn)單的幾何形狀到復(fù)雜的陰影和漸變等視覺(jué)效果。
在這個(gè) macOS Core Graphics 教程中啃洋,你將會(huì)創(chuàng)造一個(gè)叫做 DiskInfo 的自定義視圖传货,它能用一張餅圖和一個(gè)條狀圖來(lái)顯示出你 Mac 上的硬盤可用空間。這個(gè)教程將會(huì)讓你擁有把平淡單調(diào)的 UI 變得精彩紛呈的能力:
在這個(gè)教程中你將學(xué)會(huì):
- 創(chuàng)建并配置一個(gè)自定義視圖宏娄,這是繪制圖形元素的必要條件问裕;
- 實(shí)現(xiàn)實(shí)時(shí)渲染預(yù)覽功能,有了它你不需要編譯和運(yùn)行孵坚,就能在 Interface Builder 里看到你對(duì)圖形的各種修改粮宛;
- 用代碼繪制路徑、填充圖形卖宠、創(chuàng)建剪切蒙版剪輯和渲染文本巍杈;
- 使用 AppKit 里的 Cocoa Drawing 工具提供的高級(jí)類和方法。
在第一部分中扛伍,你將會(huì)通過(guò) Core Graphics 來(lái)實(shí)現(xiàn)繪制一個(gè)餅圖筷畦,稍后你將會(huì)學(xué)習(xí)如何用 Cocoa Drawing 實(shí)現(xiàn)相同的效果。
所以拿起你的小畫刷刺洒,我們要開始作畫啦~
準(zhǔn)備開始
點(diǎn)擊這里下載 DiskInfo 的起步工程鳖宾,編譯并運(yùn)行它:
這個(gè) app 會(huì)羅列出你的所有硬盤,點(diǎn)擊任何一個(gè)即可查看他的詳細(xì)信息逆航。
在操作之前鼎文,我們先來(lái)熟悉一下這個(gè)項(xiàng)目的結(jié)構(gòu):
- ViewController.swift:app 的主要 View Controller;
-
VolumeInfo.swift:實(shí)現(xiàn)了用于處理硬盤信息的
VolumeInfo
類因俐,以及用于分析不同文件類型所占空間的FilesDistribution
結(jié)構(gòu)體拇惋; - NSColor+DiskInfo.swift 和 NSFont+DiskInfo.swift:擴(kuò)展了 NSColor,定義了 app 中會(huì)用到的顏色和字體抹剩;
- CGFloat+Radians.swift:擴(kuò)展了 CGFloat蚤假,提供了轉(zhuǎn)換角度值和弧度制的 helper 方法;
- MountedVolumesDataSource.swift 和 MountedVolumesDelegate.swift:實(shí)現(xiàn)了顯示硬盤信息所必需的各類方法吧兔。
注意:這個(gè) app 可以顯示你真正的硬盤用量信息磷仰,但在這個(gè)教程中,它將會(huì)生成隨機(jī)的數(shù)據(jù)境蔼。
每次啟動(dòng) app 時(shí)都計(jì)算一次硬盤上所有文件的類型會(huì)很耗時(shí)灶平,也會(huì)消磨完你所有的樂(lè)趣伺通,沒(méi)人愿意在這上面浪費(fèi)時(shí)間,對(duì)吧??逢享?
創(chuàng)建一個(gè)自定義視圖
你要做的第一件事是創(chuàng)建一個(gè)名叫 GraphView
的自定義視圖罐监。這將會(huì)是你繪制餅狀圖和條形圖的地方。這個(gè)部分中你需要做兩件事:
- 創(chuàng)建一個(gè)
NSView
的子類瞒爬; - 重寫
draw(_:)
方法弓柱,加入一些用于繪制的代碼。
創(chuàng)建 NSView 的子類
選中項(xiàng)目導(dǎo)航器的 Views 分組侧但,點(diǎn)擊去 Xcode 菜單上的 File → New → File…矢空,然后點(diǎn)擊 macOS → Source → Cocoa Class 模版。
點(diǎn)擊 Next禀横,把新的類命名為 GraphView
屁药,并讓它繼承自 NSView
,把語(yǔ)言選擇為 Swift柏锄。
點(diǎn)擊 Next 和 Create 來(lái)保存你的文件酿箭。
打開 Main.storyboard,在 View Controller Scene 中趾娃,從控件庫(kù)里拖入一個(gè) Custom View缭嫡。
選中這個(gè) Custom View,在身份檢查器里抬闷,把它的類名設(shè)置為 GraphView
械巡。
你需要一些約束,所以保持 Graph View 的選中狀態(tài)饶氏,點(diǎn)擊 Auto Layout 工具欄上的 Pin 按鈕讥耗,把它的 Top、Bottom疹启、Leading 和 Trailing 約束設(shè)置為 0古程,然后點(diǎn)擊 Add 4 Constrains 按鈕。
點(diǎn)擊 Auto Layout 工具欄上的三角形的 Resolve Auto Layout Issues 按鈕喊崖,然后在 Selected Views 部分中點(diǎn)擊 Update Frames挣磨。如果這個(gè)選項(xiàng)不可用,你可能需要先點(diǎn)擊空白處取消選中 Graph View荤懂,然后再次選中它茁裙。
重寫 draw(_:)
打開 GraphView.swift,你能看到 Xcode 自動(dòng)為我們創(chuàng)建了一個(gè) draw(_:)
的實(shí)現(xiàn)节仿。把那行注釋替換成以下代碼晤锥,并確保你別不小心刪掉了它調(diào)用父類此方法的那一行哦。
NSColor.white.setFill()
NSRectFill(bounds)
第一行代碼把填充顏色設(shè)置為了白色,然后通過(guò)調(diào)用 NSRectFill
方法矾瘾,你把整個(gè)視圖的背景設(shè)成了白色女轿。
編譯并運(yùn)行:
你已經(jīng)把自定義視圖的背景從默認(rèn)的灰色改成了白色。
哈哈壕翩,我們的畫布已經(jīng)就緒蛉迹!就是這么簡(jiǎn)單~
實(shí)時(shí)渲染預(yù)覽:@IBDesignable
和 @IBInspectable
Xcode 6 為我們帶來(lái)了一個(gè)牛×的功能:實(shí)時(shí)渲染預(yù)覽放妈。它允許你在 Interface Builder 里查看你自定義的視圖的樣子北救,而不用每次都編譯和運(yùn)行。
要啟用這個(gè)功能芜抒,你需要用 @IBDesignable
來(lái)修飾你的類珍策;并實(shí)現(xiàn) prepareForInterfaceBuilder()
方法來(lái)提供一些示例數(shù)據(jù)(實(shí)現(xiàn)這個(gè)方法不是必須的)。
打開 GraphView.swift挽绩,在類的定義之前加入:
@IBDesignable
現(xiàn)在你需要提供一些示例數(shù)據(jù),把這些代碼添加到 GraphView
類中:
var fileDistribution: FilesDistribution? {
didSet {
needsDisplay = true
}
}
override func prepareForInterfaceBuilder() {
let used = Int64(100000000000)
let available = used / 3
let filesBytes = used / 5
let distribution: [FileType] = [
.apps(bytes: filesBytes / 2, percent: 0.1),
.photos(bytes: filesBytes, percent: 0.2),
.movies(bytes: filesBytes * 2, percent: 0.15),
.audio(bytes: filesBytes, percent: 0.18),
.other(bytes: filesBytes, percent: 0.2)
]
fileDistribution = FilesDistribution(capacity: used + available,
available: available,
distribution: distribution)
}
這將會(huì)定義一個(gè) fileDistribution
屬性用于存儲(chǔ)硬盤的信息驾中。當(dāng)這個(gè)屬性發(fā)生改變唉堪,它會(huì)設(shè)置這個(gè)視圖的 needsDisplay
屬性為 true
,從而讓視圖重繪自己的內(nèi)容肩民。
然后唠亚,它實(shí)現(xiàn)了 prepareForInterfaceBuilder()
方法,以此創(chuàng)建了一個(gè)各種文件類型的例子持痰,用于給 Xcode 預(yù)覽這個(gè)視圖灶搜。
注意:你甚至可以在 Interface Builder 里實(shí)時(shí)修改視覺(jué)屬性。這要求你用
@IBInspectable
來(lái)修飾這個(gè)屬性工窍。
下一步:用 @IBInspectable
修飾所有的視覺(jué)屬性割卖,把這些代碼添加到 GraphView
的聲明中:
// 1
fileprivate struct Constants {
static let barHeight: CGFloat = 30.0
static let barMinHeight: CGFloat = 20.0
static let barMaxHeight: CGFloat = 40.0
static let marginSize: CGFloat = 20.0
static let pieChartWidthPercentage: CGFloat = 1.0 / 3.0
static let pieChartBorderWidth: CGFloat = 1.0
static let pieChartMinRadius: CGFloat = 30.0
static let pieChartGradientAngle: CGFloat = 90.0
static let barChartCornerRadius: CGFloat = 4.0
static let barChartLegendSquareSize: CGFloat = 8.0
static let legendTextMargin: CGFloat = 5.0
}
// 2
@IBInspectable var barHeight: CGFloat = Constants.barHeight {
didSet {
barHeight = max(min(barHeight, Constants.barMaxHeight), Constants.barMinHeight)
}
}
@IBInspectable var pieChartUsedLineColor: NSColor = NSColor.pieChartUsedStrokeColor
@IBInspectable var pieChartAvailableLineColor: NSColor = NSColor.pieChartAvailableStrokeColor
@IBInspectable var pieChartAvailableFillColor: NSColor = NSColor.pieChartAvailableFillColor
@IBInspectable var pieChartGradientStartColor: NSColor = NSColor.pieChartGradientStartColor
@IBInspectable var pieChartGradientEndColor: NSColor = NSColor.pieChartGradientEndColor
@IBInspectable var barChartAvailableLineColor: NSColor = NSColor.availableStrokeColor
@IBInspectable var barChartAvailableFillColor: NSColor = NSColor.availableFillColor
@IBInspectable var barChartAppsLineColor: NSColor = NSColor.appsStrokeColor
@IBInspectable var barChartAppsFillColor: NSColor = NSColor.appsFillColor
@IBInspectable var barChartMoviesLineColor: NSColor = NSColor.moviesStrokeColor
@IBInspectable var barChartMoviesFillColor: NSColor = NSColor.moviesFillColor
@IBInspectable var barChartPhotosLineColor: NSColor = NSColor.photosStrokeColor
@IBInspectable var barChartPhotosFillColor: NSColor = NSColor.photosFillColor
@IBInspectable var barChartAudioLineColor: NSColor = NSColor.audioStrokeColor
@IBInspectable var barChartAudioFillColor: NSColor = NSColor.audioFillColor
@IBInspectable var barChartOthersLineColor: NSColor = NSColor.othersStrokeColor
@IBInspectable var barChartOthersFillColor: NSColor = NSColor.othersFillColor
// 3
func colorsForFileType(fileType: FileType) -> (strokeColor: NSColor, fillColor: NSColor) {
switch fileType {
case .audio(_, _):
return (strokeColor: barChartAudioLineColor, fillColor: barChartAudioFillColor)
case .movies(_, _):
return (strokeColor: barChartMoviesLineColor, fillColor: barChartMoviesFillColor)
case .photos(_, _):
return (strokeColor: barChartPhotosLineColor, fillColor: barChartPhotosFillColor)
case .apps(_, _):
return (strokeColor: barChartAppsLineColor, fillColor: barChartAppsFillColor)
case .other(_, _):
return (strokeColor: barChartOthersLineColor, fillColor: barChartOthersFillColor)
}
}
這一大坨代碼的作用是:
- 聲明了帶有許多常量的結(jié)構(gòu)體 —— 你得在這個(gè) app 的各個(gè)地方用到它們;
- 用
@IBInspectable
修飾所有可配置的屬性患雏。并使用 NSColor+DiskInfo.swift 中的值為它們賦值鹏溯。注意:要使一個(gè)屬性「inspectable」(可以在 Interface Builder 里直接編輯),你必須聲明它的類型淹仑,即使大多數(shù)情況下 Swift 會(huì)幫你做這件事兒丙挽; - 定義一個(gè) Helper 方法,為不同的文件類型返回它的筆觸顏色和填充顏色匀借。你在繪制「什么文件占了多大地兒」的圖表的時(shí)候會(huì)用到它颜阐。
打開 Main.stroyboard,你應(yīng)該能注意到 Graph View 已經(jīng)從默認(rèn)的灰色變成了白色吓肋,這意味著實(shí)時(shí)渲染預(yù)覽已經(jīng)起作用了凳怨。
選中 Graph View,打開屬性檢查器是鬼,你會(huì)發(fā)現(xiàn)所有的「inspectable」屬性已經(jīng)出現(xiàn)在這里了猿棉。
現(xiàn)在開始磅叛,要查看調(diào)整好的效果,你可以直接編譯和運(yùn)行萨赁,也可以直接在 Interface Builder 里查看弊琴。
萬(wàn)事俱備,是時(shí)候開始真正的繪制啦杖爽!
Graphics Context(圖形上下文)
在使用 Core Graphics 的時(shí)候敲董,你并不是直接在視圖中繪畫,而是使用一個(gè)叫 Graphics Context(圖形上下文)的東西慰安,它是系統(tǒng)渲染圖形與把圖形顯示在視圖中的中間層腋寨。
Core Graphics 使用一個(gè)叫「Painter’s Model(畫家模式)」的模式,你可以想像成自己拿著筆化焕,唰唰地在畫布上繪圖樣子萄窜。你需要放置一個(gè)路徑,然后去填充它撒桨,你沒(méi)辦法去改變已經(jīng)布置好的像素們查刻,但你可以在它們之上繼續(xù)畫圖。
這個(gè)「上下文」非常重要凤类,因?yàn)樗麤Q定了你最終得到的效果穗泵。
繪制路徑
要使用 Core Graphics 繪制一個(gè)路徑,你需要定義一個(gè) Path(路徑)谜疤,也就是 CGPathRef
和可變的 CGMutablePathRef
佃延。
路徑準(zhǔn)備好以后,把它添加到圖形上下文里夷磕,就可以根據(jù)路徑和繪制屬性渲染出你要的圖形了履肃。
為餅圖…繪制一個(gè)路徑
條形圖的基本元素是圓角矩形,所以我們從這里開始入手坐桩。
打開 GraphView.swift榆浓,把這個(gè)擴(kuò)展添加在文件底部類定義以外的地方:
// MARK: - 用于繪制的 extension
extension GraphView {
func drawRoundedRect(rect: CGRect, inContext context: CGContext?,
radius: CGFloat, borderColor: CGColor, fillColor: CGColor) {
// 1
let path = CGMutablePath()
// 2
path.move( to: CGPoint(x: rect.midX, y:rect.minY ))
path.addArc( tangent1End: CGPoint(x: rect.maxX, y: rect.minY ),
tangent2End: CGPoint(x: rect.maxX, y: rect.maxY), radius: radius)
path.addArc( tangent1End: CGPoint(x: rect.maxX, y: rect.maxY ),
tangent2End: CGPoint(x: rect.minX, y: rect.maxY), radius: radius)
path.addArc( tangent1End: CGPoint(x: rect.minX, y: rect.maxY ),
tangent2End: CGPoint(x: rect.minX, y: rect.minY), radius: radius)
path.addArc( tangent1End: CGPoint(x: rect.minX, y: rect.minY ),
tangent2End: CGPoint(x: rect.maxX, y: rect.minY), radius: radius)
path.closeSubpath()
// 3
context?.setLineWidth(1.0)
context?.setFillColor(fillColor)
context?.setStrokeColor(borderColor)
// 4
context?.addPath(path)
context?.drawPath(using: .fillStroke)
}
}
太長(zhǎng)不看???:以上就是繪制一個(gè)圓角矩形的方法,用人類能理解的語(yǔ)言解釋一遍就是:
- 創(chuàng)建一個(gè)可以改變的路徑撕攒;
- 一步一步勾勒出一個(gè)圓角矩形:
- 移動(dòng)到矩形底邊的中點(diǎn)陡鹃,這里將是我們的起點(diǎn);
- 使用
addArc(tangent1End:tangent2End:radius)
方法繪制右下角的線段抖坪,這個(gè)方法會(huì)繪制出底部的水平線以及右下角的圓角萍鲸; - 添加右邊的線段和右上角的圓角;
- 添加頂部的線段和左上角的圓角擦俐;
- 添加左邊的線段和左下角的圓角脊阴;
- 閉合路徑,也就是從上一步的重點(diǎn)連接到起點(diǎn);
- 設(shè)置繪制屬性:線寬嘿期、填充顏色和邊框顏色品擎;
- 把路徑添加到圖形上下文,并使用
.fillStroke
參數(shù)繪制著個(gè)路徑备徐,這個(gè)參數(shù)將會(huì)告訴 Core Graphics 這條路徑需要填充顏色并繪制邊框萄传。
計(jì)算位置
使用 Core Graphics 進(jìn)行繪制的過(guò)程其實(shí)就是計(jì)算各個(gè)視覺(jué)元素在視圖中位置的過(guò)程。所以我們需要關(guān)心的就是把不同的元素放置在哪蜜猾,以及當(dāng)視圖的大小發(fā)生變化時(shí)它們?cè)撊绾螒?yīng)對(duì)秀菱。
我們準(zhǔn)備這樣布局我們的視圖:
打開 GraphView.swift 并添加這個(gè)擴(kuò)展:
// MARK: - 用于計(jì)算的 extension
extension GraphView {
// 1
func pieChartRectangle() -> CGRect {
let width = bounds.size.width * Constants.pieChartWidthPercentage - 2 * Constants.marginSize
let height = bounds.size.height - 2 * Constants.marginSize
let diameter = max(min(width, height), Constants.pieChartMinRadius)
let rect = CGRect(x: Constants.marginSize,
y: bounds.midY - diameter / 2.0,
width: diameter, height: diameter)
return rect
}
// 2
func barChartRectangle() -> CGRect {
let pieChartRect = pieChartRectangle()
let width = bounds.size.width - pieChartRect.maxX - 2 * Constants.marginSize
let rect = CGRect(x: pieChartRect.maxX + Constants.marginSize,
y: pieChartRect.midY + Constants.marginSize,
width: width, height: barHeight)
return rect
}
// 3
func barChartLegendRectangle() -> CGRect {
let barchartRect = barChartRectangle()
let rect = barchartRect.offsetBy(dx: 0.0, dy: -(barchartRect.size.height + Constants.marginSize))
return rect
}
}
以上代碼做了這些必要的計(jì)算:
- 從計(jì)算餅圖的位置開始入手,它將會(huì)垂直居中蹭睡,并占據(jù)視圖 1/3 的寬度衍菱;
- 計(jì)算條形圖的位置,它將會(huì)占據(jù) 2/3 的寬度肩豁,并處于視圖中部偏上的位置脊串;
- 根據(jù)餅圖的最小 Y 值和邊距來(lái)計(jì)算圖例的位置。
現(xiàn)在我們來(lái)把把它繪制到你的視圖中去清钥,在 GraphView
的用于繪制的擴(kuò)展中加入:
func drawBarGraphInContext(context: CGContext?) {
let barChartRect = barChartRectangle()
drawRoundedRect(rect: barChartRect, inContext: context,
radius: Constants.barChartCornerRadius,
borderColor: barChartAvailableLineColor.cgColor,
fillColor: barChartAvailableFillColor.cgColor)
}
你需要一個(gè) Helper 方法來(lái)繪制條形圖琼锋,它會(huì)繪制一個(gè)圓角矩形,并使用畫筆顏色和填充顏色在空白處繪制圖形循捺,你可以在 NSColor+DiskInfo 擴(kuò)展中找到這個(gè)些顏色斩例。
把 draw(_:)
方法里的所有代碼替換成:
super.draw(dirtyRect)
let context = NSGraphicsContext.current()?.cgContext
drawBarGraphInContext(context: context)
這段代碼會(huì)真正地把圖形繪制到視圖中去雄人。首先从橘,通過(guò)調(diào)用 NSGraphicsContext.current()
,我們獲取到了當(dāng)前視圖的圖形上下文础钠,然后我們調(diào)用剛剛編寫的方法繪制出了條形圖恰力。
編譯并運(yùn)行,你可以看到條形圖已經(jīng)就位了:
現(xiàn)在旗吁,打開 Main.storyboard 并選中 View Controller Scene踩萎, 你會(huì)看到這個(gè):
Interface Builder 為你實(shí)時(shí)渲染了預(yù)覽。你可以試著去修改一下顏色很钓,它也會(huì)實(shí)時(shí)響應(yīng)你的修改香府,是不是很棒棒呀??~
剪切一部分區(qū)域(也就是蒙版)
現(xiàn)在我們來(lái)制作文件分布圖,也就是這個(gè)家伙:
先暫停一下下码倦,我們來(lái)理理思路企孩。顯而易見的是,每種文件都有自己的專屬顏色袁稽,我們的 app 只需要根據(jù)這種文件占硬盤空間大小的百分比來(lái)計(jì)算每個(gè)方塊的寬度勿璃,然后用對(duì)應(yīng)的顏色把它繪制出來(lái)。
你需要繪制一個(gè)不規(guī)則的圖形,比如一個(gè)!@#¥%%补疑。然而歧沪,我們可以通過(guò)一個(gè)叫 clipping areas(蒙板) 的技術(shù)來(lái)避免編寫重復(fù)代碼。
??這一段的第一句實(shí)在沒(méi)看懂莲组,求指正:You could create a special shape, such as a filled rectangle with two lines at bottom and top of the rectangle诊胞。
你可以把蒙版想象成「在一張紙上剪了個(gè)窟窿」,你只能透過(guò)這個(gè)窟窿看到部分的圖形胁编。這個(gè)「窟窿」就叫做「Clipping Mask(剪切蒙版)」厢钧,你需要在 Core Graphics 里定義它。
在這個(gè)條形圖的例子里嬉橙,你需要為每種文件分類創(chuàng)建一個(gè)完整的圓角矩形早直,然后通過(guò)剪切蒙版來(lái)使它們只顯示正確的部分:
理論說(shuō)完了,我們來(lái)動(dòng)手吧~
開支繪制之前市框,你需要為選中的硬盤設(shè)置 fileDistribution
霞扬。打開 Main.storyboard,我們來(lái)創(chuàng)建一個(gè) Outlet 連接枫振。
在項(xiàng)目導(dǎo)航器里按住 Option? 鍵的同時(shí)點(diǎn)擊 ViewController.swift喻圃,使它顯示在右半邊的協(xié)助編輯器里,然后按住 Control? 鍵的同時(shí)把 Graph View 拖動(dòng)到 View Controller 的代碼里粪滤。
在彈出的小氣泡里斧拍,把這個(gè) Outlet 命名為 graphView
,并點(diǎn)擊 Connect杖小。
打開 ViewController.swift 并把這行代碼添加到 showVolumeInfo(_:)
的末尾:
graphView.fileDistribution = volume.fileDistribution
這行代碼設(shè)置了 fileDistribution
的值肆汹,從而讓 Graph View 能獲取各類文件占的百分比。
打開 GraphView.swift予权,把這些代碼添加到 drawBarGraphInContext(context:)
的末尾來(lái)繪制條形圖:
// 1
if let fileTypes = fileDistribution?.distribution, let capacity = fileDistribution?.capacity, capacity > 0 {
var clipRect = barChartRect
// 2
for (index, fileType) in fileTypes.enumerated() {
// 3
let fileTypeInfo = fileType.fileTypeInfo
let clipWidth = floor(barChartRect.width * CGFloat(fileTypeInfo.percent))
clipRect.size.width = clipWidth
// 4
context?.saveGState()
context?.clip(to: clipRect)
let fileTypeColors = colorsForFileType(fileType: fileType)
drawRoundedRect(rect: barChartRect, inContext: context,
radius: Constants.barChartCornerRadius,
borderColor: fileTypeColors.strokeColor.cgColor,
fillColor: fileTypeColors.fillColor.cgColor)
context?.restoreGState()
// 5
clipRect.origin.x = clipRect.maxX
}
}
這些代碼做了這些事兒:
- 先確保了 Graph View 擁有一個(gè)有效的
fileDistribution
昂勉; - 遍歷
fileDistribution
里的每一種文件類型; - 根據(jù)文件的占比計(jì)算蒙版的大猩ㄏ佟岗照;
- 存儲(chǔ)圖形上下文的狀態(tài),設(shè)置蒙版的大小笆环,用文件類型對(duì)應(yīng)的顏色繪制圓角矩形攒至,并恢復(fù)圖形上下文的狀態(tài);
- 把剪切蒙版的 x 移動(dòng)到正確的位置躁劣。
你可能會(huì)奇怪:為什么要先存儲(chǔ)迫吐,再恢復(fù)圖形上下文?還記得「painter’s model」嗎习绢?你添加到圖形上下文里的所有東西都會(huì)被保存在上下文中渠抹,就像你畫在紙上的畫蝙昙,會(huì)一直在那里。
如果你添加了多個(gè)剪切蒙版梧却,事實(shí)上你是只創(chuàng)建了一個(gè)剪切蒙版奇颠,并應(yīng)用到所有矩形上知市。要避免這種情況赦役,你需要在添加新的剪切蒙版之前存儲(chǔ)上下文的狀態(tài),等你使用完了這個(gè)蒙版陋桂,再把它恢復(fù)出來(lái)广鳍,再處理新的蒙版荆几。
此時(shí),Xcode 會(huì)彈出一個(gè)警告赊时,因?yàn)?index
從沒(méi)被使用過(guò)吨铸。別擔(dān)心,它的待會(huì)兒就會(huì)派上用場(chǎng)祖秒。
編譯并運(yùn)行诞吱,或者直接打開 Main.storyboard:
哈哈,DiskInfo 功能似乎已經(jīng)漸漸完善了呢~除了圖例竭缝,這個(gè)條形圖已經(jīng)基本完工了??房维。
繪制文本
在自定義視圖里繪制文本特別簡(jiǎn)單,你需要為這個(gè)文本的各種屬性創(chuàng)建一個(gè)字典抬纸,包含了字體咙俩、尺寸、顏色和對(duì)齊湿故,把它傳入 String
的 draw(in:withAttributes:)
方法阿趁。這些屬性將會(huì)在我們計(jì)算矩形大小和位置的時(shí)候派上用場(chǎng)。
打開 GraphView.swift晓锻,把這個(gè)屬性添加到類的定義里:
fileprivate var bytesFormatter = ByteCountFormatter()
這將會(huì)創(chuàng)建一個(gè) ByteCountFormatter歌焦。它會(huì)幫我們完成「把字節(jié)轉(zhuǎn)化成人話」這個(gè)高深而繁重的工作飞几。
現(xiàn)在砚哆,在 drawBarGraphInContext(context:)
方法的 for (index,fileType) in fileTypes.enumerated()
循環(huán)里加入這些代碼:
// 1
let legendRectWidth = (barChartRect.size.width / CGFloat(fileTypes.count))
let legendOriginX = barChartRect.origin.x + floor(CGFloat(index) * legendRectWidth)
let legendOriginY = barChartRect.minY - 2 * Constants.marginSize
let legendSquareRect = CGRect(x: legendOriginX, y: legendOriginY,
width: Constants.barChartLegendSquareSize,
height: Constants.barChartLegendSquareSize)
let legendSquarePath = CGMutablePath()
legendSquarePath.addRect( legendSquareRect )
context?.addPath(legendSquarePath)
context?.setFillColor(fileTypeColors.fillColor.cgColor)
context?.setStrokeColor(fileTypeColors.strokeColor.cgColor)
context?.drawPath(using: .fillStroke)
// 2
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineBreakMode = .byTruncatingTail
paragraphStyle.alignment = .left
let nameTextAttributes = [
NSFontAttributeName: NSFont.barChartLegendNameFont,
NSParagraphStyleAttributeName: paragraphStyle]
// 3
let nameTextSize = fileType.name.size(withAttributes: nameTextAttributes)
let legendTextOriginX = legendSquareRect.maxX + Constants.legendTextMargin
let legendTextOriginY = legendOriginY - 2 * Constants.pieChartBorderWidth
let legendNameRect = CGRect(x: legendTextOriginX, y: legendTextOriginY,
width: legendRectWidth - legendSquareRect.size.width - 2 *
Constants.legendTextMargin,
height: nameTextSize.height)
// 4
fileType.name.draw(in: legendNameRect, withAttributes: nameTextAttributes)
// 5
let bytesText = bytesFormatter.string(fromByteCount: fileTypeInfo.bytes)
let bytesTextAttributes = [
NSFontAttributeName: NSFont.barChartLegendSizeTextFont,
NSParagraphStyleAttributeName: paragraphStyle,
NSForegroundColorAttributeName: NSColor.secondaryLabelColor]
let bytesTextSize = bytesText.size(withAttributes: bytesTextAttributes)
let bytesTextRect = legendNameRect.offsetBy(dx: 0.0, dy: -bytesTextSize.height)
bytesText.draw(in: bytesTextRect, withAttributes: bytesTextAttributes)
看起來(lái)這一大堆代碼還挺唬人的,其實(shí)很簡(jiǎn)單:
- 你已經(jīng)很熟悉這一段代碼了:計(jì)算圖例的彩色方塊的位置屑墨,為它創(chuàng)建一條路徑躁锁,并用對(duì)應(yīng)的顏色填充;
- 創(chuàng)建一個(gè)字典卵史,包含了兩個(gè)屬性:字體和
NSMutableParagraphStyle
战转。后者會(huì)定義這些文本會(huì)怎樣在給定的矩形里被繪制出來(lái)。在這個(gè)例子中以躯,文本會(huì)顯示為左對(duì)齊槐秧,且若文本超出了矩形范圍啄踊,系統(tǒng)會(huì)在他的末尾加上省略號(hào); - 計(jì)算用于繪制文本的矩形的位置和大械蟊辍颠通;
- 調(diào)用
draw(in:withAttributes:)
,繪制文本膀懈; - 使用
bytesFormatter
獲取文本顿锰,并設(shè)置「文件大小」的文本的屬性。這和之前唯一的區(qū)別是:這個(gè)文本用NSFontAttributeName
設(shè)置了一個(gè)不同的顏色启搂。
編譯并運(yùn)行硼控,或者前往 Main.storyboard。
熱烈祝賀條形圖殺青成功胳赌!你現(xiàn)在可以調(diào)整一下窗口的大小牢撼,看看圖例里的文本時(shí)如何給自己加上省略號(hào)來(lái)適應(yīng)狹小的空間的。
給自己鼓個(gè)掌吧??~
Cocoa Drawing
macOS 還提供了使用 AppKit 的框架來(lái)進(jìn)行繪制的選項(xiàng)疑苫。它將會(huì)提供更高層的抽象繪圖法浪默。它使用各種類來(lái)代替 C 語(yǔ)言的函數(shù),它還包含了許多 Helper 方法來(lái)更輕松地應(yīng)對(duì)常見繪圖任務(wù)缀匕。在兩個(gè)框架中纳决,圖形上下文是一樣的。如果你對(duì) Core Graphics 很熟悉的話乡小,你應(yīng)該能很輕松地掌握 Cocoa Drawing阔加。
和 Core Graphics 一樣,你需要?jiǎng)?chuàng)建并繪制路徑满钟,但在 Cocoa Drawing胜榔,我們使用 NSBezierPath
,它和 CGPathRef
是一樣的湃番。
我們要繪制的餅圖是這樣的:
你需要分三步來(lái)繪制它:
- 創(chuàng)建一條圓形的路徑夭织,用于顯示總硬盤空間,然后用定義好的顏色繪制它吠撮;
- 為已用空間創(chuàng)建一條路徑尊惰,并繪制它;
- 為已用空間的路徑繪制一個(gè)漸變填充泥兰。
打開 GraphView.swift弄屡,把這個(gè)方法添加到用于繪制的 extension 里:
func drawPieChart() {
guard let fileDistribution = fileDistribution else {
return
}
// 1
let rect = pieChartRectangle()
let circle = NSBezierPath(ovalIn: rect)
pieChartAvailableFillColor.setFill()
pieChartAvailableLineColor.setStroke()
circle.stroke()
circle.fill()
// 2
let path = NSBezierPath()
let center = CGPoint(x: rect.midX, y: rect.midY)
let usedPercent = Double(fileDistribution.capacity - fileDistribution.available) /
Double(fileDistribution.capacity)
let endAngle = CGFloat(360 * usedPercent)
let radius = rect.size.width / 2.0
path.move(to: center)
path.line(to: CGPoint(x: rect.maxX, y: center.y))
path.appendArc(withCenter: center, radius: radius,
startAngle: 0, endAngle: endAngle)
path.close()
// 3
pieChartUsedLineColor.setStroke()
path.stroke()
}
我們來(lái)分析一下這段代碼:
- 用
init(ovalIn:)
構(gòu)造方法創(chuàng)建一條圓形的路徑,設(shè)置它的填充顏色和筆觸顏色鞋诗,然后繪制這條路徑膀捷; - 為已用空間創(chuàng)建一條路徑:
- 根據(jù)已用空間計(jì)算扇形的角度;
- 移動(dòng)到大圓的圓心削彬;
- 添加一條從圓心到圓的右頂點(diǎn)的線段全庸;
- 根據(jù)之前計(jì)算的角度添加一條圓恍阒佟;
- 閉合圖形壶笼,也就是從圓弧的終點(diǎn)連接到圓心啄育;
- 調(diào)用
stroke()
方法,設(shè)置筆觸顏色拌消;
你應(yīng)該能發(fā)現(xiàn)這段代碼和之前的區(qū)別:
- 代碼中沒(méi)有提到過(guò)圖形上下文挑豌,因?yàn)槲覀冋{(diào)用的方法會(huì)自動(dòng)獲取當(dāng)前的上下文,在這個(gè)例子中墩崩,就是視圖自己的圖形上下文氓英;
- 角是以角度制計(jì)算,而不是弧度制鹦筹。CGFloat+Radians.swift 擴(kuò)展了 CGFloat 類來(lái)進(jìn)行了自動(dòng)轉(zhuǎn)換铝阐。
現(xiàn)在把這行代碼添加到 draw(_:)
方法中來(lái)繪制餅圖:
drawPieChart()
編譯并運(yùn)行:
進(jìn)展不錯(cuò)!
繪制漸變
Cocoa Drawing 使用 NSGradient
來(lái)繪制漸變铐拐。
你需要在已使用的扇形里繪制漸變徘键,該怎么實(shí)現(xiàn)呢……???
沒(méi)錯(cuò)遍蟋,用剪切蒙版按岛Α!
你已經(jīng)創(chuàng)建了一條路徑來(lái)繪制已用空間的扇形虚青,在我們繪制漸變之前它呀,先來(lái)把它用作剪切蒙版。
把這些代碼添加到 drawPieChart()
方法中:
if let gradient = NSGradient(starting: pieChartGradientStartColor,
ending: pieChartGradientEndColor) {
gradient.draw(in: path, angle: Constants.pieChartGradientAngle)
}
第一行代碼會(huì)試著去創(chuàng)建一個(gè)兩種顏色構(gòu)成的漸變棒厘。如果創(chuàng)建成功了纵穿,就會(huì)調(diào)用 draw(in:angle:)
方法來(lái)繪制它。在大括號(hào)里奢人,這個(gè)方法會(huì)設(shè)置蒙版谓媒,并在蒙版區(qū)域內(nèi)繪制漸變。是不是特別棒~
編譯并運(yùn)行:
練習(xí)/挑戰(zhàn):繪制餅圖的圖例
現(xiàn)在我們的自定義視圖已經(jīng)越來(lái)越完美了何乎,但還有一個(gè)待辦事項(xiàng):繪制餅圖的圖例句惯,也就是其內(nèi)部的文字說(shuō)明。
你已經(jīng)知道該怎么去做了宪赶,準(zhǔn)備好接受挑戰(zhàn)了嘛??宗弯?
一些小提示:
- 使用
bytesFormatter
來(lái)獲取硬盤的可用空間(fileDistribution.available
屬性)和總空間(fileDistribution.capacity
屬性)脯燃; - 計(jì)算文本的位置搂妻,確保你的文本顯示在各個(gè)扇形的中央;
- 在計(jì)算好的位置用以下屬性繪制文本:
- Font:
NSFont.pieChartLegendFont
辕棚; - Used space text color:
NSColor.pieChartUsedSpaceTextColor
欲主; - Available space text color:
NSColor.pieChartAvailableSpaceTextColor
邓厕。
- Font:
答案:繪制圖例
把這些代碼添加到 drawPieChart()
方法中:
// 1
let usedMidAngle = endAngle / 2.0
let availableMidAngle = (360.0 - endAngle) / 2.0
let halfRadius = radius / 2.0
// 2
let usedSpaceText = bytesFormatter.string(fromByteCount: fileDistribution.capacity)
let usedSpaceTextAttributes = [
NSFontAttributeName: NSFont.pieChartLegendFont,
NSForegroundColorAttributeName: NSColor.pieChartUsedSpaceTextColor]
let usedSpaceTextSize = usedSpaceText.size(withAttributes: usedSpaceTextAttributes)
let xPos = rect.midX + CGFloat(cos(usedMidAngle.radians)) *
halfRadius - (usedSpaceTextSize.width / 2.0)
let yPos = rect.midY + CGFloat(sin(usedMidAngle.radians)) *
halfRadius - (usedSpaceTextSize.height / 2.0)
usedSpaceText.draw(at: CGPoint(x: xPos, y: yPos),
withAttributes: usedSpaceTextAttributes)
// 3
let availableSpaceText = bytesFormatter.string(fromByteCount: fileDistribution.available)
let availableSpaceTextAttributes = [
NSFontAttributeName: NSFont.pieChartLegendFont,
NSForegroundColorAttributeName: NSColor.pieChartAvailableSpaceTextColor]
let availableSpaceTextSize = availableSpaceText.size(withAttributes: availableSpaceTextAttributes)
let availableXPos = rect.midX + cos(-availableMidAngle.radians) *
halfRadius - (availableSpaceTextSize.width / 2.0)
let availableYPos = rect.midY + sin(-availableMidAngle.radians) *
halfRadius - (availableSpaceTextSize.height / 2.0)
availableSpaceText.draw(at: CGPoint(x: availableXPos, y: availableYPos),
withAttributes: availableSpaceTextAttributes)
代碼含義:
- 計(jì)算兩個(gè)區(qū)域的角度;
- 創(chuàng)建已用空間的文本的屬性扁瓢,并計(jì)算其 x 和 y详恼,然后繪制它;
- 創(chuàng)建總空間的文本的屬性引几,并計(jì)算其 x 和 y昧互,然后繪制它;
現(xiàn)在伟桅,編譯并運(yùn)行你的 app敞掘,好好欣賞一下你的杰出作品:
恭喜你!你使用 Core Graphics 和 Cocoa Drawing 創(chuàng)建了一個(gè)美麗的 app楣铁!
接下來(lái)該做啥玖雁?
你可以點(diǎn)擊這里下載完整的工程文件。
這個(gè) macOS Core Graphics 教程覆蓋了 macOS 中用于繪制自定義視圖的不同框架的基本知識(shí):
- 如何使用 Core Graphics 和 Cocoa Drawing 創(chuàng)建和繪制路徑盖腕;
- 如何剪切一個(gè)區(qū)域赫冬;
- 如何繪制文本串;
- 如何繪制漸變溃列。
之后的日子里劲厌,當(dāng)你需要?jiǎng)?chuàng)建一些整潔、優(yōu)美的用戶界面的時(shí)候听隐,你應(yīng)該能自信地拿出 Core Graphics 和 Cocoa Drawing 揮灑創(chuàng)意了脊僚。
如果你還想繼續(xù)深入,可以參考這些資源:
- Apple 的 Introduction to Cocoa Drawing Guide
- Apple 的 Quartz 2D Programming Guide