简单入门 iOS 中的绘图 (一)

在日常的 iOS 开发中,我们除了会直接使用由系统框架提供的 UI 组件以外,也会自定义各种不同的 UI 组件。有各种各样不同的方式来实现自定义的 UI 组件,其中使用系统的绘图 API 来绘制图形是比较常见的一种方式。

Graphics Contexts

Graphics Contexts 可以理解为一块画布,所以绘图相关的操作都是在它上面进行的。

那么应该怎么创建或者获取 Graphics Contexts 呢?Graphics Contexts 的来源一般有两种:1) 通过系统获取,2) 自行创建。

获取 Graphics Contexts

通过系统获取 Graphics Contexts

一般,在以下场景中,我们能够之间从系统获取 Graphics Contexts

  • drawRect:
  • drawInContext:
  • drawLayer:inContext:

我们可以通过创建 UIView 的子类来重写 drawRect: 方法,在该方法中,我们已经处于一个 Graphics Contexts 中,之间在其上绘制就好。

drawInContext:drawLayer:inContext: 都和 CALayer 有关,drawInContext:CALayer 的一个实例方法,drawLayer:inContext:CALayer 的代理方法(UIView 是嵌有一个 CALayer 的,也是该 CALayer 的代理)。我们可以通过创建 CALayer 的子类或者成为 CALayer 的代理来实现上述两个方法,然后完成绘制的操作。在这两个方法中,都有一个 Graphics Contexts,可以在其上绘制,与 drawRect: 不同的是,传入的 Graphics Contexts 并不是当前的 Graphics Contexts。(⚠️:在实践中发现,如果使用 drawInContext:drawLayer:inContext: 来绘制,需要对 CALayer 调用 setNeedsDisplay 方法,否则绘制不会发生。)

创建 Graphics Contexts

我们不会通过构造方法来创建 Graphics Contexts,日常中基本上只会在一个场景下创建 Graphics Contexts,那就是绘制生产图片的时候。 我们会使用 UIGraphicsBeginImageContextWithOptions(_ size: CGSize, _ opaque: Bool, _ scale: CGFloat) 函数来创建 Graphics Contexts,该函数不经创建了图片的 Graphics Contexts,还将其设置为了当前的 Graphics Contexts

绘制一张图片

绘制一张图片所需的基本代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 创建图片绘制所需的 *Graphics Contexts*
// 第一个参数表示画布的大小,同时也是生产的图片大学
// 第二个参数表示图片是否是(包含)透明
// 第三个表示图片的 scale 参数,如果为 0,则使用系统的 scale 参数
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0)
// 2. 最画布上进行绘制

// do some drawing

// 3. 生成图片
let img = UIGraphicsGetImageFromCurrentImageContext()
// 4. 关闭创建的 *Graphics Contexts*
UIGraphicsEndImageContext()
// 5. 使用图片
// use the image

图形绘制 API

在 iOS 系统提供的绘图 API 中,有两套是我们经常用到的。一套包含在 UIKit 框架中,另一套包含在 Core Graphics 框架中(也就是 Quartz 或者 Quartz 2D)。

使用 UIKit 绘图

在使用 UIKit 绘图时,需要 Graphics Contexts 已经是当前的 Contexts
对于 UIKit 中的 UIImage, NSString, UIBezierPathUIColor,都只能绘制在当前的 Graphics Contexts,而不能进行额外指定。不过我们可以通过 UIGraphicsPushContext 函数将一个 Graphics Contexts 设置为当前的 Context。需要记住的是,在完成绘图操作后,需要调用 UIGraphicsPopContext 函数将之前的 Graphics Contexts 撤销为当前的 Context。可以使用 Swift 中的 defer 来避免遗忘该步操作。

使用 Core Graphics 绘图

使用 Core Graphics 绘图时,并不需要已经处于一个 Graphics Contexts 中。Core Graphics 绘图 API 是一个全功能的 API,UIKit 的绘制功能其实也是建立在其之上的,Core Graphics 是更低层的 API 接口,基本上都有 C 语言函数实现。

举个 🌰

我们可以在三个地方(UIViewdrawRect:CALayer 相关的 drawInContext:drawLayer:inContext:,图片绘制),使用两套 API 来绘图,所以一共产生了六种方式。

接下来将使用两种方式来完成同一个任务:画一个有颜色的圆(为了方便区分使用不同的颜色):

1. drawRect: & UIkit

1
2
3
4
5
override func draw(_ rect: CGRect) {
let p = UIBezierPath(ovalIn: self.bounds.insetBy(dx: 4, dy: 4))
UIColor.blue.setFill()
p.fill()
}

2. drawRect: & Core Graphics

1
2
3
4
5
6
override func draw(_ rect: CGRect) {
let con = UIGraphicsGetCurrentContext()
con?.addEllipse(in: self.bounds.insetBy(dx: 4, dy: 4))
con?.setFillColor(UIColor.red.cgColor)
con?.fillPath()
}

3. drawLayer:inContext: & UIKit

1
2
3
4
5
6
7
8
override func draw(_ layer: CALayer, in ctx: CGContext) {
UIGraphicsPushContext(ctx)
defer { UIGraphicsPopContext() }

let p = UIBezierPath(ovalIn: self.bounds.insetBy(dx: 4, dy: 4))
UIColor.yellow.setFill()
p.fill()
}

4. drawLayer:inContext: & CoreGraphics

1
2
3
4
5
6
7
8
9
10
override func draw(_ layer: CALayer, in ctx: CGContext) {
ctx.saveGState()
defer {
ctx.restoreGState()
}

ctx.addEllipse(in: self.bounds.insetBy(dx: 4, dy: 4))
ctx.setFillColor(UIColor.green.cgColor)
ctx.fillPath()
}

5. 图片绘制 & UIKit

1
2
3
4
5
6
7
8
9
10
let imageSize = CGSize(width: 200, height: 200)
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0)
let p = UIBezierPath(ovalIn: CGRect(origin: .zero, size: imageSize).insetBy(dx: 4, dy: 4))
UIColor.purple.setFill()
p.fill()
let img = UIGraphicsGetImageFromCurrentImageContext()

UIGraphicsEndImageContext()

return img

6. 图片绘制 & Core Graphics

1
2
3
4
5
6
7
8
9
10
11
12
let imageSize = CGSize(width: 200, height: 200)
UIGraphicsBeginImageContextWithOptions(imageSize, false, 0)

let ctx = UIGraphicsGetCurrentContext()
ctx?.addEllipse(in: CGRect(origin: .zero, size: imageSize).insetBy(dx: 4, dy: 4))
ctx?.setFillColor(UIColor.cyan.cgColor)
ctx?.fillPath()
let img = UIGraphicsGetImageFromCurrentImageContext()

UIGraphicsEndImageContext()

return img

绘制结果

Untitled-w598

0%