一 、概述

iOS支持两套图形API簇:Core Graphics/QuartZ 2D 和OpenGL ES,他们都可以看作是代码版的PS。OpenGL ES是跨平台的图形API,属于OpenGL的一个简化版本。QuartZ 2D是苹果公司开发的一套API,它是Core Graphics Framework的一部分。需要注意的是:OpenGL ES只是应用程序编程接口,具体的实现会因平台的不同而有所不同。

我们可以使用Quartz 2D API来实现许多功能,如基本路径的绘制、透明度、描影、绘制阴影、透明层、颜色管理、反锯齿、PDF文档生成和PDF元数据访问。在需要的时候,Quartz 2D还可以借助图形硬件的功能。

本文主要讲述了Quartz 2D的一些基本概念,如:
CG坐标系统、仿射变换、CGContext、GState(CTM、clip path等)及使用CG需要注意的一些问题。

二、一些基本概念

2.1 坐标系

iOS中存在两种坐标系统,一种是原点在屏幕左上角的左手坐标系统(默认Z轴正方向是垂直于屏幕指向用户),iOS中的UIKit和CALayer使用的就是这种坐标系统;一种是原点在屏幕左下角的右手坐标系统,CoreGraphics使用的就是这种坐标系统。Mac OS中的情况与iOS中还有所不同,这里就不再表述,可以参考文后的链接。

image

左/右手坐标系统是3D坐标系统。关于左手坐标系统和右手坐标系统的判定如上图所示,大拇指指向Z轴的正方向,其余弯曲四指的方向为旋转正方向,即从X轴正方向—>Y轴正方向,如果能用左手做出以上动作则为左手坐标系统,反之则为右手坐标系统。例如上图中左边的为左手坐标系统,右边的为右手坐标系统。

2.2 仿射变换

仿射变换,又称仿射映射,是指在几何中,一个向量空间进行一次线性变换并接上一个平移,变换为另一个向量空间。

为了表示仿射变换,需要使用齐次坐标,即用三维向量 (x, y, 1) 表示二维向量,对于高维来说也是如此。按照这种方法,就可以用矩阵乘法表示变换。 x' = ax +cy + tx; y' = bx + dy +ty ,计算过程如下图所示:

image

比如,我们在CoreGraphics中进行绘图,绘完之后要在UIKit中进行显示,由于两者的坐标系统不同,这个时候就需要进行坐标变换,由于变换是在二维空间进行,所以也属于仿射变换,很容易得到变换矩阵:(假设在3.5英寸屏幕下)

[1,0,0  0,-1,0  0,320,1];    

仿射变换的参数为:

affineTransform(a,b,c,d,tx,ty) = (1,0,0,-1,0,320)

上面的变换是在用户空间进行变换的,如果要显示在屏幕上,还得再进行一次用户空间—>设备空间的变换。幸运的是,UIKit会自动为我们做这些事情。

CoreGraphics提供的和仿射变换相关的API和inline function可以在CGAffineTransform.h中找到。

2.3 CGContext

CoreGraphics API所有的操作都是在上下文中进行的,用CGContextRef来表示,上下文可以看作是一块画布,每次在绘制之前都要先备好一块画布。

在使用UIKit框架中的方法绘图时,需要注意一点,UIKIt中的绘图API是没有context参数的,只能使用当前context作为自己的context;UIKit维护着一个context的栈,只有在栈顶的context--也就是当前context,才能被UIKit使用。可以通过UIGraphicsGetCurrentContext来获取当前的context。

有两种常见的获取CGContextRef的方法。

第一种方法是创建一个图片类型的上下文,使用UIKit框架中的方法:

1
void UIGraphicsBeginImageContextWithOptions (CGSize size, BOOL opaque, CGFloat scale) ;

这个API会创建一个处理图片的context,并压入堆栈成为当前context。然后可以在此context中进行绘图操作,并生成图片。通过调用UIGraphicsGetImageFromCurrentImageContext可以从当前context中获取一个UIImage对象。最后,记得调用UIGraphicsEndImageContext来关闭当前context。

有时候,可能会遇到通过

1
2
3
CGContextRef CGBitmapContextCreate(void *data, size_t width,
size_t height, size_t bitsPerComponent, size_t bytesPerRow,
CGColorSpaceRef space, CGBitmapInfo bitmapInfo)

来创建一个图形context,如果我们要使用UIKit框架中的API,就得先通过调用

1
UIGraphicsPushContext(CGContextRef context)

将手动创建的context押入堆栈,成为当前context,在绘制完毕之后,还要记得调用

1
UIGraphicsPopContext()

让当前context出栈。

第二种方法是重写UIView中的drawRect方法,当drawRect方法被调用时,UIKIt会自动创建一个context并成为当前conntext,如前所述,这个context可以通过UIGraphicsGetCurrentContext来获取到,在此context所做的绘制操作都将显示在UIView上。

2.4 CTM

当我们使用CoreGraphics做图时,使用的是右手坐标系,而绘制结果是通过UIView显示,在上面我们已经知道了两个坐标系的仿射变换是

affineTransform(a,b,c,d,tx,ty) = (1,0,0,-1,0,320)

不管是CG坐标还是UIView坐标,都是点的坐标,格式是CFFloat类型;而图像最终是显示在屏幕上面,这就牵扯到一个从用户空间 到 设备空间的坐标转换,设备空间的坐标是像素的坐标,所以在视网膜屏下,一个点包含四个像素(这也解释了为什么点的坐标可以是浮点类型),我们可以得出从用户空间到设备空间的仿射变换为

affineTransform(a,b,c,d,tx,ty) = (2,0,0,2,0,0)

所以,在我们使用CoreGraphics绘图,并且要通过UIView显示时,要经历两次坐标的转换,我们也可以用一个变换来表示这两次坐标的转换

affineTransform(a,b,c,d,tx,ty) = (1,0,0,-1,0,320) * (2,0,0,2,0,0) = (2,0,0,-2,0,960)

这个值就是CTM(current transform matrix),默认值就是上面这个。

利用CTM,我们可以很轻松让整个context上的对象完成平移、缩放、旋转等操作。

2.5 GState

GState也就是图形状态信息,包含以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//当前转换矩阵
CTM (current transformation matrix)

//裁剪路径
clip region

image interpolation quality

//线条有关
line width
line join
miter limit
line cap
line dash

//自定义曲线的平滑度,一般使用默认
flatness

//是否抗锯齿
should anti-alias

//渲染目的,位图信息默认是感知渲染,其他是默认渲染;还可以自定义绝对色度、相对色度
//饱和度渲染等
rendering intent

//填充颜色和描边颜色
fill color space
stroke color space
fill color
stroke color

//用来确定新的绘图对象如何与已存在的对象混合。比如,默认的普通混合模式下,
//result = alpha *forground + (1 - alpha)*background。
alpha value

//字体相关
font
font size
character spacing
text drawing mode

//阴影参数
shadow parameters

//模型
the pattern phase

//字体平滑参数
the font smoothing parameter

//颜色混合模式
blend mode

当我们在context上绘图时,需要设定比如线条、字体、裁剪路径等一些信息,context本身维护着一个状态信息堆栈,这样我们在绘图不同的东西时,就可以很方便的切换回之前的状态。
通常我们在绘制过程中,经常会使用到以下两个API:

1
2
CGContextSaveGState(context); // 状态信息入栈
CGContextRestoreGState(context);//状态信息出栈

2.6 非零缠绕计数准则和奇数-偶数准则

当我们需要填充或裁剪路径时,有以下两种方法可以选择:

1
2
3
4
5
CGContextClip(context);
CGContextFillPath(context);

CGContextEOClip(context);
CGContextEOFillPath(context);

其对应的就是两种不同的填充/裁剪路径准则,上面的API默认使用的是winding-number,下面的使用的就是even-odd准则,两种准则的目的都是用于确定某一个点是否被渲染。

对于填充路径,winding-number的判断方法是这样的:从某一个点从左到右画一条射线,如果路径是以正方向通过射线,则winding-number+1,否则winding-number - 1,如果最终结果winding-number != 0,则这个点就会被渲染,否则不会被渲染。

even-odd的判断方法与此类似,不同的是它考察的是射线和路径交点的个数,而和方向无关,如果交点个数为奇数,则此点被渲染,为偶数则不被渲染。

裁剪路径的方法与此类似。

image

2.7 CGPath

我们可以直接在context上绘图,也可以在context上使用路径,但是直接使用会有不可复用的问题(如下面被取代的API所示),所以推荐的做法是先把轨迹组合成一个路径,再把路径添加到context中,这样对于绘制复杂的场景就可以复用这些路径。

1
2
3
4
5
6
7
8
9
10
CGPathCreateMutable //取代CGContextBeginPath
CGPathMoveToPoint //取代CGContextMoveToPoint
CGPathAddLineToPoint //取代CGContexAddLineToPoint
CGPathAddCurveToPoint //取代CGContexAddCurveToPoint
CGPathAddEllipseInRect //取代CGContexAddEllipseInRect
CGPathAddArc //取代CGContexAddArc
CGPathAddRect //取代CGContexAddRect
CGPathCloseSubpath //取代CGContexClosePath

CGContextAddPath//添加路径到上下文

2.8 Transparency Layers

除了在context绘图和使用CGPath之外,我们还可以在Layer上进行绘图,其中包括Transparency Layers(透明层)和CGLayer。透明层有什么作用呢?通过下面两幅图就可以看出差别。

没有使用透明层

image

使用透明层

image

每个context维护一个透明层栈,并且透明层是可以嵌套的。但由于层通常是栈的一部分,所以我们不能单独操作它们。

我们通过调用函数CGContextBeginTransparencyLayer来开始一个透明层,该函数需要两个参数:图形上下文与CFDictionary对象。字典中包含我们所提供的指定层额外信息的选项,但由于Quartz 2D API中没有使用字典,所以我们传递一个NULL。在调用这个函数后,图形状态参数保持不变,除了alpha值[默认设置为1]、阴影[默认关闭]、混合模式[默认设置为normal]、及其它影响最终组合的参数。

在开始透明层操作后,我们可以绘制任何想显示在层上的对象。指定上下文中的绘制操作将被当成一个组合对象绘制到一个透明背景上。这个背景被当作一个独立于图形上下文的目标缓存。

当绘制完成后,我们调用函数CGContextEndTransparencyLayer。Quartz将结合对象放入上下文,并使用上下文的全局alpha值、阴影状态及裁减区域作用于组合对象。

在透明层中绘制需要三步:

  • 调用函数CGContextBeginTransparencyLayer
  • 在透明层中绘制需要组合的对象
  • 调用函数CGContextEndTransparencyLayer

image

绘制上图的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void MyDrawTransparencyLayer (CGContext myContext, // 1
CGFloat wd,
CGFloat ht)
{
CGSize myShadowOffset = CGSizeMake (10, -20);// 2
CGContextSetShadow (myContext, myShadowOffset, 10); // 3
CGContextBeginTransparencyLayer (myContext, NULL);// 4
// Your drawing code here// 5
CGContextSetRGBFillColor (myContext, 0, 1, 0, 1);
CGContextFillRect (myContext, CGRectMake (wd/3+ 50,ht/2 ,wd/4,ht/4));
CGContextSetRGBFillColor (myContext, 0, 0, 1, 1);
CGContextFillRect (myContext, CGRectMake (wd/3-50,ht/2-100,wd/4,ht/4));
CGContextSetRGBFillColor (myContext, 1, 0, 0, 1);
CGContextFillRect (myContext, CGRectMake (wd/3,ht/2-50,wd/4,ht/4));
CGContextEndTransparencyLayer (myContext);// 6
}

2.9 CGLayer

CGLayer是被设计来复用的,其可以看作是一个子context。如果是绘制一些相当复杂的,并且部分内容需要被重新绘制的,你只需将那部分内容绘制到CGLayer一次,然后便可绘制这个CGLayer到父context中。

看起来很美好,但是性能并不象传说中那样美好,已不建议使用。

2.10 Pattern

Pattern,即模版,功能上和CGLayer差不多,也是拿来复用的,可以用于填充context,就像我们可以使用颜色去填充context一样,我们也可以使用模版去填充context。

使用模版比使用其他方法的效率要高很多,具体的例子和效率对比可以参考文后的链接。

2.11 Blend Mode

略.

总结

使用CoreGraphics,从CG坐标到UIKit坐标再到设备坐标,UIKit会为我们设定一个合适的CTM值,我们也可以利用CTM来做自定义的旋转、平移、缩放等操作。我们可以直接在cotext中直接绘图,也可以使用路径来组合轨迹,还可以在透明层上绘制。对于需要重复使用的内容,我们可以使用模版,不但编程简单而且还能提高效率。

参考资料

Quartz 2D Programming Guide

CGContext Reference

Cocoa Drawing Guide

Quartz 2D Programming Guide的部分中文翻译

iOS绘图教程

Mac,iOS界面中的三维坐标系

iOS中使用blend改变图片颜色

RayWenderlich上的CG教程

计算机图形的渲染过程

绘制像素到屏幕上

CGLayer no longer recommended

Core Graphics 101: 模版