Skip to content

Instantly share code, notes, and snippets.

@Josscii
Last active August 4, 2022 14:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Josscii/5863179a58b47b95e2611d7475a2047a to your computer and use it in GitHub Desktop.
Save Josscii/5863179a58b47b95e2611d7475a2047a to your computer and use it in GitHub Desktop.
iOS 绘图那些事

iOS 绘图那些事

在 iOS 中,我们通过 Core Graphics (也叫 Quarz 2D) 来绘图,而在 Core Grapihcs 之上的 UIKit 又封装了如 UIBeizerPath 等高级 api,这里就来简单的谈谈基础和技巧。

draw(_:)

对于 UIView 来说,我们可以重写 draw(_:) 方法来进行绘图,官方文档中对它的描述是:

Specifically, UIKit creates and configures a graphics context for drawing and adjusts the transform of that context so that its origin matches the origin of your view’s bounds rectangle.

这里有两个地方需要解释,第一个是 context,它封装了所有的绘图信息,你也可以把它想象成一个画布,你所有的绘图操作都需要在这上面进行;第二个是坐标系,对 CG 来说,坐标系是左下角为原点,而对 UIKit 来说,左上是原点,所以需要一个转换。

我们可以打印出 UIKit 为我们创建的这个 context 可以看到它是一个 kCGContextTypeBitmap 类型的。

<CGContext 0x60800017a880> (kCGContextTypeBitmap)
	<<CGColorSpace 0x60800003f240> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1)>
		width = 400, height = 400, bpc = 8, bpp = 32, row bytes = 1600 
		kCGImageAlphaNoneSkipFirst | kCGImageByteOrder32Little 

UIGraphicsBeginImageContextWithOptions

有时候我们需要在 draw(_:) 之外绘图,UIKit 同样为我们提供了一个创建 context 的便捷方式,文档中对它的说明是:

In contrast, if your application creates an image context by calling the function UIGraphicsBeginImageContextWithOptions, UIKit applies the same transformation to the context’s coordinate system as it does to a UIView object’s graphics context. This allows your application to use the same drawing code for either without having to worry about different coordinate systems.

也就是说,这个方法做了和 draw(_:) 类似的事情。具体的用法很简单:

UIGraphicsBeginImageContextWithOptions(size, false, 0)
// your custom drawing code
UIGraphicsEndImageContext()

这里有三个参数,size 是画布的大小,opaque 是标识视图是否透明,如果为 true,画布的背景就是黑色,反之为透明,最后一个参数是 scale,0 代表适配设备的 scale。

绘图

具体的绘图分为两步:

  • 创建 path
  • 绘制 path

对 Objective-C 来说,可以用 CG 开头的底层 api,但这块 api 已经被 Swift 用面向对象的实现方式重构了,所以用 Swift 写起来会轻松不少,而 UIKit 也封装了 UIBezierPath,两种方式可是混合使用。下面是一些使用示例:

// in draw(_:)
/// use UIKit methods
let path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 20, height: 20))
UIColor.red.setFill()
path.fill()
/// use CG methods
if let context = UIGraphicsGetCurrentContext() {
    context.addEllipse(in: CGRect(x: 0, y: 0, width: 50, height: 50))
    context.setFillColor(UIColor.red.cgColor)
    context.fillPath()
}

State

CGContext 维护了一个 state 栈,state 就是当然 context 的各种参数,如填充颜色,画布大小等。你可以保存一份当前的 state 到栈上,等到需要时,将栈上的 state 取回来。至于那些参数是和这个 state 有关的,你可以在这里找到。因为 clip 操作会裁剪后续画布的大小,所以很常见的一个做法是,在 clip 之前先 save 一下,等操作完成后再回到原来的状态。注意,初始状态下,clip 区域就是整个画布。

context.addEllipse(in: CGRect(x: 0, y: 0, width: 50, height: 50))
context.saveGState()
context.move(to: CGPoint(x: 0, y: 40))
context.addLine(to: CGPoint(x: 20, y: 30))
context.setStrokeColor(UIColor.red.cgColor)
context.strokePath()
context.restoreGState()

值得注意的是,context 有一个 currentPath 的值,这个值在执行绘制操作后会被清空,当然,剪裁后同样会被清空。

fill rule

如果一个 path 包含了多个子 path,那么,你可以指定它的填充规则,其实也就是确定 path 内部的那些点应该被上色,那些不应该。

非零原则:从一个区域中的某个点出发,画一条穿过所有子 path 的线,从零开始计算,如果子 path 是从左向右绘制的,就 +1,反之,-1。如果结果为 0,那么这个区域就不应该被填充,反之。

奇偶原则:从一个区域中的某个点出发,画一条穿过所有子 path 的线,如果这条线和奇数个子 path 相交,这个区域就应该被填充,反之。

CAShapeLayer

CAShapeLayer 能够让我们指定一个 path 来绘制,更可喜的是,我们可以对这个 path 做动画。

let shapeLayer = CAShapeLayer()
shapeLayer.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
shapeLayer.fillColor = UIColor.red.cgColor
let path1 = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 50, height: 50))
shapeLayer.path = path1.cgPath

let anim = CABasicAnimation(keyPath: "path")
anim.fromValue = path1.cgPath
anim.toValue = path2.cgPath
anim.duration = 2
shapeLayer.add(anim, forKey: nil)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment