[譯] macOS 上的 Core Graphics 入門教程

本文翻譯自 raywenderlich.comCore 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.swiftNSFont+DiskInfo.swift:擴(kuò)展了 NSColor,定義了 app 中會(huì)用到的顏色和字體抹剩;
  • CGFloat+Radians.swift:擴(kuò)展了 CGFloat蚤假,提供了轉(zhuǎn)換角度值和弧度制的 helper 方法;
  • MountedVolumesDataSource.swiftMountedVolumesDelegate.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è)部分中你需要做兩件事:

  1. 創(chuàng)建一個(gè) NSView 的子類瞒爬;
  2. 重寫 draw(_:) 方法弓柱,加入一些用于繪制的代碼。

創(chuàng)建 NSView 的子類

選中項(xiàng)目導(dǎo)航器的 Views 分組侧但,點(diǎn)擊去 Xcode 菜單上的 FileNewFile…矢空,然后點(diǎn)擊 macOSSourceCocoa Class 模版。

點(diǎn)擊 Next禀横,把新的類命名為 GraphView屁药,并讓它繼承自 NSView,把語(yǔ)言選擇為 Swift柏锄。

點(diǎn)擊 NextCreate 來(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 按鈕讥耗,把它的 TopBottom疹启、LeadingTrailing 約束設(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)
  }
}

這一大坨代碼的作用是:

  1. 聲明了帶有許多常量的結(jié)構(gòu)體 —— 你得在這個(gè) app 的各個(gè)地方用到它們;
  2. @IBInspectable 修飾所有可配置的屬性患雏。并使用 NSColor+DiskInfo.swift 中的值為它們賦值鹏溯。注意:要使一個(gè)屬性「inspectable」(可以在 Interface Builder 里直接編輯),你必須聲明它的類型淹仑,即使大多數(shù)情況下 Swift 會(huì)幫你做這件事兒丙挽;
  3. 定義一個(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ǔ)言解釋一遍就是:

  1. 創(chuàng)建一個(gè)可以改變的路徑撕攒;
  2. 一步一步勾勒出一個(gè)圓角矩形:
    • 移動(dòng)到矩形底邊的中點(diǎn)陡鹃,這里將是我們的起點(diǎn);
    • 使用 addArc(tangent1End:tangent2End:radius) 方法繪制右下角的線段抖坪,這個(gè)方法會(huì)繪制出底部的水平線以及右下角的圓角萍鲸;
    • 添加右邊的線段和右上角的圓角;
    • 添加頂部的線段和左上角的圓角擦俐;
    • 添加左邊的線段和左下角的圓角脊阴;
    • 閉合路徑,也就是從上一步的重點(diǎn)連接到起點(diǎn);
  3. 設(shè)置繪制屬性:線寬嘿期、填充顏色和邊框顏色品擎;
  4. 把路徑添加到圖形上下文,并使用 .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ì)算:

  1. 從計(jì)算餅圖的位置開始入手,它將會(huì)垂直居中蹭睡,并占據(jù)視圖 1/3 的寬度衍菱;
  2. 計(jì)算條形圖的位置,它將會(huì)占據(jù) 2/3 的寬度肩豁,并處于視圖中部偏上的位置脊串;
  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
  }
}

這些代碼做了這些事兒:

  1. 先確保了 Graph View 擁有一個(gè)有效的 fileDistribution昂勉;
  2. 遍歷 fileDistribution 里的每一種文件類型;
  3. 根據(jù)文件的占比計(jì)算蒙版的大猩ㄏ佟岗照;
  4. 存儲(chǔ)圖形上下文的狀態(tài),設(shè)置蒙版的大小笆环,用文件類型對(duì)應(yīng)的顏色繪制圓角矩形攒至,并恢復(fù)圖形上下文的狀態(tài);
  5. 把剪切蒙版的 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ì)齊湿故,把它傳入 Stringdraw(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)單:

  1. 你已經(jīng)很熟悉這一段代碼了:計(jì)算圖例的彩色方塊的位置屑墨,為它創(chuàng)建一條路徑躁锁,并用對(duì)應(yīng)的顏色填充;
  2. 創(chuàng)建一個(gè)字典卵史,包含了兩個(gè)屬性:字體和 NSMutableParagraphStyle战转。后者會(huì)定義這些文本會(huì)怎樣在給定的矩形里被繪制出來(lái)。在這個(gè)例子中以躯,文本會(huì)顯示為左對(duì)齊槐秧,且若文本超出了矩形范圍啄踊,系統(tǒng)會(huì)在他的末尾加上省略號(hào);
  3. 計(jì)算用于繪制文本的矩形的位置和大械蟊辍颠通;
  4. 調(diào)用 draw(in:withAttributes:),繪制文本膀懈;
  5. 使用 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)繪制它:

  1. 創(chuàng)建一條圓形的路徑夭织,用于顯示總硬盤空間,然后用定義好的顏色繪制它吠撮;
  2. 為已用空間創(chuàng)建一條路徑尊惰,并繪制它;
  3. 為已用空間的路徑繪制一個(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)分析一下這段代碼:

  1. init(ovalIn:) 構(gòu)造方法創(chuàng)建一條圓形的路徑,設(shè)置它的填充顏色和筆觸顏色鞋诗,然后繪制這條路徑膀捷;
  2. 為已用空間創(chuàng)建一條路徑:
    • 根據(jù)已用空間計(jì)算扇形的角度;
    • 移動(dòng)到大圓的圓心削彬;
    • 添加一條從圓心到圓的右頂點(diǎn)的線段全庸;
    • 根據(jù)之前計(jì)算的角度添加一條圓恍阒佟;
    • 閉合圖形壶笼,也就是從圓弧的終點(diǎn)連接到圓心啄育;
  3. 調(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)了嘛??宗弯?

一些小提示:

  1. 使用 bytesFormatter 來(lái)獲取硬盤的可用空間(fileDistribution.available 屬性)和總空間(fileDistribution.capacity 屬性)脯燃;
  2. 計(jì)算文本的位置搂妻,確保你的文本顯示在各個(gè)扇形的中央;
  3. 在計(jì)算好的位置用以下屬性繪制文本:
    • Font:NSFont.pieChartLegendFont辕棚;
    • Used space text color:NSColor.pieChartUsedSpaceTextColor欲主;
    • Available space text color:NSColor.pieChartAvailableSpaceTextColor邓厕。

答案:繪制圖例

把這些代碼添加到 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)

代碼含義:

  1. 計(jì)算兩個(gè)區(qū)域的角度;
  2. 創(chuàng)建已用空間的文本的屬性扁瓢,并計(jì)算其 xy详恼,然后繪制它;
  3. 創(chuàng)建總空間的文本的屬性引几,并計(jì)算其 xy昧互,然后繪制它;

現(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ù)深入,可以參考這些資源:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末遵绰,一起剝皮案震驚了整個(gè)濱河市辽幌,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌椿访,老刑警劉巖乌企,帶你破解...
    沈念sama閱讀 216,843評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異成玫,居然都是意外死亡加酵,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,538評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門哭当,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)猪腕,“玉大人,你說(shuō)我怎么就攤上這事钦勘÷希” “怎么了?”我有些...
    開封第一講書人閱讀 163,187評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵彻采,是天一觀的道長(zhǎng)腐缤。 經(jīng)常有香客問(wèn)我捌归,道長(zhǎng),這世上最難降的妖魔是什么岭粤? 我笑而不...
    開封第一講書人閱讀 58,264評(píng)論 1 292
  • 正文 為了忘掉前任惜索,我火速辦了婚禮,結(jié)果婚禮上剃浇,老公的妹妹穿的比我還像新娘巾兆。我一直安慰自己,他們只是感情好虎囚,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,289評(píng)論 6 390
  • 文/花漫 我一把揭開白布臼寄。 她就那樣靜靜地躺著,像睡著了一般溜宽。 火紅的嫁衣襯著肌膚如雪吉拳。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,231評(píng)論 1 299
  • 那天适揉,我揣著相機(jī)與錄音留攒,去河邊找鬼。 笑死嫉嘀,一個(gè)胖子當(dāng)著我的面吹牛炼邀,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播剪侮,決...
    沈念sama閱讀 40,116評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼拭宁,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了瓣俯?” 一聲冷哼從身側(cè)響起杰标,我...
    開封第一講書人閱讀 38,945評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎彩匕,沒(méi)想到半個(gè)月后腔剂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,367評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡驼仪,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,581評(píng)論 2 333
  • 正文 我和宋清朗相戀三年掸犬,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绪爸。...
    茶點(diǎn)故事閱讀 39,754評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡湾碎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出奠货,到底是詐尸還是另有隱情介褥,我是刑警寧澤,帶...
    沈念sama閱讀 35,458評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站呻顽,受9級(jí)特大地震影響雹顺,放射性物質(zhì)發(fā)生泄漏丹墨。R本人自食惡果不足惜廊遍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,068評(píng)論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望贩挣。 院中可真熱鬧喉前,春花似錦、人聲如沸王财。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,692評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)绒净。三九已至见咒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間挂疆,已是汗流浹背改览。 一陣腳步聲響...
    開封第一講書人閱讀 32,842評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留缤言,地道東北人宝当。 一個(gè)月前我還...
    沈念sama閱讀 47,797評(píng)論 2 369
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像胆萧,于是被迫代替她去往敵國(guó)和親庆揩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,654評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容