原文地址 翻译:DeveloperLx
更新于2016年9月22日: 本教程已更新以适配Xcode 8及Swift 3。
你一定见到过很多app带有美丽的图形和时髦的view。它们会带给你深刻的影响,因为它们是 so 如此 得漂亮。
Core Graphics 是苹果的2D绘制引擎,它是macOS和iOS中最酷的框架之一。它能够绘制任何你可以想象到的东西,从最简单的形状,文本,到最复杂的视觉效果,包括阴影,渐变效果等。
在这篇macOS的Core Graphics教程中,你将会创建一个名为 DiskInfo 的app,在其中包含一个可以展示某硬盘可用空间及文件分布的view。这将是一个很好的例子,来说明如何使用Core Graphics来讲一个枯燥无味,基于文本的界面变得漂亮起来:
跟随教程你可以了解到如何:
- 创建及配置一个标准的view,它是任何图形元素的基础的层
- 实现实时的渲染,这样你就不必每次改变图形的时候,都重新运行项目了
- 通过使用路径、填充、裁切及文本,来用代码进行绘制
- 使用 Cocoa Drawing ,一个可以在 AppKit 的app下可用的工具,它给出了更高层级的类和方法
在本教程的第一部分,你将使用Core Graphics来实现条状的图表,之后再来学习如何使用Cocoa Drawing来绘制饼状图。
所以戴上你的画家帽子,开始学习如何绘制你的世界吧。
首先,从 here 这里 下载DiskInfo的初始项目。运行它。
这个app列出了你全部的硬盘,当你点击其中一个的时候,详细的信息就会被展示出来。
在继续下一步之前,查看一下项目的结构,来熟悉代码的分布:
需要进行一下指导?
- 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,名为
GraphView
。你将在这里绘制饼状和条状的图表,因此它相当得重要。
你需要完成两个目标来创建自定义的view:
-
创建一个
NSView
的子类。 -
重写
draw(\_:)
,添加一些绘制的代码。
在一个较高的层面看,它就是如此得容易。继续下列的步骤来了解如何实现这点。
在Project Navigator中选择 Views 组。依次选择 File \ New \ File… ,并选择 macOS \ Source \ Cocoa Class 文件模板。
点击
Next
,并在确认界面中,将新的类命名为
GraphView
,并将其作为
NSView
的子类,并确保语言为
Swift
。
点击 Next 来 创建 并保存你的新文件。
打开 Main.storyboard ,并找到 View Controller 场景。从 Objects Inspector 中拖拽一个 Custom View 到它上面,就像下面这样:
选择这个view,并在
Identity Inspector
中。将类名设置为
GraphView
。
现在你需要一些约束来进行布局,所以选中这个view,在自动布局工具栏中点击 Pin 按钮。在弹出的面板中,将 Top , Bottom , Leading 和 Trailing 约束都设置为0,然后点击 Add 4 Constraints 按钮。
点击自动布局工具栏中三角形的 Resolve Auto Layout Issues 按钮,在 Selected Views 部分中,点击 Update Frames - 它现在看起来是被禁用的,所以点击其它任意地方来取消选择GraphView,然后在重新选择它。
打开
GraphView.swift
。你将看到
Xcode
已创建了
draw(\_:)
的默认的实现。将其中的注释替换为下列的代码,并确保你保留着对父类方法的调用:
NSColor.white.setFill()
NSRectFill(bounds)
首先你将填充颜色设置为白色,然后调用
NSRectFill
方法来填充这个view的背景。
运行项目。
你自定义的view的背景现在已由标准灰变为了白色。
是的,创建自定义绘制的view就是如此的容易。
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)
}
}
上述代码:
- 使用常量来声明一个结构体 - magic number是代码中的禁忌 - 你在整个教程中都会使用到它们。
- 将这个view中全部可配置的property标注为 @IBInspectable 的,并使用在 NSColor+DiskInfo.swift 中的值来设置它们。专业提示:要让一个property可检视化,你必须声明它的类型,即使这个显然可以从它的内容中推断出来。
- 声明一个助手方法,来依据文件的类型返回线条和填充的颜色。它会让你在绘制文件分布时非常方便。
打开 Main.storyboard 并查看graph view。它现在已使用白色提换了默认的颜色,意味着实时渲染已可以work。如果没看马上看到效果的话,请保持耐心;它可能需要一两秒的时间来进行渲染。
选择graph view并打开 Attributes Inspector 。你就可以看到刚刚添加的所有inspectable的property了。
现在,你就既可以运行app来查看效果,也可以直接在Interface Builder来查看了。
是时候来进行绘制了。
当你使用Core Graphics,你不会直接绘制到view上,而是使用一个 Graphics Context ,系统会在这里把绘制的内容渲染出来并展示到view上。
Core Graphics应用了一个“画家模型”,因此当你在上下文中进行绘制的时候,请想象你正在一张画布上嗖嗖地进行绘图。你画下一条条的路径,并进行填充,然后又画下另一条路径,又进行填充。你已经画下的像素是不可以被擦除的,但可以再画下另外的像素来覆盖它。
这里的概念是非常重要的,因为这个顺序将影响到最终的结果。
要在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)
}
}
这 就是你绘制圆角矩形的方法。以下是更容易理解的解释:
- 创建一个可变的path。
- 构建圆角矩形的path,跟随以下步骤:
- 首先移动到矩形底部的中心点。
-
使用
addArc(tangent1End:tangent2End:radius)
方法来绘制矩形右下侧的部分。这个方法将会绘制水平的线及圆角。 - 添加右侧的线段及右上角的弧线。
- 添加顶部的线段及左上角的弧线。
- 添加左侧的线段及左下角的弧线。
- 闭合路径,这将在最后一个点到起始的点之间绘制一条线段。
- 设置绘制属性:线条粗细,填充颜色和边界颜色。
-
将路径添加到上下文中,并使用
.fillStroke
参数来绘制它,该参数会告知Core Graphics去填充矩形并绘制边框。
你再也不会以同样的方式来看矩形了!以下是上述代码运行的结果:
注意 :更多有关path绘制如何工作的内容,请参考苹果官方文档 Quartz 2D Programming Guide 中Paths & Arcs部分。
用Core Graphics进行绘制,基本上全都在是计算在你的view上可视元素的位置。规划将不同的元素放在什么地方,以及当所在view的尺寸发生变化时,如何做出相应的改变,是非常得重要的。
这里是你自定义的view的布局:
打开 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
}
}
上述的代码实现了以下所需求的计算:
- 从计算饼状图表的位置开始 - 它位于垂直居中的位置,并占据了此view宽度的三分之一。
- 计算条状图表的位置。它占据了三分之二的宽度,并位于此view垂直中心靠上的位置。
- 接下来计算图形说明的位置,基于饼状图最小的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:)绘制出条状的图表。
运行项目。你会看到条状图表已出现在了合适的位置上。
现在,打开
Main.storyboard
并选择
View Controller
场景。
你会看到:
Interface Builder现在已经实时地将这个view绘制出来了。你也可以换一些颜色,这个view就会做出相应的变化。是不是很酷?
这一部分将会完成分布图表,也就是一个看起来像这样子的图表:
先来扯一通理论吧。正如你所知道的,每个文件类型都有它自己的颜色,因此app需要基于相应文件大小所占的百分比,计算其对应的条的宽度,然后用相应的颜色绘制出来。
你可以创建一个特定的形状,例如用顶部和底部的两条线构成的填充好的矩形,然后绘制出来。然而,有另一种技术可以让你复用你的代码来获得相同的结果: 裁切区域 。
你可以吧裁切区域看做是一张被挖过一个洞的纸,然后把你放到你的画布上方:这样你就只能看到洞中透出来的部分了。这个“洞”被称作是裁切蒙版,在Core Graphics中由一个path来指定。
在条状图表中,你就可以为每种文件类型创建被完全填充的相同的条,然后使用裁切蒙版来只展示正确的比例,就像下图中所示的一样:
了解了裁切区域的工作原理,你就可以把条状图表搞出来了。
在绘制之前,你需要设置当一个一篇被选中时
fileDistribution
的值。打开
Main.storyboard
,并前往
View Controller
来创建一个outlet。
在Project Navigator中 按住Option点击 ViewController.swift ,在 Assistant Editor 中打开它,然后 按住Control 把graph view拖拽到view controller的源码中来为它创建一个outlet。
在弹出的面板中,将outlet命名为
graphView
,并点击
Connect
。
打开
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
}
}
上述代码:
-
确保这里的
fileDistribution
有效。 - 迭代全部的文件类型。
- 基于文件类型的百分比和前一文件类型,计算裁切的rect。
- 保存上下文的状态,设置裁切的区域,并使用对应文件类似的颜色绘制矩形。然后再恢复上下文的状态。
- 在下一次迭代之前移动裁切rect的 x 位置。
你可能会好奇为什么要保存及恢复上下文的状态。还记的画家模式么?你添加到上下文的任何内容都会保留到这里。
如果你添加多个裁切区域,你实际上就会创建一个涵盖所有裁切区域的大的裁切区域。为了避免这样,你就要在添加裁切区域之前,保存状态,然后当你用到的时候,再恢复上下文到这个状态,如此来处理裁切区域。
现在,由于
index
从未被使用过,
Xcode
会提示一个warning。不必担心,我们将在后面修复它。
运行项目,或打开 Main.storyboard 以在Interface Builder中查看它。
看起来我们已经实现了一些功能了。条状的图表已几乎完成,你只需要为它添加一些标记说明。
在自定义的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)
相当多的代码,但要看看懂并不困难:
- 你早已熟悉了这个代码:计算说明颜色块的位置,创建它的路径并使用合适的颜色进行绘制。
-
创建一个包含有字体和段落格式
NSMutableParagraphStyle
的属性的字典。这个段落格式会确定文本如何被绘制到给定的矩形中。在本例中,它被设定为靠左对齐及过长时省略尾部。 - 计算文本将要被绘制到的矩形的范围。
-
使用
draw(in:withAttributes:)
方法绘制文本。 -
使用
bytesFormatter
获取文本的尺寸大小,并为文件大小的文本创建属性。这里相对于之前的代码,主要的区别是它通过NSFontAttributeName
在属性字典中设置了一个不同的文本颜色。
运行项目,或直接打开 Main.storyboard 来查看效果。
条状的图表完成了!你可以调整窗口的大小,看查看它如何适配新的尺寸。注意观察当没有足够大的空间时,文本如何被绘制。
是不是很酷!
macOS 提供了使用 AppKit 框架来完成绘制的替代选项。它提供了更高层次抽象的方法。他用类来替换 C 的函数,并提供助手方法来使得执行一些常见的任务变得更加容易。概念在两个框架中都是相同的,如果你熟悉Core Graphics的话,Cocoa Drawing是非常容易上手的。
正如在Core Graphics中所做的一样,你需要使用
NSBezierPath
来创建和绘制路径,它是在Cocoa Drawing中等价于
CGPathRef
的对象:
饼状图看起来应该是这样的:
你会分三步来绘制它:
- 首先,你创建了一个圆形的路径来表示可用的空间,然后用配置好的颜色来描边和填充它。
- 然后为已用的空间创建相应的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()
}
上述代码:
-
使用构造器
init(ovalIn:)
来创建一个圆形的path,设定好笔划和填充的颜色,然后绘制path。 -
为已用部分的圆创建相应的路径。首先,基于已用的空间计算结束处的角度。然后分四步来创建path:
- 移动到圆形的中点。
- 从中心到圆形的右侧添加一条线。
- 从当前的点添加一条弧线到刚计算出的角度。
- 闭合路径。然后再从弧形结束的点处连接一条线到圆形的中心。
-
设定好笔划的颜色,并使用
stroke()
方法来绘制path。
你会发现很多相对于Core Graphics不同的情况:
- 在代码中没有对图形上下文的引用。这是因为这些方法会自动获取当前的上下文,在这个case中,它就是当前view的上下文了。
- 这里需要的是角度,而不是弧度。所以需要的话,可使用 CGFloat+Radians.swift 中的方法来进行转换。
现在,添加下列的代码到
draw(_:)
中来绘制饼状图表:
drawPieChart()
看起来是不好赞!
Cocoa Drawing
使用
NSGradient
来绘制渐变效果。
你需要把渐变的效果绘制到可用部分的圆中,你早已知道该怎么做了。
没错,就是裁切区域!
你早已创建了一条用来进行绘制的路径,在绘制渐变效果之前,你就可以把它当做一个裁切区域来用。
添加下列的代码到
drawPieChart()
的尾部:
if let gradient = NSGradient(starting: pieChartGradientStartColor,
ending: pieChartGradientEndColor) {
gradient.draw(in: path, angle: Constants.pieChartGradientAngle)
}
在第一行代码中,你尝试用两种颜色来创建一个渐变效果。如果创建成功的话,就调用
draw(in:angle:)
方法来绘制它。这个方法在内部设置了裁切区域,并绘制渐变的效果。这样子是不很棒?
运行项目。
现在这个自定义的view已经看起来越来越棒了。现在就剩一件事需要做了:在饼状图表中绘制可用和已用空间的标记文本。
你早就知道该怎么做了吧。想不想挑战一下?:]
下面是你需要做的事:
-
使用
bytesFormatter
来获取可用空间的文本(fileDistribution.available
property)以及全部空间(fileDistribution.capacity
property)。 - 计算文本的位置,把它放到可用和已用部分的中点处。
- 使用下列的熟悉绘制文本:
-
字体:
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)
上述代码:
- 计算你绘制已用和可用部分文本位置相应的角度。
- 创建文本的属性,并计算已用空间文本的位置 x 和 y - 然后绘制出来。
- 创建文本的属性,并计算可用空间文本的位置 x 和 y - 然后绘制出来。
现在,运行项目来查看你最终的作品吧。
祝贺!你已经用Core Graphics和Cocoa Drawing构建了一个超美的app!
你可以从 这里 下载最终完整的项目。
本教程已覆盖了在 macOS 中,可用来在定制的view上进行绘制的不同的框架。你已经了解了:
- 如何使用Core Graphics和Cocoa Drawing创建及绘制path
- 如何使用裁切区域
- 如何绘制文本
- 如何绘制渐变效果
现在,你应当非常自信地使用Core Graphics和Cocoa Drawing去绘制漂亮的、相应式的图形了。
如果你想要学到更多相关的内容,可以参考下列的资源: