Skip to content

Latest commit

 

History

History
1757 lines (1736 loc) · 82.7 KB

Core Graphics on macOS Tutorial.md

File metadata and controls

1757 lines (1736 loc) · 82.7 KB

macOS教程:Core Graphics

更新于2016年9月22日: 本教程已更新以适配Xcode 8及Swift 3。

OSXCoreGraphics-feature

你一定见到过很多app带有美丽的图形和时髦的view。它们会带给你深刻的影响,因为它们是 so 如此 得漂亮。

Core Graphics 是苹果的2D绘制引擎,它是macOS和iOS中最酷的框架之一。它能够绘制任何你可以想象到的东西,从最简单的形状,文本,到最复杂的视觉效果,包括阴影,渐变效果等。

在这篇macOS的Core Graphics教程中,你将会创建一个名为 DiskInfo 的app,在其中包含一个可以展示某硬盘可用空间及文件分布的view。这将是一个很好的例子,来说明如何使用Core Graphics来讲一个枯燥无味,基于文本的界面变得漂亮起来:

Disc information drawn with Core Graphics

跟随教程你可以了解到如何:

  • 创建及配置一个标准的view,它是任何图形元素的基础的层
  • 实现实时的渲染,这样你就不必每次改变图形的时候,都重新运行项目了
  • 通过使用路径、填充、裁切及文本,来用代码进行绘制
  • 使用 Cocoa Drawing ,一个可以在 AppKit 的app下可用的工具,它给出了更高层级的类和方法

在本教程的第一部分,你将使用Core Graphics来实现条状的图表,之后再来学习如何使用Cocoa Drawing来绘制饼状图。

所以戴上你的画家帽子,开始学习如何绘制你的世界吧。

入门

首先,从 here 这里 下载DiskInfo的初始项目。运行它。

Original view without any Core Graphics drawing

这个app列出了你全部的硬盘,当你点击其中一个的时候,详细的信息就会被展示出来。

在继续下一步之前,查看一下项目的结构,来熟悉代码的分布:

sshot-starterproject-structure-xcode-edit

需要进行一下指导?

  • ViewController.swift :app的主view controller
  • VolumeInfo.swift :包含了 VolumeInfo 类的实现,它可以从硬盘中读取信息,而 FilesDistribution 结构体则用来处理文件类型间的拆分
  • NSColor+DiskInfo.swift NSFont+DiskInfo.swift :extension,定义了默认颜色和字体常量
  • CGFloat+Radians.swift :extension提供了助手方法,以便在角度和弧度之间转换
  • MountedVolumesDataSource.swift MountedVolumesDelegate.swift :实现了要求的方法,在outline view中展示硬盘的信息

注意: 这个app展示了正确的硬盘使用信息,但由于教程本身的原因,它只是提供了随机的文件分布。

在每次运行app时,计算真实的文件分布,这将耗尽所有的时间,破坏你的乐趣,没有人想要这样。

创建自定义的view

你要做的第一件事是创建一个自定义的view,名为 GraphView 。你将在这里绘制饼状和条状的图表,因此它相当得重要。

你需要完成两个目标来创建自定义的view:

  1. 创建一个 NSView 的子类。
  2. 重写 draw(\_:) ,添加一些绘制的代码。

在一个较高的层面看,它就是如此得容易。继续下列的步骤来了解如何实现这点。

创建NSView的子类

在Project Navigator中选择 Views 组。依次选择 File \ New \ File… ,并选择 macOS \ Source \ Cocoa Class 文件模板。

点击 Next ,并在确认界面中,将新的类命名为 GraphView ,并将其作为 NSView 的子类,并确保语言为 Swift

点击 Next 创建 并保存你的新文件。

打开 Main.storyboard ,并找到 View Controller 场景。从 Objects Inspector 中拖拽一个 Custom View 到它上面,就像下面这样:

sshot-drag-customview

选择这个view,并在 Identity Inspector 中。将类名设置为 GraphView

sshot-attrinspector-change-class

现在你需要一些约束来进行布局,所以选中这个view,在自动布局工具栏中点击 Pin 按钮。在弹出的面板中,将 Top Bottom Leading Trailing 约束都设置为0,然后点击 Add 4 Constraints 按钮。

sshot-addconstraints-trim

点击自动布局工具栏中三角形的 Resolve Auto Layout Issues 按钮,在 Selected Views 部分中,点击 Update Frames - 它现在看起来是被禁用的,所以点击其它任意地方来取消选择GraphView,然后在重新选择它。

sshot-updateframes-2-trim

重写draw(\_:)

打开 GraphView.swift 。你将看到 Xcode 已创建了 draw(\_:) 的默认的实现。将其中的注释替换为下列的代码,并确保你保留着对父类方法的调用:

NSColor.white.setFill()
NSRectFill(bounds)

首先你将填充颜色设置为白色,然后调用 NSRectFill 方法来填充这个view的背景。

运行项目。

sshot-build-run-whitecolor

你自定义的view的背景现在已由标准灰变为了白色。

first-custom-view

是的,创建自定义绘制的view就是如此的容易。

实时渲染:@IBDesignable 和 @IBInspectable

Xcode 6引入了一个令人震惊的特性:实时渲染。它让你可以在Interface Builder中查看你自定义view的样子 - 无需运行项目。

要打开这个特性,你只需在你的类中添加 @IBDesignable 标注,且可选择的,你可以实现 prepareForInterfaceBuilder() 方法来提供一些样本的数据。

打开 GraphView.swift ,并在类的声明前添加下列的代码:

@IBDesignable

现在,你需要提供一些样本数据。在 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)
}

这定义了 fileDistribution 属性,用来储存硬盘信息。当这个property发生变化的时候,就将这个view的 needsDisplay property设置为 true ,来强制这个view重新绘制它的内容。

然后实现 prepareForInterfaceBuilder() 方法来创建一个样本的文件分布,Xcode就会用它来渲染这个view。

注意 :你也可以在Interface Builder中实时地改变你自定义view的可视的属性。你只需添加 @IBInspectable 这个标注到这个property上。

接下来:让这个view中所有的可视的property标注@IBInspectable。在 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. 使用常量来声明一个结构体 - magic number是代码中的禁忌 - 你在整个教程中都会使用到它们。
  2. 将这个view中全部可配置的property标注为 @IBInspectable 的,并使用在 NSColor+DiskInfo.swift 中的值来设置它们。专业提示:要让一个property可检视化,你必须声明它的类型,即使这个显然可以从它的内容中推断出来。
  3. 声明一个助手方法,来依据文件的类型返回线条和填充的颜色。它会让你在绘制文件分布时非常方便。

打开 Main.storyboard 并查看graph view。它现在已使用白色提换了默认的颜色,意味着实时渲染已可以work。如果没看马上看到效果的话,请保持耐心;它可能需要一两秒的时间来进行渲染。

sshot-render-white-trim

选择graph view并打开 Attributes Inspector 。你就可以看到刚刚添加的所有inspectable的property了。

sshot-ibdesignable-trim

现在,你就既可以运行app来查看效果,也可以直接在Interface Builder来查看了。

是时候来进行绘制了。

图形的上下文

当你使用Core Graphics,你不会直接绘制到view上,而是使用一个 Graphics Context ,系统会在这里把绘制的内容渲染出来并展示到view上。

image-rendering

Core Graphics应用了一个“画家模型”,因此当你在上下文中进行绘制的时候,请想象你正在一张画布上嗖嗖地进行绘图。你画下一条条的路径,并进行填充,然后又画下另一条路径,又进行填充。你已经画下的像素是不可以被擦除的,但可以再画下另外的像素来覆盖它。

这里的概念是非常重要的,因为这个顺序将影响到最终的结果。

image-drawing-order

用路径来绘制形状

要在Core Graphics中绘制一个形状,你就需要设定它的 path ,在Core Graphics中它是由 CGPathRef 类型所代表的,相应地,它的可变版本的类型则是 CGMutablePathRef 。path只是形状的一个向量的表示。它无法自己去绘制自己。

当你的路径完成后,你就可以把它添加到上下文中了。上下文就会使用路径和绘制属性等信息来渲染出你所期待的图形。

为条状图表绘制路径

圆角矩形是条状图表中的基本形状,所以就从这里开始吧。

打开 GraphView.swift 并添加下列的extension到文件尾部,类定义的外部:

// MARK: - Drawing 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)
}
}

就是你绘制圆角矩形的方法。以下是更容易理解的解释:

  1. 创建一个可变的path。
  2. 构建圆角矩形的path,跟随以下步骤:
    • 首先移动到矩形底部的中心点。
    • 使用 addArc(tangent1End:tangent2End:radius) 方法来绘制矩形右下侧的部分。这个方法将会绘制水平的线及圆角。
    • 添加右侧的线段及右上角的弧线。
    • 添加顶部的线段及左上角的弧线。
    • 添加左侧的线段及左下角的弧线。
    • 闭合路径,这将在最后一个点到起始的点之间绘制一条线段。
  3. 设置绘制属性:线条粗细,填充颜色和边界颜色。
  4. 将路径添加到上下文中,并使用 .fillStroke 参数来绘制它,该参数会告知Core Graphics去填充矩形并绘制边框。

你再也不会以同样的方式来看矩形了!以下是上述代码运行的结果:

image-roundedrect

注意 :更多有关path绘制如何工作的内容,请参考苹果官方文档 Quartz 2D Programming Guide 中Paths & Arcs部分。

计算条状图表的位置

用Core Graphics进行绘制,基本上全都在是计算在你的view上可视元素的位置。规划将不同的元素放在什么地方,以及当所在view的尺寸发生变化时,如何做出相应的改变,是非常得重要的。

这里是你自定义的view的布局:

image-viewlayout

打开 GraphView.swift 并添加下列的extension:

// MARK: - Calculations 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
}
}

上述的代码实现了以下所需求的计算:

  1. 从计算饼状图表的位置开始 - 它位于垂直居中的位置,并占据了此view宽度的三分之一。
  2. 计算条状图表的位置。它占据了三分之二的宽度,并位于此view垂直中心靠上的位置。
  3. 接下来计算图形说明的位置,基于饼状图最小的Y位置以及此view的边缘。

现在该将它绘制到你的view上了。在 GraphView 的绘制extension中添加下面的方法:

func drawBarGraphInContext(context: CGContext?) {
let barChartRect = barChartRectangle()
drawRoundedRect(rect: barChartRect, inContext: context,
radius: Constants.barChartCornerRadius,
borderColor: barChartAvailableLineColor.cgColor,
fillColor: barChartAvailableFillColor.cgColor)
}

上面的代码添加了一个用来绘制条状图表的助手方法。它使用可用空间的填充和线条的颜色,绘制了一个圆角矩形作为背景。你可以在 NSColor+DiskInfo extension中找到这些颜色。

使用下列代码替换 draw(_:) 方法中的全部内容:

super.draw(dirtyRect)
let context = NSGraphicsContext.current()?.cgContext
drawBarGraphInContext(context: context)

此处才是绘制真正发生的地方。首先,你通过调用 NSGraphicsContext.current() 方法获取到了这个view当前的图形上下文,接着调用drawBarGraphInContext(context:)绘制出条状的图表。

运行项目。你会看到条状图表已出现在了合适的位置上。

sshot-build-run-barchart-first
现在,打开 Main.storyboard 并选择 View Controller 场景。

你会看到:

sshot-live-render-barchart-first

Interface Builder现在已经实时地将这个view绘制出来了。你也可以换一些颜色,这个view就会做出相应的变化。是不是很酷?

裁切区域

这一部分将会完成分布图表,也就是一个看起来像这样子的图表:

barchart

先来扯一通理论吧。正如你所知道的,每个文件类型都有它自己的颜色,因此app需要基于相应文件大小所占的百分比,计算其对应的条的宽度,然后用相应的颜色绘制出来。

你可以创建一个特定的形状,例如用顶部和底部的两条线构成的填充好的矩形,然后绘制出来。然而,有另一种技术可以让你复用你的代码来获得相同的结果: 裁切区域

你可以吧裁切区域看做是一张被挖过一个洞的纸,然后把你放到你的画布上方:这样你就只能看到洞中透出来的部分了。这个“洞”被称作是裁切蒙版,在Core Graphics中由一个path来指定。

在条状图表中,你就可以为每种文件类型创建被完全填充的相同的条,然后使用裁切蒙版来只展示正确的比例,就像下图中所示的一样:

image-clippingarea

了解了裁切区域的工作原理,你就可以把条状图表搞出来了。

在绘制之前,你需要设置当一个一篇被选中时 fileDistribution 的值。打开 Main.storyboard ,并前往 View Controller 来创建一个outlet。

在Project Navigator中 按住Option点击 ViewController.swift ,在 Assistant Editor 中打开它,然后 按住Control 把graph view拖拽到view controller的源码中来为它创建一个outlet。

image-outlet-1

在弹出的面板中,将outlet命名为 graphView ,并点击 Connect

image-outlet-2

打开 ViewController.swift ,并在 showVolumeInfo(_:) 的尾部添加下列代码:

graphView.fileDistribution = volume.fileDistribution

根据被选择的硬盘,设置 fileDistribution 的值。

打开 GraphView.swift ,并在 drawBarGraphInContext(context:) 方法的尾部添加下列代码以绘制条状图表:

// 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. 确保这里的 fileDistribution 有效。
  2. 迭代全部的文件类型。
  3. 基于文件类型的百分比和前一文件类型,计算裁切的rect。
  4. 保存上下文的状态,设置裁切的区域,并使用对应文件类似的颜色绘制矩形。然后再恢复上下文的状态。
  5. 在下一次迭代之前移动裁切rect的 x 位置。

你可能会好奇为什么要保存及恢复上下文的状态。还记的画家模式么?你添加到上下文的任何内容都会保留到这里。

如果你添加多个裁切区域,你实际上就会创建一个涵盖所有裁切区域的大的裁切区域。为了避免这样,你就要在添加裁切区域之前,保存状态,然后当你用到的时候,再恢复上下文到这个状态,如此来处理裁切区域。

现在,由于 index 从未被使用过, Xcode 会提示一个warning。不必担心,我们将在后面修复它。

运行项目,或打开 Main.storyboard 以在Interface Builder中查看它。

sshot-build-rung-graphbar-drawn

看起来我们已经实现了一些功能了。条状的图表已几乎完成,你只需要为它添加一些标记说明。

绘制文本

在自定义的view中绘制文本超级得简单。你只需要创建一个文本属性的字典 - 例如字体,大小,颜色,对齐方式 - 计算好要绘制到的rect的位置,然后调用 String draw(in:withAttributes:) 方法就可以了。

打开 GraphView.swift 并添加下列的property到类中:

fileprivate var bytesFormatter = ByteCountFormatter()

这就创建了一个 ByteCountFormatter 。它承担了将字节转换为人类可读的文本的重要工作。

现在,添加下列的代码到 drawBarGraphInContext(context:) 中。确保添加到 for (index,fileType) in fileTypes.enumerated() 的循环中:

// 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)

相当多的代码,但要看看懂并不困难:

  1. 你早已熟悉了这个代码:计算说明颜色块的位置,创建它的路径并使用合适的颜色进行绘制。
  2. 创建一个包含有字体和段落格式 NSMutableParagraphStyle 的属性的字典。这个段落格式会确定文本如何被绘制到给定的矩形中。在本例中,它被设定为靠左对齐及过长时省略尾部。
  3. 计算文本将要被绘制到的矩形的范围。
  4. 使用 draw(in:withAttributes:) 方法绘制文本。
  5. 使用 bytesFormatter 获取文本的尺寸大小,并为文件大小的文本创建属性。这里相对于之前的代码,主要的区别是它通过 NSFontAttributeName 在属性字典中设置了一个不同的文本颜色。

运行项目,或直接打开 Main.storyboard 来查看效果。

sshot-build-run-graphbar-legend

条状的图表完成了!你可以调整窗口的大小,看查看它如何适配新的尺寸。注意观察当没有足够大的空间时,文本如何被绘制。

是不是很酷!

Cocoa Drawing

macOS 提供了使用 AppKit 框架来完成绘制的替代选项。它提供了更高层次抽象的方法。他用类来替换 C 的函数,并提供助手方法来使得执行一些常见的任务变得更加容易。概念在两个框架中都是相同的,如果你熟悉Core Graphics的话,Cocoa Drawing是非常容易上手的。

正如在Core Graphics中所做的一样,你需要使用 NSBezierPath 来创建和绘制路径,它是在Cocoa Drawing中等价于 CGPathRef 的对象:

饼状图看起来应该是这样的:

piechart

你会分三步来绘制它:

pichart-steps

  • 首先,你创建了一个圆形的路径来表示可用的空间,然后用配置好的颜色来描边和填充它。
  • 然后为已用的空间创建相应的path,并描出来。
  • 最后,基于已用部分路径,绘制出渐变的效果。

打开 GraphView.swift 并添加下列的方法到绘制的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()
}

上述代码:

  1. 使用构造器 init(ovalIn:) 来创建一个圆形的path,设定好笔划和填充的颜色,然后绘制path。
  2. 为已用部分的圆创建相应的路径。首先,基于已用的空间计算结束处的角度。然后分四步来创建path:
    1. 移动到圆形的中点。
    2. 从中心到圆形的右侧添加一条线。
    3. 从当前的点添加一条弧线到刚计算出的角度。
    4. 闭合路径。然后再从弧形结束的点处连接一条线到圆形的中心。
  3. 设定好笔划的颜色,并使用 stroke() 方法来绘制path。

你会发现很多相对于Core Graphics不同的情况:

  • 在代码中没有对图形上下文的引用。这是因为这些方法会自动获取当前的上下文,在这个case中,它就是当前view的上下文了。
  • 这里需要的是角度,而不是弧度。所以需要的话,可使用 CGFloat+Radians.swift 中的方法来进行转换。

现在,添加下列的代码到 draw(_:) 中来绘制饼状图表:

drawPieChart()

运行项目。
sshot-build-rung-pie-stroke

看起来是不好赞!

绘制渐变效果

Cocoa Drawing 使用 NSGradient 来绘制渐变效果。

你需要把渐变的效果绘制到可用部分的圆中,你早已知道该怎么做了。

没错,就是裁切区域!

你早已创建了一条用来进行绘制的路径,在绘制渐变效果之前,你就可以把它当做一个裁切区域来用。

添加下列的代码到 drawPieChart() 的尾部:

if let gradient = NSGradient(starting: pieChartGradientStartColor,
ending: pieChartGradientEndColor) {
gradient.draw(in: path, angle: Constants.pieChartGradientAngle)
}

在第一行代码中,你尝试用两种颜色来创建一个渐变效果。如果创建成功的话,就调用 draw(in:angle:) 方法来绘制它。这个方法在内部设置了裁切区域,并绘制渐变的效果。这样子是不很棒?

运行项目。

sshot-build-run-gradient

现在这个自定义的view已经看起来越来越棒了。现在就剩一件事需要做了:在饼状图表中绘制可用和已用空间的标记文本。

你早就知道该怎么做了吧。想不想挑战一下?:]

下面是你需要做的事:

  1. 使用 bytesFormatter 来获取可用空间的文本( fileDistribution.available property)以及全部空间( fileDistribution.capacity property)。
  2. 计算文本的位置,把它放到可用和已用部分的中点处。
  3. 使用下列的熟悉绘制文本:
    • 字体: NSFont.pieChartLegendFont
    • 已用空间文本的颜色: NSColor.pieChartUsedSpaceTextColor
    • 可用空间文本的颜色: 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. 计算你绘制已用和可用部分文本位置相应的角度。
  2. 创建文本的属性,并计算已用空间文本的位置 x y - 然后绘制出来。
  3. 创建文本的属性,并计算可用空间文本的位置 x y - 然后绘制出来。

现在,运行项目来查看你最终的作品吧。

Final app made using Core Graphics on macOS

祝贺!你已经用Core Graphics和Cocoa Drawing构建了一个超美的app!

从这儿去向哪里

你可以从 这里 下载最终完整的项目。

本教程已覆盖了在 macOS 中,可用来在定制的view上进行绘制的不同的框架。你已经了解了:

  • 如何使用Core Graphics和Cocoa Drawing创建及绘制path
  • 如何使用裁切区域
  • 如何绘制文本
  • 如何绘制渐变效果

现在,你应当非常自信地使用Core Graphics和Cocoa Drawing去绘制漂亮的、相应式的图形了。

如果你想要学到更多相关的内容,可以参考下列的资源:

  • Introduction to Cocoa Drawing Guide
  • Quartz 2D Programming Guide