版本记录
版本号 | 时间 |
---|---|
V1.0 | 2019.02.12 星期二 |
前言
quartz
是一个通用的术语,用于描述在iOS
和MAC OS X
中整个媒体层用到的多种技术 包括图形、动画、音频、适配。Quart 2D
是一组二维绘图和渲染API
,Core Graphic
会使用到这组API
,Quartz Core
专指Core Animation
用到的动画相关的库、API
和类。CoreGraphics
是UIKit
下的主要绘图系统,频繁的用于绘制自定义视图。Core Graphics
是高度集成于UIView
和其他UIKit
部分的。Core Graphics
数据结构和函数可以通过前缀CG
来识别。在app中很多时候绘图等操作我们要利用CoreGraphic
框架,它能绘制字符串、图形、渐变色等等,是一个很强大的工具。感兴趣的可以看我另外几篇。
1. CoreGraphic框架解析(一)—— 基本概览
2. CoreGraphic框架解析(二)—— 基本使用
3. CoreGraphic框架解析(三)—— 类波浪线的实现
4. CoreGraphic框架解析(四)—— 基本架构补充
5. CoreGraphic框架解析 (五)—— 基于CoreGraphic的一个简单绘制示例 (一)
6. CoreGraphic框架解析 (六)—— 基于CoreGraphic的一个简单绘制示例 (二)
7. CoreGraphic框架解析 (七)—— 基于CoreGraphic的一个简单绘制示例 (三)
8. CoreGraphic框架解析 (八)—— 基于CoreGraphic的一个简单绘制示例 (四)
9. CoreGraphic框架解析 (九)—— 一个简单小游戏 (一)
10. CoreGraphic框架解析 (十)—— 一个简单小游戏 (二)
11. CoreGraphic框架解析 (十一)—— 一个简单小游戏 (三)
12. CoreGraphic框架解析 (十二)—— Shadows 和 Gloss (一)
13. CoreGraphic框架解析 (十三)—— Shadows 和 Gloss (二)
14. CoreGraphic框架解析 (十四)—— Arcs 和 Paths (一)
15. CoreGraphic框架解析 (十五)—— Arcs 和 Paths (二)
开始
首先看下写作环境
Swift 4.2, iOS 12, Xcode 10
在本教程中,您将学习如何使用Core Graphics
绘制线条,矩形和渐变 - 从美化表格视图开始!
这篇将揭开Core Graphics的神秘面纱。您将通过实际练习逐步学习API,首先使用Core Graphics
美化table views
。
Core Graphics
是iOS上非常酷的API。作为开发人员,您可以使用它来自定义您的UI,并使用一些非常简洁的效果,通常甚至无需让设计师参与其中。任何与2D绘图相关的东西 - 比如绘制形状,填充它们并赋予它们渐变 - 都是使用Core Graphics的一个很好的选择。
Core Graphics的历史可以追溯到OS X
的早期阶段,是目前仍在使用的最古老的API之一。也许这就是为什么,对于许多iOS开发人员来说,Core Graphics起初可能有些令人生畏:它是一个庞大的API,并且有很多障碍可以发现。但是,自从Swift 3以来,C风格的API已经更新,看起来和感觉就像您熟悉和喜爱的现代Swift API!
在本教程中,您将构建一个Star Wars Top Trumps
应用程序,该应用程序由包含Starships
列表的主视图组成:
以及一个每个Starship
的详细视图
在创建此应用程序时,您将学习如何开始使用Core Graphics,如何填充和描边矩形以及如何绘制线条和渐变以制作自定义表格视图单元格和背景。
是时候与Core Graphics
享受一些乐趣了!
打开入门项目并快速浏览一下。该应用程序基于Xcode提供的Master-Detail App
模板。主视图控制器包含Star Ships
列表,详细视图控制器显示每艘船的详细信息。
打开MasterViewController.swift
。在该类的顶部,注意一个starships
变量,它包含Starship
类型的数组和StarshipDataProvider
类型的dataProvider
变量。
通过Command-单击StarshipDataProvider
并选择Jump to Definition
跳转到StarshipDataProvider.swift
。这是一个简单的类,它读取bundle
文件Starships.json
,并将内容转换为Starship
数组。
你可以在Starship.swift
找到Starship
的定义。它只是一个简单的结构体,具有Starships
常见属性的属性。
接下来,打开DetailViewController.swift
。在类定义为枚举之前定义在文件的顶部,FieldsToDisplay
,它定义要在枚举中显示的Starship
属性的人类可读标题。在这个文件中,tableView(_:cellForRowAt :)
只是一个很大的switch
语句,用于将每个Starship
属性的数据格式化为正确的格式。
构建并运行应用程序。
登陆页面是MasterViewController
,显示星球大战宇宙中的星舰列表。 点击以选择X-wing
,应用程序将导航到该船的详细视图,其中显示了X翼的图像,然后是各种属性,例如它的成本和飞行速度。
这是一个功能齐全,如果非常无聊的应用程序。 是时候添加一些bling
了!
Analyzing the Table View Style
在本教程中,您将为两个不同的表视图添加不同的样式。 仔细看看这些变化是什么样的。
在主视图控制器中,每个单元格:
- 有从深蓝色到黑色的渐变色。
- 以黄色勾勒出轮廓,从cell bounds 有一定的inset。
并在详细视图控制器中:
- table本身有从深蓝色到黑色的渐变色。
- 每个cell都有一个黄色分离线,将其与相邻cell分开。
要绘制这两种设计,您只需要知道如何使用Core Graphics
绘制矩形,渐变和线条,这正是您将要学习的内容。
Hello, Core Graphics!
虽然本教程涵盖了在iOS上使用Core Graphics
,但重要的是要知道Core Graphics可用于所有主要的Apple平台,包括通过AppKit
的MacOS
,iOS和通过UIKit
的tvOS
以及通过WatchKit
的Apple Watch
。
您可以考虑使用Core Graphics,如在物理画布上绘画;绘图操作的顺序很重要。例如,如果您绘制重叠的形状,那么您添加的最后一个将位于顶部并与下面的重叠。
Apple以这样的方式构建Core Graphics
,使您作为开发人员在单独的时刻提供有关绘制内容的说明而不是在何处绘制。
由CGContext
类表示的核心图形上下文定义了where
。您可以告诉上下文要执行的绘制操作。 CGContexts
用于绘制到位图图像,绘制为PDF文件,最常见的是直接绘制到UIView中。
在这个绘画类比中,核心图形上下文代表画家绘制的画布。
核心图形上下文是状态机(State Machines)
。也就是说,当您设置填充颜色时,可以为整个画布设置填充颜色,并且在您更改之前,您绘制的任何形状都将具有相同的填充颜色。
每个UIView
都有自己的核心图形上下文。要使用Core Graphics绘制UIView的内容,必须在视图的draw(_ :)
中编写绘图代码。这是因为iOS在调用draw(_ :)
之前设置了正确的CGContext
以便绘制到视图中。
现在您了解了如何在UIKit中使用Core Graphics的基础知识,现在是时候更新您的应用了!
Drawing Rectangles
首先,通过从“文件”菜单中选择New ▸ File…
来创建新的视图文件。选择Cocoa Touch Class
,按Next
,然后将类名设置为StarshipsListCellBackground
。使其成为UIView的子类,然后创建类文件。将以下代码添加到新类:
override func draw(_ rect: CGRect) {
// 1
guard let context = UIGraphicsGetCurrentContext() else {
return
}
// 2
context.setFillColor(UIColor.red.cgColor)
// 3
context.fill(bounds)
}
下面逐行分析:
- 1) 首先,使用
UIGraphicsGetCurrentContext()
获取此UIView
实例的当前CGContext
。请记住,iOS会在调用draw(_ :)
之前自动为您设置。如果由于任何原因无法获取上下文,则可以从方法中提前返回。 - 2) 然后,在上下文本身上设置填充颜色。
- 3) 最后,您告诉它填充视图的边界。
如您所见,Core Graphics API
不包含直接绘制填充颜色的形状的方法。相反,有点像添加油漆到特定的画笔,你将颜色设置为CGContext
的状态,然后,你告诉上下文分别用该颜色绘制什么。
您可能还注意到,当您在上下文中调用setFillColor(_ :)
时,您没有提供标准的UIColor
。相反,您必须使用CGColor
,这是Core Graphics
内部用于表示颜色的基本数据类型。只需访问任何UIColor的cgColor
属性,将UIColor转换为CGColor非常容易。
1. Showing Your New Cell
要查看您的新视图,请打开MasterViewController.swift
。在tableView(_:cellForRowAt :)
中,在方法的第一行中出现单元格后立即添加以下代码:
if !(cell.backgroundView is StarshipsListCellBackground) {
cell.backgroundView = StarshipsListCellBackground()
}
if !(cell.selectedBackgroundView is StarshipsListCellBackground) {
cell.selectedBackgroundView = StarshipsListCellBackground()
}
此代码将单元格的背景视图设置为新视图的背景视图。 构建并运行应用程序,您将在每个单元格中看到可爱的,如果花哨的红色背景。
惊人! 您现在可以使用Core Graphics
进行绘制。 不管你信不信,你已经学会了一系列非常重要的技巧:如何绘制上下文,如何更改填充颜色以及如何用颜色填充矩形。 你可以用它制作一些非常漂亮的用户界面。
但是你要更进一步,学习一种最有用的技术来制作优秀的用户界面:渐变!
Creating New Colors
您将在此项目中反复使用相同的颜色,因此为UIColor
创建一个扩展,以使这些颜色易于访问。 转到File ▸ New ▸ File…
并创建一个名为UIColorExtensions.swift
的新Swift文件。 用以下内容替换文件的内容:
import UIKit
extension UIColor {
public static let starwarsYellow =
UIColor(red: 250/255, green: 202/255, blue: 56/255, alpha: 1.0)
public static let starwarsSpaceBlue =
UIColor(red: 5/255, green: 10/255, blue: 85/255, alpha: 1.0)
public static let starwarsStarshipGrey =
UIColor(red: 159/255, green: 150/255, blue: 135/255, alpha: 1.0)
}
此代码定义了三种新颜色,您可以在UIColor
上以静态属性的形式访问这些颜色。
Drawing Gradients
接下来,由于您要在此项目中绘制大量渐变,因此请添加辅助方法来绘制渐变。 这将通过将渐变代码保存在一个位置来简化项目,并避免重复自己。
选择File ▸ New ▸ File…
并创建一个名为CGContextExtensions.swift
的新Swift文件。 用以下内容替换文件的内容:
import UIKit
extension CGContext {
func drawLinearGradient(
in rect: CGRect,
startingWith startColor: CGColor,
finishingWith endColor: CGColor
) {
// 1
let colorSpace = CGColorSpaceCreateDeviceRGB()
// 2
let locations = [0.0, 1.0] as [CGFloat]
// 3
let colors = [startColor, endColor] as CFArray
// 4
guard let gradient = CGGradient(
colorsSpace: colorSpace,
colors: colors,
locations: locations
) else {
return
}
}
}
这个方法做了很多:
- 1) 首先,设置正确的色彩空间。您可以使用色彩空间做很多事情,但是您几乎总是希望使用
CGColorSpaceCreateDeviceRGB
来使用与设备相关的标准RGB色彩空间。 - 2) 接下来,设置一个数组,跟踪渐变范围内每种颜色的位置。值0表示渐变的开始,1表示渐变的结束。
注意:如果需要,可以在渐变中使用三种或更多颜色,并且可以设置每种颜色在渐变中的位置,就像这样的数组。这对某些效果很有用。
- 3) 之后,使用传递给方法的颜色创建一个数组。注意在这里使用
CFArray
而不是Array
,因为您正在使用较低级别的C API
。 - 4) 然后,通过初始化
CGGradient
对象,传入颜色空间,颜色数组和先前创建的位置来创建渐变。如果由于某种原因,可选的初始化程序失败,则提前返回。
你现在有一个渐变引用,但它实际上还没有绘制任何东西 - 它只是一个指向你稍后实际绘制时使用的信息的指针。现在几乎是绘制渐变的时候了,但在你做之前,还需要更多的理论。
1. The Graphics State Stack
请记住,Core Graphics
上下文是状态机。 在上下文中设置状态时必须要小心,特别是在传递上下文的函数中,或者在本例中是上下文本身的方法,因为在修改上下文之前无法知道上下文的状态。 请考虑UIView中的以下代码:
override func draw(_ rect: CGRect) {
// ... get context
context.setFillColor(UIColor.red.cgColor)
drawBlueCircle(in: context)
context.fill(someRect)
}
// ... many lines later
func drawBlueCircle(in context: CGContext) {
context.setFillColor(UIColor.blue.cgColor)
context.addEllipse(in: bounds)
context.drawPath(using: .fill)
}
看一下这段代码,您可能会认为它会在视图中绘制一个红色矩形和一个蓝色圆圈,但你错了! 相反,这段代码绘制了一个蓝色矩形和一个蓝色圆圈 - 但为什么呢?
因为drawBlueCircle(in :)
在上下文中设置了蓝色填充颜色,并且因为上下文是状态机,所以它会覆盖先前的红色填充颜色集。
这就是saveGState()
及其伙伴方法restoreGState()
的用武之地!
每个CGContext
都维护一个图形状态的堆栈,其中包含当前绘图环境的大部分(尽管不是全部)方面。 saveGState()
将当前状态的副本推送到图形状态堆栈,然后您可以使用restoreGState()
将上下文恢复到该状态,并在该过程中从堆栈中删除状态。
在上面的示例中,您应该像这样修改drawBlueLines(in :)
:
func drawBlueCircle(in context: CGContext) {
context.saveGState()
context.setFillColor(UIColor.blue.cgColor)
context.addEllipse(in: bounds)
context.drawPath(using: .fill)
context.restoreGState()
}
您可以打开RedBluePlayground.playground
来自行测试。
2. Completing the Gradient
掌握有关图形状态堆栈的知识,是时候完成绘制背景渐变了。 将以下内容添加到drawLinearGradient(in:startingWith:finishingWith:)
的末尾:
// 5
let startPoint = CGPoint(x: rect.midX, y: rect.minY)
let endPoint = CGPoint(x: rect.midX, y: rect.maxY)
// 6
saveGState()
// 7
addRect(rect)
clip()
drawLinearGradient(
gradient,
start: startPoint,
end: endPoint,
options: CGGradientDrawingOptions()
)
restoreGState()
- 5) 首先计算渐变的起点和终点。您可以将其设置为从矩形的顶部中间到底部中间的线。有用的是,
CGRect
包含一些实例变量,如midX
和maxY
,这使得这非常简单。 - 6) 接下来,由于您即将修改上下文的状态,因此您可以保存其图形状态并通过还原它来结束该方法。
- 7) 最后,在提供的矩形中绘制渐变。
drawLinearGradient(_:start:end:options :)
是实际绘制渐变的方法,但除非另有说明,否则它将使用渐变填充整个上下文,即整个视图。在这里,您只想填充提供的矩形中的渐变。要做到这一点,你需要了解clipping
。
剪切(clipping)
是Core Graphics
中一个非常棒的功能,可以将绘图限制为任意形状。您所要做的就是将形状添加到上下文中,然后,不要像通常那样填充它,而是在上下文中调用clip()
,然后将所有未来的绘制限制到该区域。
因此,在这种情况下,您将在最终调用drawLinearGradient(_:start:end:options :)
绘制渐变之前,在上下文和剪辑上设置提供的矩形。
是时候给这个方法一个旋转!打开StarshipsListCellBackground.swift
,在获取当前的UIGraphicsContext
之后,用以下内容替换代码:
let backgroundRect = bounds
context.drawLinearGradient(
in: backgroundRect,
startingWith: UIColor.starwarsSpaceBlue.cgColor,
finishingWith: UIColor.black.cgColor
)
构建并运行
您现在已成功将渐变背景添加到自定义单元格。 干得好,但是,可以公平地说,现在成品并不是很好看。 是时候用一些标准的UIKit主题来修复它了。
Fixing the Theme
打开Main.storyboard
并选择Master scene
中的表格视图。 在“属性”检查器中,将Separator
设置为None
。
然后,在Master Navigation Controller
场景中选择Navigation Bar
并将导航栏样式设置为黑色并取消选择半透明(Translucent)
。 在Detail Navigation Controller
场景中重复Navigation Bar
。
接下来,打开MasterViewController.swift
。 在viewDidLoad()
的末尾,添加以下内容:
tableView.backgroundColor = .starwarsSpaceBlue
然后在tableView(_:cellForRowAt :)
中,在返回单元格之前,设置文本的颜色:
cell.textLabel!.textColor = .starwarsStarshipGrey
最后,打开AppDelegate.swift
并在application(_:didFinishLaunchingWithOptions:)
中返回之前添加以下内容:
// Theming
UINavigationBar.appearance().tintColor = .starwarsYellow
UINavigationBar.appearance().barTintColor = .starwarsSpaceBlue
UINavigationBar.appearance().titleTextAttributes =
[.foregroundColor: UIColor.starwarsStarshipGrey]
构建并运行
那更好!您的主表视图开始看起来非常太空。
Stroking Paths
在Core Graphics
中进行描边意味着沿着路径绘制一条线,而不是像之前那样填充它。
当Core Graphics
描绘路径时,它会在路径的精确边缘的中间绘制描边线。这可能会导致一些常见问题。
1. Outside the Bounds
首先,如果您正在绘制一个矩形的边缘,例如,边框,默认情况下,Core Graphics将不会绘制一半的描边路径。
为什么?因为为UIView
设置的上下文仅扩展到视图的边界。想象一下,在视图边缘周围有一个点边框。因为Core Graphics在路径的中间向下划线,所以该线将在视图边界之外半个点,在视图边界内半个点。
一个常见的解决方案是将行程的路径插入每个方向的线宽度的一半,使其位于视图内。
下图显示了一个黄色矩形,在灰色背景上有一个点宽的红色描边,以一个点间隔条纹。在左图中,描边路径遵循视图的边界并已被裁剪。您可以看到这一点,因为红线是灰色方块宽度的一半。在右图中,描边路径已插入半个点的间隔,现在具有正确的线宽。
2. Anti-Aliasing
其次,您需要了解可能影响边框外观的抗锯齿效果。消除锯齿,如果您不熟悉它(即使您可能在计算机游戏设置屏幕上听说过它!),也是一种渲染引擎用于避免在显示图形时出现“锯齿状”边缘和线条的技术不要完美映射到设备上的物理像素。
以前一段视图周围的一点边框为例。如果边框遵循视图的边界,则Core Graphics将尝试在矩形的任一侧绘制半个点宽的线。
在非视网膜显示器上,一个点等于设备上的一个像素。不可能只照亮一半像素,因此Core Graphics将使用消除锯齿来绘制两个像素,但是在较浅的阴影中只能呈现单个像素的外观。
在以下几组屏幕截图中,左图像是非视网膜显示器,中间图像是视网膜显示器,其比例为2,第三图像是视网膜显示器,其比例为3。
对于第一个图,请注意2x图像如何不显示任何抗锯齿,因为黄色矩形的两边的半点落在像素边界上。然而,在1x和3x图像中发生抗锯齿。
在下一组屏幕截图中,描边矩形已插入半个点间隔,使得笔划线与点精确对齐,从而与像素边界对齐。 注意没有锯齿伪像。
Adding a Border
回到你的应用程序! cell开始看起来很好,但你会增加另一种触感,让它们脱颖而出。 这一次,你将在cell边缘绘制一个明亮的黄色框。
您已经知道如何轻松填充矩形。 好吧,在他们周围描边也同样容易。
打开StarshipsListCellBackground.swift
并将以下内容添加到draw(_ :)
的底部:
let strokeRect = backgroundRect.insetBy(dx: 4.5, dy: 4.5)
context.setStrokeColor(UIColor.starwarsYellow.cgColor)
context.setLineWidth(1)
context.stroke(strokeRect)
在这里,您可以创建一个用于描边的矩形,它在x和y方向上从背景矩形中插入4.5个点。 然后将描边颜色设置为黄色,将线宽设置为一个点,最后描边矩形。 构建并运行您的项目。
现在你的星舰列表真的看起来像是来自遥远的星系!
Building a Card Layout
虽然你的主视图控制器看起来很花哨,但细节视图控制器仍然需要一些修饰!
对于此视图,您将首先使用自定义UITableView
子类在表视图背景上绘制渐变。
创建一个名为StarshipTableView.swift
的新Swift文件。 用以下内容替换生成的代码:
import UIKit
class StarshipTableView: UITableView {
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {
return
}
let backgroundRect = bounds
context.drawLinearGradient(
in: backgroundRect,
startingWith: UIColor.starwarsSpaceBlue.cgColor,
finishingWith: UIColor.black.cgColor
)
}
}
现在这应该开始变得熟悉了。 在新表视图子类的draw(_ :)
方法中,您将获得当前的CGContext
,然后在视图的边界绘制一个渐变,从顶部的蓝色开始,在底部朝向黑色。 简单!
打开Main.storyboard
并单击Detail
场景中的TableView
。 在Identity
检查器中,将类设置为新的StarshipTableView
。
构建并运行应用程序,然后点击X-wing
那一行 。
您的详细视图现在具有从上到下运行的漂亮的全屏渐变,但表格视图中的单元格遮挡了效果的最佳部分。 是时候解决这个问题并为细节cell添加更多的天赋。
回到Main.storyboard
,在Detail Scene
中选择FieldCell
。 在“属性”检查器中,将背景设置为Clear Color
。 接下来,打开DetailViewController.swift
,在tableView(_:cellForRowAt :)
的最底部,在返回单元格之前,添加以下内容:
cell.textLabel!.textColor = .starwarsStarshipGrey
cell.detailTextLabel!.textColor = .starwarsYellow
这只是将单元格的字段名称和值设置为适合星球大战主题的更合适的颜色。
然后,在tableView(_:cellForRowAt :)
之后添加以下方法来设置表视图头的样式:
override func tableView(
_ tableView: UITableView,
willDisplayHeaderView view: UIView,
forSection section: Int
) {
view.tintColor = .starwarsYellow
if let header = view as? UITableViewHeaderFooterView {
header.textLabel?.textColor = .starwarsSpaceBlue
}
}
在这里,您将表格视图的标题视图的色调颜色设置为主题黄色,为其提供黄色背景,并将其文本颜色设置为主题蓝色。
Drawing Lines
作为bling
的最后一点,您将在详细视图中为每个单元格添加一个拆分器。 创建一个新的Swift文件,这次称为YellowSplitterTableViewCell.swift
。 用以下内容替换生成的代码:
import UIKit
class YellowSplitterTableViewCell: UITableViewCell {
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {
return
}
let y = bounds.maxY - 0.5
let minX = bounds.minX
let maxX = bounds.maxX
context.setStrokeColor(UIColor.starwarsYellow.cgColor)
context.setLineWidth(1.0)
context.move(to: CGPoint(x: minX, y: y))
context.addLine(to: CGPoint(x: maxX, y: y))
context.strokePath()
}
}
在YellowSplitterTableVIewCell
中,您使用Core Graphics
来划分单元格边界底部的一条线。 注意所使用的y值是如何比视图的边界小半个点,以确保分割器完全在单元内绘制。
现在,您需要实际绘制显示拆分器的线。
要在A和B之间绘制一条线,首先移动到A点,这不会导致Core Graphics绘制任何东西。 然后,您将一条线添加到B点,该线将点A到点B的线添加到上下文中。 然后,您可以调用strokePath()
来描边该行。
最后,再次打开Main.storyboard
并使用Identity检查器将Detail
场景中的FieldCell
类设置为新创建的YellowSplitterTableViewCell
。 构建并运行您的应用程序。 然后,打开X-wing
细节视图。 漂亮!
后记
本篇主要讲述了Lines, Rectangles 和 Gradients,感兴趣的给个赞或者关注~~~