南峰子的技术博客

攀登,一步一个脚印,方能知其乐


  • 首页

  • 知识小集

  • Swift

  • Objective-C

  • Cocoa

  • 翻译

  • 源码分析

  • 杂项

  • 归档

Quartz 2D编程指南之七:阴影

发表于 2014-12-05   |   分类于 翻译

阴影是绘制在一个图形对象下的且有一定偏移的图片,它用于模拟光源照射到图形对象上所形成的阴影效果,如果7-1所示。文本也可以有阴影。阴影可以让一幅图像看上去是立体的或者是浮动的。

Figure 7-1 A shadow

image

阴影有三个属性:

  1. x偏移值,用于指定阴影相对于图片在水平方向上的偏移值。
  2. y偏移值,用于指定阴影相对于图片在竖直方向上的偏移值。
  3. 模糊(blur)值,用于指定图像是有一个硬边(hard edge,如图7-2左边图片所示),还是一个漫射边(diffuse edge,如图7-1右边图片所示)

本章将描述阴影是如何工作的及如何用Quartz 2D API来创建阴影。

Figure 7-2 A shadow with no blur and another with a soft edge

image

阴影是如何工作的

Quartz中的阴影是图形状态的一部分。我们可以调用函数CGContextSetShadow来创建,并传入一个图形上下文、偏移值及模糊值。阴影被设置后,任何绘制的对象都有一个阴影,且该阴影在设备RGB颜色空间中呈现出黑色的且alpha值为1/3。换句话说,阴影是用RGBA值{0, 0, 0, 1.0/3.0}设置的。

我们可以调用函数CGContextSetShadowWithColor来设置彩色阴影,并传递一个图形上下文、 偏移值、模糊值有CGColor颜色对象。颜色值依赖于颜色空间。

如何在调用CGContextSetShadow或CGContextSetShadowWithColor之前保存了图形状态,我们可以通过恢复图形状态来关闭阴影。我们也可以通过设置阴影颜色为NULL来关闭阴影。

基于图形上下文的阴影绘制惯例

偏移值指定了阴影相对于相关图像的位置。这些偏移值由图形上下文来描述,并用于计算阴影的位置:

  1. 一个正值的x偏移量指定阴影位于图形对象的右侧。
  2. 在Mac OS X中,正值的y指定阴影位于图形对象的上边,这与Quartz 2D默认的坐标值匹配。
  3. 在iOS中,如果我们用Quartz 2D API来创建PDF或者位图图形上下文,则正值的y指定阴影位于图形对象的上边。
  4. 在iOS中,如果图形上下文是由UIKit创建的,则正值的y指定阴影位于图形对象的下边。这与UIKit坐标系统相匹配。
  5. 阴影绘制惯例不受CTM影响

绘制阴影

按照如下步骤来绘制阴影

  1. 保存图形状态
  2. 调用函数CGContextSetShadow,传递相应的值
  3. 使用阴影绘制所有的对象
  4. 恢复图形状态

按照如下步骤来绘制彩色阴影

  1. 保存图形状态
  2. 创建一个CGColorSpace对象,确保Quartz能正确地解析阴影颜色
  3. 创建一个CGColor对象来指定阴影的颜色
  4. 调用CGContextSetShadowWithColor,并传递相应的值
  5. 使用阴影绘制所有的对象
  6. 恢复图形状态

图7-3显示了两个带有阴影的矩形,其中一个是彩色阴影。

Figure 7-3 A colored shadow and a gray shadow

image

列表清单显示了如何创建图7-3中的图像。

Listing 7-1 A function that sets up shadows

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
void MyDrawWithShadows (CGContextRef myContext, float wd, float ht);
{
CGSize myShadowOffset = CGSizeMake (-15, 20);
float myColorValues[] = {1, 0, 0, .6};
CGColorRef myColor;
CGColorSpaceRef myColorSpace;
CGContextSaveGState(myContext);
CGContextSetShadow (myContext, myShadowOffset, 5);
// Your drawing code here
CGContextSetRGBFillColor (myContext, 0, 1, 0, 1);
CGContextFillRect (myContext, CGRectMake (wd/3 + 75, ht/2 , wd/4, ht/4));
myColorSpace = CGColorSpaceCreateDeviceRGB ();
myColor = CGColorCreate (myColorSpace, myColorValues);
CGContextSetShadowWithColor (myContext, myShadowOffset, 5, myColor);
// Your drawing code here
CGContextSetRGBFillColor (myContext, 0, 0, 1, 1);
CGContextFillRect (myContext, CGRectMake (wd/3-75,ht/2-100,wd/4,ht/4));
CGColorRelease (myColor);
CGColorSpaceRelease (myColorSpace);
CGContextRestoreGState(myContext);
}

Quartz 2D编程指南之六:模式(Pattern)

发表于 2014-12-05   |   分类于 翻译

模式(Pattern)是绘制操作的一个序列,这些绘制操作可以重复地绘制到一个图形上下文上。我们可以像使用颜色一样使用这些模式。当我们使用pattern来绘制时,Quartz将Page分割成模式单元格的集合,其中每个单元格的大小不是模式图片的大小,并使用我们提供的回调函数来绘制这些单元格。图6-1演示了一个绘制到window图形上下文的模式。

Figure 6-1 A pattern drawn to a window

image

模式的骨架(Anatomy)

模式单元格是模式的基础组件。图6-1中的模式的单元格如图6-2所示。其中黑色边框不是模式单元格的一部分;之所以画出来是为了显示模式单元格的边界。

Figure 6-2 A pattern cell

image

该模式单元格的大小包含四个带颜色的矩形以及这些矩形上部及右侧的白色区域,如图6-3所示。每个模式单元格的黑色边框不是单元格的一部分;画出来只是为是标明单元格的边界。当我们创建一个模式单元格时,我们需要定义单元格的边界并在这个范围内进行绘制。

Figure 6-3 Pattern cells with black rectangles drawn to show the bounds of each cell

image

我们可以指定水平和竖直方向上两个单元格之间的间距。图6-3所绘制的单元格是相互紧挨着的。而图6-4在两个方向上都指定了单元格之间的间距。我们可以为两个方向指定不同的间距。我们亦可以指定间距为负数,这样单元格便会重叠。

Figure 6-4 Spacing between pattern cells

image

当我们绘制一个模式单元格时,Quartz使用模式空间(pattern space)作为坐标系统。模式空间是一个抽象空间,它会使用我们创建模式时指定的变换矩阵(pattern matrix)来映射到默认用户空间。

注意:模式空间与用户空间是分开的。未转换的模式空间映射到基础的用户空间(未转换的),而不管当前转换矩阵(CTM)。当我们在模式空间上应用转换时,Quartz只将转换应用于模式空间。

如果我们不想要Quartz来转换模式单元格,我们可以指定单位矩阵。然而,我们可以使用转换矩阵来达到有趣的效果。图6-5显示了缩放6-2中的模式单元格的效果。图6-6旋转了这些单元格。图6-7则平移了这些单元格。

Figure 6-5 A scaled pattern cell

image

Figure 6-6 A rotated pattern cell

image
Figure 6-7 A translated pattern cell

image

着色模式(Colored Patterns)和模板模式(Stencil Patterns)

着色模式有与其相关的固有颜色。如果修改了创建模式单元格的颜色,则模式也便失去了意义。图6-8中显示的苏格兰格子就是着色模式的一个例子。着色模式中的颜色是模式单元格创建流程的一部分,而不是绘制流程的一部分。

Figure 6-8 A colored pattern has inherent color

image

而其它模式只限定了形状,因此可以认为是模板模式(或者是非着色模式、甚至可以作为图像蒙板)。图6-9中展示的红色和黑色星星就是使用相同的模式单元格。单元格由一个五角星组成。当定义模式单元格时,没有与之相关的颜色。颜色值是在绘制过程中指定的,而不是创建过程的一部分。

Figure 6-9 A stencil pattern does not have inherent color

image

在Quartz 2D中,我们可以创建这两种模式。

平铺(Tiling)

平铺(Tiling)是将模式单元格绘制到页面(Page)的某个部分的过程。当Quartz将模式渲染到一个设备时,Quartz可能需要调整模式以适应设备空间。即,在用户空间定义的模式单元格在渲染到设备时可能无法精确匹配,这是由用户空间单元和设备像素之间的差异导致的。

Quartz有三个平铺选项,以在必要时调整模式:

  1. 没有失真(no distortion): 以细微调整模式单元格之间的间距为代价,但通常不超过一个设备像素。
  2. 最小的失真的恒定间距:设定单元格之间的间距,以细微调整单元大小为代价,但通常不超过一个设备像素。
  3. 恒定间距:设定单元格之间间距,以调整单元格大小为代价,以求尽快的平铺

模式如何工作

模式操作类似于颜色,我们设置一个填充或描边(stroke)模式,然后调用绘制函数。Quartz使用我们设置的模式作为“涂料”。例如,如果我们要使用纯色绘制一个填充的的矩形,我们首先调用函数(如CGContextSetFillColor)来设置填充颜色。然后调用函数CGContextFillRect以使用我们指定的颜色来填充矩形。为了绘制一个模式,颜色调用函数CGContextSetFillPattern来设置指定的模式。绘制颜色和绘制模式的不同之处在于我们必须先定义一个模式。我们为函数CGContextSetFillPattern提供模式和颜色信息。我们将在下面的绘制着色模式和绘制模板模式章节看到如何创建、设置和绘制模式。

这里有个例子说明Quartz在幕后是如何绘制一个模式的。当我们填充或描边一个模式时,Quartz会按照以下指令来绘制每一个模式单元格:

  1. 保存图形状态
  2. 将当前转换矩阵应用到原始的模式单元格上
  3. 连接CTM与模式矩阵
  4. 裁剪模式单元格的边界矩形
  5. 调用绘制回调函数来绘制单元格
  6. 恢复图形状态

Quartz会执行所有平铺操作,重复绘制模式单元格到绘制空间,直到渲染满整个空间。我们可以填充和描边一个模式。模式单元格可以是指定的任何大小。如果我们想要看到模式,我们需要确保模式单元格与绘制空间匹配。例如,如果我们的模式单元格是8*10个单位的,而我们用这个模式来描边一个只有2个单位的直线,则这个模式单元格将会被裁剪。这种情况下,我们可能无法辨认出我们的模式。

绘制着色模式

绘制着色模式需要执行以下五步操作:

  1. 写一个绘制着色模式单元格的回调函数
  2. 设置着色模式的颜色空间
  3. 设置着色模式的骨架(Anatomy)
  4. 指定着色模式作为填充或描边模式
  5. 使用着色模式绘制

绘制模板模式也是类似这几步。两者之间的区别在于如何设置颜色信息。

写一个绘制着色模式单元格的回调函数

一个模式单元格看起来是什么样的完全取决于我们。在这个例子中,代码清单6-1绘制了图6-2所示的模式单元格。

Listing 6-1 A drawing callback that draws a colored pattern cell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define H_PATTERN_SIZE 16
#define V_PATTERN_SIZE 18
void MyDrawColoredPattern (void *info, CGContextRef myContext)
{
CGFloat subunit = 5;
CGSize size = {subunit, subunit};
CGPoint point1 = {0,0}, point2 = {subunit, subunit}, point3 = {0,subunit}, point4 = {subunit,0};
CGRect myRect1 = {point1, size}, myRect2 = {point2, size}, myRect3 = {point3, size}, myRect4 = {point4, size};
CGContextSetRGBFillColor (myContext, 0, 0, 1, 0.5);
CGContextFillRect (myContext, myRect1);
CGContextSetRGBFillColor (myContext, 1, 0, 0, 0.5);
CGContextFillRect (myContext, myRect2);
CGContextSetRGBFillColor (myContext, 0, 1, 0, 0.5);
CGContextFillRect (myContext, myRect3);
CGContextSetRGBFillColor (myContext, .5, 0, .5, 0.5);
CGContextFillRect (myContext, myRect4);
}

模式单元格绘制函数是类似于下面这种格式的一个回调函数

1
2
3
4
typedef void (*CGPatternDrawPatternCallback) (
void *info,
CGContextRef context
);

我们可以随意命名我们的回调函数。代码清单6-1中命名为MyDrawColoredPattern。这个回调函数带有两个参数:

  1. info: 一个指向模式相关数据的指针。这个参数是可选的,可以传递NULL。传递给回调的数据与后面创建模式的数据是一样的。
  2. context: 绘制模式单元格的图形上下文

代码清单6-1中绘制的模式单元格是随意的。以下是一些关于绘制代码的重要信息:

  1. 需要声明模式大小。在绘制时我们需要记住模式大小。在这个例子中,大小是全局声明的,绘制函数没有具体提到大小,除了在注释中。然后,我们将模式大小指定给Quartz 2D。
  2. 绘制函数后面是由CGPatternDrawPatternCallback回调函数类型定义定义的原型
  3. 代码中执行的绘制设置了颜色,让其成为一个着色模式。

设置着色模式的颜色空间

代码清单6-1中的代码使用颜色来绘制模式单元格。我们必须设置基本的模式颜色空间为NULL,以确保Quartz使用绘制路径指定的颜色来绘制,如代码清单6-2所示。

Listing 6-2 Creating a base pattern color space

1
2
3
4
5
6
7
8
9
10
CGColorSpaceRef patternSpace;
// 创建模式颜色空间,并传递NULL作为参数
patternSpace = CGColorSpaceCreatePattern (NULL);
// 在模式颜色空间中设置填充颜色
CGContextSetFillColorSpace (myContext, patternSpace);
// 释放模式颜色空间
CGColorSpaceRelease (patternSpace);

设置着色模式的骨架

一个模式的骨架基本信息保存在CGPattern对象中。我们调用CGPatternCreate函数来创建一个CGPattern对象,其原型如代码清单6-3所示:

Listing 6-3 The CGPatternCreate function prototype

1
2
3
4
5
6
7
8
CGPatternRef CGPatternCreate ( void *info,
CGRect bounds,
CGAffineTransform matrix,
CGFloat xStep,
CGFloat yStep,
CGPatternTiling tiling,
bool isColored,
const CGPatternCallbacks *callbacks );

其中,

  1. info:是一个指针,指向我们要传递给绘制回调函数的数据
  2. bound:指定模式单元格的大小
  3. matrix:指定模式矩阵,它将模式坐标系统映射到图形上下文的默认坐标系统。如果希望两个坐标系统是一样的,则可以使用单位矩阵。
  4. xStep, yStep:指定单元格之间的水平和竖直间距。
  5. tiling:平铺模式,可以是kCGPatternTilingNoDistortion、kCGPatternTilingConstantSpacingMinimalDistortion、kCGPatternTilingConstantSpacing
  6. isColored:指定模式单元格是着色模式(true)还是模板模式(false)
  7. callbacks:是一个指向CGPatternCallbacks结构体的指针,则定义如下:
1
2
3
4
5
6
struct CGPatternCallbacks
{
unsigned int version;
CGPatternDrawPatternCallback drawPattern;
CGPatternReleaseInfoCallback releaseInfo;
};

我们可以设置version为0。drawPattern是指向绘制回调的指针。releaseInfo是指向一个回调函数,该回调在释放CGPattern对象时被调用,以释放存储在我们传递给绘制回调的info参数中的数据。如果在这个参数中没有传递任何数据,则设置该域为NULL。

指定着色模式作为填充或描边模式

我们可以调用CGContextSetFillPattern或者CGContextSetStrokePattern函数来使用模式进行填充或描边。Quartz可以将模式用于任何填充或描边流程。

这两个函数包含以下几个参数:

  1. 图形上下文
  2. 先前创建的CGPattern对象
  3. 颜色组件的数组

虽然着色模式提供了自己的颜色,我们仍然需要传递一个单一的alpha值来告诉Quartz在绘制时着色模式的透明度。alpha值的范围在0到1中。可以如以下代码来设置着色模式的透明度:

1
2
3
CGFloat alpha = 1;
CGContextSetFillPattern (myContext, myPattern, &alpha);

使用颜色模式绘制

在完成前面的步骤之后,我们就可以调用Quartz 2D函数来绘制了。我们的模式被当作“涂料”。例如,可以调用CGContextStrokePath, CGContextFillPath, CGContextFillRect或其它函数来绘制。

完整示例

代码清单6-4包含一个绘制着色模式的函数。这个函数包含了前面讨论的所有步骤。

Listing 6-4 A function that paints a colored pattern

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
void MyColoredPatternPainting (CGContextRef myContext,
CGRect rect)
{
CGPatternRef pattern;
CGColorSpaceRef patternSpace;
CGFloat alpha = 1,
width, height;
static const CGPatternCallbacks callbacks = {0,
&MyDrawPattern,
NULL};
CGContextSaveGState (myContext);
patternSpace = CGColorSpaceCreatePattern (NULL);
CGContextSetFillColorSpace (myContext, patternSpace);
CGColorSpaceRelease (patternSpace);
pattern = CGPatternCreate (NULL,
CGRectMake (0, 0, H_PSIZE, V_PSIZE),
CGAffineTransformMake (1, 0, 0, 1, 0, 0),
H_PATTERN_SIZE,
V_PATTERN_SIZE,
kCGPatternTilingConstantSpacing,
true,
&callbacks);
CGContextSetFillPattern (myContext, pattern, &alpha);
CGPatternRelease (pattern);
CGContextFillRect (myContext, rect);
CGContextRestoreGState (myContext);
}

绘制模板模式

与绘制着色模式类似,绘制模板模式也有5个步骤:

  1. 写一个绘制模板模式单元格的回调函数
  2. 设置模板模式的颜色空间
  3. 设置模板模式的骨架(Anatomy)
  4. 指定模板模式作为填充或描边模式
  5. 使用模板模式绘制

绘制模板模式与绘制着色模式的区别在于设置颜色信息。

写一个绘制模板模式单元格的回调函数

绘制模板模式单元格的回调与前面描述的绘制颜色模式单元格类似。不同的是绘制模式单元格回调不需要指定颜色值。图6-10中显示的模式单元格即没有从绘制回调中获取颜色。

Figure 6-10 A stencil pattern cell

image

代码清单6-5绘制了图6-10中的模式单元格。可以看到代码只是简单地创建并填充了一个路径,而没有设置颜色。

Listing 6-5 A drawing callback that draws a stencil pattern cell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define PSIZE 16 // size of the pattern cell
static void MyDrawStencilStar (void *info, CGContextRef myContext)
{
int k;
double r, theta;
r = 0.8 * PSIZE / 2;
theta = 2 * M_PI * (2.0 / 5.0); // 144 degrees
CGContextTranslateCTM (myContext, PSIZE/2, PSIZE/2);
CGContextMoveToPoint(myContext, 0, r);
for (k = 1; k < 5; k++) {
CGContextAddLineToPoint (myContext,
r * sin(k * theta),
r * cos(k * theta));
}
CGContextClosePath(myContext);
CGContextFillPath(myContext);
}

设置模板模式的颜色空间

模板模式要求我们设置一个模式颜色空间用于Quartz的绘制,如代码清单6-6所示。

Listing 6-6 Code that creates a pattern color space for a stencil pattern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CGPatternRef pattern;
CGColorSpaceRef baseSpace;
CGColorSpaceRef patternSpace;
// 创建一个通用RGB颜色空间。
baseSpace = CGColorSpaceCreateWithName (kCGColorSpaceGenericRGB);
// 创建一个模式颜色空间。该颜色空间指定如何表示模式的颜色。后面要设置模式的颜色时,必须使用这个颜色空间来进行设置
patternSpace = CGColorSpaceCreatePattern (baseSpace);
// 设置颜色空间来在填充模式时使用
CGContextSetFillColorSpace (myContext, patternSpace);
// 释放模式颜色空间
CGColorSpaceRelease(patternSpace);
// 释放基础颜色空间
CGColorSpaceRelease(baseSpace);

设置模板模式的骨架(Anatomy)

这一步与上面设置着色模式是一样的,不同的是isColored参数需要传递false。

指定模板模式作为填充或描边模式

我们可以调用CGContextSetFillPattern或者CGContextSetStrokePattern函数来使用模式进行填充或描边。Quartz可以将模式用于任何填充或描边流程。

这两个函数包含以下几个参数:

  1. 图形上下文
  2. 先前创建的CGPattern对象
  3. 颜色组件的数组

由于模板模式在绘制回调中不提供颜色值,所以我们必须传递一个颜色给填充或描边函数来告诉Quartz使用什么颜色。代码清单6-7显示了为模板模式设置颜色的例子。

Listing 6-7 Code that sets opacity for a colored pattern

1
2
static const CGFloat color[4] = { 0, 1, 1, 0.5 }; //cyan, 50% transparent
CGContextSetFillPattern (myContext, myPattern, color);

使用模板模式绘制

在完成前面的步骤之后,我们就可以调用Quartz 2D函数来绘制了。我们的模式被当作“涂料”。例如,可以调用CGContextStrokePath, CGContextFillPath, CGContextFillRect或其它函数来绘制。

完整示例

代码清单6-8包含一个绘制模板模式的函数。这个函数包含了前面讨论的所有步骤。

Listing 6-8 A function that paints a stencil pattern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define PSIZE 16
void MyStencilPatternPainting (CGContextRef myContext,
const Rect *windowRect)
{
CGPatternRef pattern;
CGColorSpaceRef baseSpace;
CGColorSpaceRef patternSpace;
static const CGFloat color[4] = { 0, 1, 0, 1 };
static const CGPatternCallbacks callbacks = {0, &drawStar, NULL};
baseSpace = CGColorSpaceCreateDeviceRGB ();
patternSpace = CGColorSpaceCreatePattern (baseSpace);
CGContextSetFillColorSpace (myContext, patternSpace);
CGColorSpaceRelease (patternSpace);
CGColorSpaceRelease (baseSpace);
pattern = CGPatternCreate(NULL, CGRectMake(0, 0, PSIZE, PSIZE),
CGAffineTransformIdentity, PSIZE, PSIZE,
kCGPatternTilingConstantSpacing,
false, &callbacks);
CGContextSetFillPattern (myContext, pattern, color);
CGPatternRelease (pattern);
CGContextFillRect (myContext,CGRectMake (0,0,PSIZE*20,PSIZE*20));
}

Quartz 2D编程指南之五:变换

发表于 2014-12-02   |   分类于 翻译

Quartz 2D 绘制模型定义了两种独立的坐标空间:用户空间(用于表现文档页)和设备空间(用于表现设备的本地分辨率)。用户坐标空间用浮点数表示坐标,与设备空间的像素分辨率没有关系。当我们需要一个点或者显示文档时, Quartz会将用户空间坐标系统映射到设备空间坐标系统。因此,我们不需要重写应用程序或添加额外的代码来调整应用程序的输出以适应不同的设备。

我们可以通过操作CTM(current transformation matrix)来修改默认的用户空间。在创建图形上下文后,CTM是单位矩阵,我们可以使用 Quartz的变换函数来修改CTM,从而修改用户空间中的绘制操作。

本章内容包括:

  1. 变换操作函数概览
  2. 如何修改CTM
  3. 如何创建一个仿射变换
  4. 如何选择两个相同的变换
  5. 如何获取user-to-device-space变换

Quartz变换函数

我们可能使用Quartz内置的变换函数方便的平移、旋转和缩放我们的绘图。只需要短短几行代码,我们便可以按顺序应用变换或结合使用变换。图5-1显示了缩放和旋转一幅图片的效果。我们使用的每个变换操作都更新了CTM。CTM总是用于表示用户空间和设备空间的当前映射关系。这种映射确保了应用程序的输出在任何显示器或打印机上看上去都很棒。

Figure 5-1 Applying scaling and rotation

image

Quartz 2D API提供了5个函数,以允许我们获取和修改CTM。我们可以旋转、平移、缩放CTM。我们还可以联结一个仿射变换矩阵。

有时我们可以不想操作用户空间,直到我们决定将变换应用到CTM时,Quartz为此允许我们创建应用于此的仿射矩阵。我们可以使用另外一组函数来创建仿射变换,这些变换可以与CTM联结在一起。

我们可以不需要了解矩阵的数学含义而使用这些函数。

修改CTM(Current Transformation Matrix)

我们在绘制图像前操作CTM来旋转、缩放或平移page,从而变换我们将要绘制的对象。以变换CTM之前,我们需要保存图形状态,以便绘制后能恢复。我们同样能用仿射矩阵来联结CTM。在本节中,我们将介绍与CTM函数相关的四种操作–平移、旋转、缩放和联结。

假设我们提供了一个可用的图形上下文、一个指向可绘制图像的矩形的指针和一个可用的CGImage对象,则下面一行代码绘制了一个图像。该行代码可以绘制如图5-2所示的图片。在阅读了本节余下的部分后,我们将看到如何将变换应用于图像。

1
CGContextDrawImage (myContext, rect, myImage);

Figure 5-2 An image that is not transformed

image

平移变换根据我们指定的x, y轴的值移动坐标系统的原点。我们通过调用CGContextTranslateCTM函数来修改每个点的x, y坐标值。如图5-3显示了一幅图片沿x轴移动了100个单位,沿y轴移动了50个单位。具体代码如下:

1
CGContextTranslateCTM (myContext, 100, 50);

Figure 5-3 A translated image

image

旋转变换根据指定的角度来移动坐标空间。我们调用CGContextRotateCTM函数来指定旋转角度(以弧度为单位)。图5-4显示了图片以原点(左下角)为中心旋转45度,代码所下所示:

1
CGContextRotateCTM (myContext, radians(–45.));

由于旋转操作使图片的部分区域置于上下文之外,所以区域外的部分被裁减。我们用弧度来指定旋转角度。如果需要进行旋转操作,下面的代码将会很有用

1
2
#include <math.h>
static inline double radians (double degrees) {return degrees * M_PI/180;}

Figure 5-4 A rotated image

image

缩放操作根据指定的x, y因子来改变坐标空间的大小,从而放大或缩小图像。x, y因子的大小决定了新的坐标空间是否比原始坐标空间大或者小。另外,通过指定x因子为负数,可以倒转x轴,同样可以指定y因子为负数来倒转y轴。通过调用CGContextScaleCTM函数来指定x, y缩放因子。图5-5显示了指定x因子为0.5,y因子为0.75后的缩放效果。代码如下:

1
CGContextScaleCTM (myContext, .5, .75);

Figure 5-5 A scaled image

image

联合变换将两个矩阵相乘来联接现价变换操作。我们可以联接多个矩阵来得到一个包含所有矩阵累积效果矩阵。通过调用CGContextConcatCTM来联接CTM和仿射矩阵。
另外一种得到累积效果的方式是执行两个或多个变换操作而不恢复图形状态。图5-6显示了先平移后旋转一幅图片的效果,代码如下:

1
2
CGContextTranslateCTM (myContext, w,h);
CGContextRotateCTM (myContext, radians(-180.));

Figure 5-6 An image that is translated and rotated

image

图5-7显示了平移、缩放和旋转一幅图片,代码如下:

1
2
3
CGContextTranslateCTM (myContext, w/4, 0);
CGContextScaleCTM (myContext, .25, .5);
CGContextRotateCTM (myContext, radians ( 22.));

Figure 5-7 An image that is translated, scaled, and then rotated

image

变换操作的顺序会影响到最终的效果。如果调换顺序,将得到不同的结果。调换上面代码的顺序将得到如图5-8所示的效果,代码如下:

1
2
3
CGContextRotateCTM (myContext, radians ( 22.));
CGContextScaleCTM (myContext, .25, .5);
CGContextTranslateCTM (myContext, w/4, 0);

Figure 5-8 An image that is rotated, scaled, and then translated

image

创建仿射变换

仿射变换操作在矩阵上,而不是在CTM上。我们可以使用这些函数来构造一个之后用于CTM(调用函数CGContextConcatCTM)的矩阵。仿射变换函数使用或者返回一个CGAffineTransform数据对象。我们可以构建简单或复杂的仿射变换。

仿射变换函数能实现与CTM函数相同的操作–平移、旋转、缩放、联合。表5-1列出了仿射变换函数及其用途。注意每种变换都有两个函数。

Table 5-1 Affine transform functions for translation, rotation, and scaling

image

Quartz同样提供了一个仿射变换函数(CGAffineTransformInvert)来倒置矩阵。倒置操作通常用于在变换对象中提供点的倒置变换。当我们需要恢复一个被矩阵变换的值时,可以使用倒置操作。将值与倒置矩阵相乘,就可得到原先的值。我们通常不需要倒置操作,因为我们可以通过保存和恢复图形状态来倒置CTM的效果。

在一些情况下,我们可能不需要变换整修空间,而只是一个点或一个大小。我们通过调用CGPointApplyAffineTransform在CGPoint结构上执行变换操作。调用CGSizeApplyAffineTransform在CGSize结构上执行变换操作。调用CGRectApplyAffineTransform在CGRect结构上执行变换操作。CGRectApplyAffineTransform返回一个最小的矩形,该矩形包含了被传递给CGRectApplyAffineTransform的矩形对象的角点。如果矩形上的仿射变换操作只有缩放和平移操作,则返回的矩形与四个变换后的角组成的矩形是一致的。

可以通过调用函数CGAffineTransformMake来创建一个新的仿射变换,但与其它函数不同的是,它需要提供一个矩阵实体。

评价仿射变换

我们可以通过调用CGAffineTransformEqualToTransform函数来决定一个仿射变换是否与另一个相同。如果两个变换相同,则返回true;否则返回false。

函数CGAffineTransformIsIdentity用于确认一个变换是否是单位变换。单位变换没有平移、缩放和旋转操作。Quartz常量CGAffineTransformIdentity表示一个单位变换。

获取用户空间到设备空间的变换

当使用Quartz 2D时,我们只是在用户空间下工作。Quartz为我们处理用户空间和设备空间的转换。如果我们的应用程序需要获取Quartz转换用户空间和设备空间的仿射变换,我们可以调用函数CGContextGetUserSpaceToDeviceSpaceTransform。

Quartz提供了一系列的函数来转换用户空间和设备空间的几何体。我们会发现这些函数使用赶来比使用CGContextGetUserSpaceToDeviceSpaceTransform函数返回的仿射变换更好用。

  1. 点:函数CGContextConvertPointToDeviceSpace和CGContextConvertPointToUserSpace将一个CGPoint数据结构从一个空间变换到另一个空间。
  2. 大小:函数CGContextConvertSizeToDeviceSpace和CGContextConvertSizeToUserSpace将一个CGSize数据结构从一个空间变换到另一个空间。
  3. 矩形:函数CGContextConvertRectToDeviceSpace和CGContextConvertRectToUserSpace将一个CGPoint数据结构从一个空间变换到另一个空间。

Quartz 2D编程指南之四:颜色与颜色空间

发表于 2014-12-01   |   分类于 翻译

不同的设备(显示器、打印机、扫描仪、摄像头)处理颜色的方式是不同的。每种设备都有其所能支持的颜色值范围。一种设备能支持的颜色可能在其它设备中无法支持。

为了有效的使用颜色及理解Quartz 2D中用于颜色及颜色空间的函数,我们需要熟悉在Color Management Overview文档中所使用的术语。该文档中讨论了色觉、颜色值、设备依赖及设备颜色空间、颜色匹配问题、再现意图(rendering intent)、颜色管理模块和ColorSync。

在本章中,我们将学习Quartz处理颜色和颜色空间,以及什么是alpha组件。本章同时也讨论如下问题:

  1. 创建颜色空间
  2. 创建和设置颜色
  3. 设置再现意图

颜色与颜色空间

Quartz中的颜色是用一组值来表示。而颜色空间用于解析这些颜色信息。例如,表4-1列出了在全亮度下蓝色值在不同颜色空间下的值。如果不知道颜色空间及颜色空间所能接受的值,我们没有办法知道一组值所表示的颜色。

Table 4-1 Color values in different color spaces

image

如果我们使用了错误的颜色空间,我们可能会获得完全不同的颜色,如图4-1所示。

Figure 4-1 Applying a BGR and an RGB color profile to the same image

image

颜色空间可以有不同数量的组件。表4-1中的颜色空间中其中三个只有三个组件,而CMYK有四个组件。值的范围与颜色空间有关。对大部分颜色空间来说,颜色值范围为[0.0, 1.0],1.0表示全亮度。例如,全亮度蓝色值在Quartz的RGB颜色空间中的值是(0, 0, 1.0)。在Quartz中,颜色值同样有一个alpha值来表示透明度。在表4-1中没有列出该值。

alpha值

alpha值是图形状态参数,Quartz用它来确定新的绘图对象如何与已存在的对象混合。在全强度下,新的绘图对象是不透明的。在0强度下,新的绘图对象是完全透明的。图4-2显示了5个大的方形,分别使用了alpha值为1.0, 0.75, 0.5, 0.1和0.0。随着大方形逐渐变得透明,底下的小的不透明的方形逐渐显现出来。

Figure 4-2 A comparison of large rectangles painted using various alpha values

image

我们可以将两个对象绘制到page上,而page可以在渲染前通过设置全局的graphics context来设置自己的透明度。图4-3显示了将全局的透明度设置为0.5和1.0的效果。

Figure 4-3 A comparison of global alpha values

image

在标准混合模式(图形状态的默认模式)下,Quartz使用下面的公式来混合源颜色和目标颜色的组件:

destination = (alpha source) + (1 - alpha) destination

其中源颜色是新绘制的颜色,目标颜色是背景颜色。该公式可用于新绘制的形状和图像。

对于对象透明度来说,alpha值为1.0时表示新对象是完全不透明的,值0.0表示新对象是完全透明的。0.0与1.0之间的值指定对象的透明度。我们可以为所有接受颜色的程序指定一个alpha值作为颜色值的最后一个组件。同样也可以使用CGContextSetAlpha函数来指定全局的alpha值。记住,如果同时设置以上两个值,Quartz将混合全局alpha值与对象的alpha值。

为了让page完全透明,我们可以调用CGContextClearRect函数来清除图形上下文(graphics context)的alpha通道。例如,我们可以在给图标创建一个透明遮罩或者使窗口的背景透明时,采用这种方法。

创建颜色空间

Quartz支持颜色管理系统使用的标准颜色空间,也支持通用的颜色空间、索引颜色空间和模式(pattern)颜色空间。设备颜色空间以一种简便的方法在不同设备间表示颜色。它用于在两种不同设备间的本地颜色空间转换颜色数据。设备依赖颜色空间的颜色在不同设备上显示时效果是一样的,它扩展了设备的能力。基于此,设备依赖颜色空间是显示颜色时最好的选择。

如果应用程序有精确的颜色表示需求,则应该总是使用设备依赖颜色空间。通用颜色空间(generic color space)是一种常用的设备依赖颜色空间。通用颜色空间通过操作系统为我们的应用程序提供最好的颜色空间。它能使在显示器上与在打印机上打印效果是一样的。

重要:IOS不支持设备依赖颜色空间或通用颜色空间。IOS应用程序必须使用设备颜色空间(device color space)。

创建设备依赖颜色空间

为了创建设备依赖颜色空间,我们需要给Quartz提供白色参考点,黑色参考点及特殊设备的gamma值。Quartz使用这些信息将源颜色空间的颜色值转化为输出设备颜色空间的颜色值。

Quartz支持设备依赖颜色空间,创建此空间的函数如下:

  1. L*a*b是非线性转换,它属于Munsell颜色符号系统(该系统使用色度、值、饱和度来指定颜色)。 L组件表示亮度值,a组件表示绿色与红色之间的值,b组件表示蓝色与黄色之间的值。该颜色空间设计用于模拟人脑解码颜色。使用函数CGColorSpaceCreateLab来创建。
  2. ICC颜色空间是由ICC(由国际色彩聪明,International Color Consortium)颜色配置而来的。ICC颜色配置了设备支持的颜色域,该颜色域与其它设备属性相符,所以该信息可被用于将一个设备的颜色空间精确地转换为另一个设备的颜色空间。大多数设备制造商都支持ICC配置。一些彩色显示器和打印机都内嵌了ICC信息,用于处理诸如TIFF的位图格式。使用函数CGColorSpaceCreateICCBased来创建。
  3. 标准化RGB是设备依赖的RGB颜色空间,它表示相对于白色参考点(设备可生成的最白的颜色)的颜色。 使用函数CGColorSpaceCreateCalibratedRGB来创建。
  4. 标准化灰度是设备依赖的灰度颜色空间,它表示相对于白色参考点(设备可生成的最白的颜色)的颜色。 使用函数CGColorSpaceCreateCalibratedGray来创建。

创建通用颜色空间

通用颜色空间的颜色与系统匹配。大部分情况下,结果是可接受的。就像名字所暗示的那样,每个“通用”颜色空间(generic gray, generic RGB, generic CMYK)都是一个指定的设备依赖颜色空间。

通过颜色空间非常容易使用;我们不需要提供任何参考点信息。我们使用函数CGColorSpaceCreateWithName来创建一个通用颜色空间,该函数可传入以下常量值:

  1. kCGColorSpaceGenericGray:指定通用灰度颜色空间,该颜色空间是单色的,可以指定从0.0(纯黑)到1.0(纯白)范围内的颜色值。
  2. kCGColorSpaceGenericRGB:指定通用RGB颜色空间,该颜色空间中的颜色值由三个组件(red, green, blue)组成,主要用于彩色显示器上的像素。RGB颜色空间中的每个组件的值范围是[0.0, 1.0]。
  3. kCGColorSpaceGenericCMYK:指定通用CMYK颜色空间,该颜色空间的颜色值由四个组件(cyan, magenta, yellow, black),主要用于打印机。CMYK颜色空间的每个组件的值范围是[0.0, 1.0]。

创建设备颜色空间

设备颜色空间主要用于IOS应用程序,因为其它颜色空间无法在IOS上使用。大多数情况下,Mac OS X应用程序应使用通用颜色空间,而不使用设备颜色空间。但是有些Quartz程序希望图像使用设备颜色空间。例如,如果调用CGImageCreateWithMask函数来指定一个图像作为遮罩,图像必须在设备的灰度颜色空间(device gray color space)中定义。

我们可以使用以下函数来创建设备颜色空间:

  1. CGColorSpaceCreateDeviceGray:创建设备依赖灰度颜色空间
  2. CGColorSpaceCreateDeviceRGB:创建设备依赖RGB颜色空间
  3. CGColorSpaceCreateDeviceCMYK:创建设备依赖CMYK颜色空间

创建索引颜色空间和模式颜色空间

索引颜色空间包含一个有256个词目的颜色表,和词目映射到基础颜色空间。颜色表中每个词目指定一个基础颜色空间中的颜色值。使用CGColorSpaceCreateIndexed函数来创建。

模式颜色空间在绘制模式时使用。 使用CGColorSpaceCreatePattern函数来创建。

设置和创建颜色

Quartz提供了一套函数用于设置填充颜色、线框颜色、颜色空间和alpha值。每个颜色参数都是图形状态参数,这就意味着一旦设置了,设置将被保存并影响后续操作,直到被修改为止。

一个颜色必须有相关联的颜色空间。否则,Quartz不知道如何解析颜色值。进一步说,说是我们必须为绘制目标提供一个合适的颜色空间。如图4-4所示,左边是CMYK颜色空间中的蓝色填充色,右边是RGB颜色空间中的蓝色填充色。这两个颜色值在理论上是一样的,但只有在相同颜色空间下的相同颜色值显示出来才是一样的。

Figure 4-4 A CMYK fill color and an RGB fill color

image

我们可以使用CGContextSetFillColorSpace和CGContextSetStrokeColorSpace函数来设置填充和线框颜色空间,或者可以使用以下便利函数来设置设备颜色空间的颜色值。

Table 4-2 Color-setting functions

image

我们在填充及线框颜色空间中指定填充及线框颜色值。例如,在RGB颜色空间中,我们使用数组(1.0, 0.0, 0.0, 1.0)来表示红色。前三个值指定红色值为全强度,而绿色和蓝色为零强度。第四个值为alpha值,用于指定颜色的透明度。

如果需要在程序中重复使用颜色,最有效的方法是通过设置填充色和线框色来创建一个CGColor对象,然后将该对象传递给函数CGContextSetFillColorWithColor及CGContextSetStrokeColorWithColor。我们可以按需要保持CGColor对象,并可以直接使用该对象来改进应用程序的显示。

我们可以调用CGColorCreate函数来创建CGColor对象,该函数需要两个参数:CGColorspace对象及颜色值数组。数组的最后一个值指定alpha值。

设置再现意图(Rending Intent)

“再现意图”用于指定如何将源颜色空间的颜色映射到图形上下文的目标颜色空间的颜色范围内。如果不显示指定再现意图,Quartz使用相对色度再现意图(relative colorimetric rendering intent)应用于所有绘制(不包含位图图像)。对于位图图像,Quartz默认使用感知(perceptual)再现意图。

我们可以调用CGContextSetRenderingIntent函数来设置再现意图,并传递图形上下文(graphics context)及下例常量作为参数:

  1. kCGRenderingIntentDefault:使用默认的渲染意图。
  2. kCGRenderingIntentAbsoluteColorimetric:绝对色度渲染意图。将输出设备颜色域外的颜色映射为输出设备域内与之最接近的颜色。这可以产生一个裁减效果,因为色域外的两个不同的颜色值可能被映射为色域内的同一个颜色值。当图形使用的颜色值同时包含在源色域及目标色域内时,这种方法是最好的。常用于logo或者使用专色(spot color)时。
  3. kCGRenderingIntentRelativeColorimetric:相对色度渲染意图。转换所有的颜色(包括色域内的),以补偿图形上下文的白点与输出设备白点之间的色差。kCGRenderingIntentPerceptual:感知渲染意图。通过压缩图形上下文的色域来适应输出设备的色域,并保持源颜色空间的颜色之间的相对性。感知渲染意图适用于相片及其它复杂的高细度图片。
  4. kCGRenderingIntentSaturation:饱和度渲染意图。把颜色转换到输出设备色域内时,保持颜色的相对饱和度。结果是包含亮度、饱和度颜色的图片。饱和度意图适用于生成低细度的图片,如描述性图表。

Quartz 2D编程指南之三:路径(Path)

发表于 2014-11-16   |   分类于 翻译

路径定义了一个或多个形状,或是子路径。一个子路径可由直线,曲线,或者同时由两者构成。它可以是开放的,也可以是闭合的。一个子路径可以是简单的形状,如线、圆、矩形、星形;也可以是复杂的形状,如山脉的轮廓或者是涂鸦。图3-1显示了一些我们可以创建的路径。左上角的直线可以是虚线;直线也可以是实线。上边中间的路径是由多条曲线组成的开放路径。右上角的同心圆填充了颜色,但没有描边。左下角的加利福尼亚州是闭合路径,由许多曲线和直线构成,且对路径进行填充和描边。两个星形阐明了填充路径的两种方式,我们将在本章详细描述。

Figure 3-1 Quartz supports path-based drawing

image

在本章中,我们将学习如何构建路径,如何对路径进行填充和描边,及影响路径表现形式的参数。

创建及绘制路径

路径创建及路径绘制是两个独立的工作。首先我们创建路径。当我们需要渲染路径时,我们需要使用Quartz来绘制它。正如图3-1中所示,我们可以选择对路径进行描边,填充路径,或同时进行这两种操作。我们同样可以将其它对象绘制到路径所表示的范围内,即对对象进行裁减。

图3-2绘制了一个路径,该路径包含两个子路径。左边的子路径是一个矩形,右边的子路径是由直线和曲线组成的抽象形状。两个子路径都进行了填充及描边。

Figure 3-2 A path that contains two shapes, or subpaths

image

图3-3显示了多条独立绘制的路径。每个路径饮食随机生成的曲线,一些进行填充,另一些进行了描边。这些路径都包含在一个圆形裁减区域内。

Figure 3-3 A clipping area constrains drawing

image

构建块(Building Block)

子路径是由直线、弧和曲线构成的。Quartz同样也提供了简便的函数用于添加矩形或椭圆等形状。点也是路径最基本的构建块,因为点定义了形状的起始点与终止点。

点

点由x, y坐标值定义,用于在用户空间指定 一个位置。我们可以调用函数CGContextMoveToPoint来为新的子路径指定起始点。Quartz跟踪当前点,用于记录路径构建过程中最新的位置。例如,如果调用函数CGContextMoveToPoint并设置位置为(10, 10),即将当前点移动到位置(10, 10)。如果在水平位置绘制50个单位长度的直线,则直线的终点为(60, 10),该点变成当前点。直线、弧和曲线总是从当前点开始绘制。

通常我们通过传递(x, y)值给Quartz函数来指定一个点。一些函数需要我们传递一个CGPoint数据结构,该结构包含两个浮点值。

直线

直线由两个端点定义。起始点通常是当前点,所以创建直线时,我们只需要指定终止点。我们使用函数CGContextAddLineToPoint来添加一条直线到子路径中。

我们可以调用CGContextAddLines函数添加一系列相关的直线到子路径中。我们传递一个点数组给这个函数。第一个点必须是第一条直线的起始点;剩下的点是端点。Quartz从第一个点开始绘制一个新子路径,然后每两个相邻点连接成一条线段。

弧

弧是圆弧段。Quartz提供了两个函数来创建弧。函数CGContextAddArc从圆中来创建一个曲线段。我们指定一个圆心,半径和放射角(以弧度为单位)。放射角为2 PI时,创建的是一个圆。图3-4显示了多个独立的路径。每个路径饮食一个自动生成的圆;一些是填充的,另一些是描边的。

Figure 3-4 Multiple paths; each path contains a randomly generated circle

image

函数CGContextAddArcToPoint用于为矩形创建内切弧的场景。Quartz使用我们提供的端点创建两条正切线。同样我们需要提供圆的半径。弧心是两条半径的交叉点,每条半径都与相应的正切线垂直。弧的两个端点是正切线的正切点,如图3-5所示。红色的部分是实际绘制的部分。

Figure 3-5 Defining an arc with two tangent lines and a radius

image

如果当前路径已经包含了一个子路径,Quartz将追加一条从当前点到弧的起始点的直线段。如果当前路径为空,Quartz将创建一个新的子路径,该子路径从弧的起始点开始,而不添加初始的直线段。

曲线

二次与三次Bezier曲线是代数曲线,可以指定任意的曲线形状。曲线上的点通过一个应用于起始、终点及一个或多个控制点的多项式计算得出。这种方式定义的形状是向量图的基础。这个公式比将位数组更容易存储,并且曲线可以在任何分辨下重新创建。

图3-6显示了一些路径的曲线。每条路径包含一条随机生成的曲线;一些是填充的,另一些是描边的。

Figure 3-6 Multiple paths; each path contains a randomly generated curve

image

我们使用函数CGContextAddCurveToPoint将Bezier曲线连接到当前点,并传递控制点和端点作为参数,如图3-7所示。两个控制点的位置决定了曲线的形状。如果两个控制点都在两个端点上面,则曲线向上凸起。如果两个控制点都在两个端点下面,则曲线向下凹。如果第二个控制点比第一个控制点离得当前点近,则曲线自交叉,创建了一个回路。

Figure 3-7 A cubic Bézier curve uses two control points

image

我们也可以调用函数CGContextAddQuadCurveToPoint来创建Bezier,并传递端点及一个控制点,如图3-8所示。控制点决定了曲线弯曲的方向。由于只使用一个控制点,所以无法创建出如三次Bezier曲线一样多的曲线。例如我们无法创建出交叉的曲线。

Figure 3-8 A quadratic Bézier curve uses one control point

image

闭合路径

我们可以调用函数CGContextClosePath来闭合曲线。该函数用一条直接来连接当前点与起始点,以使路径闭合。起始与终点重合的直线、弧和曲线并不自动闭合路径,我们必须调用CGContextClosePath来闭合路径。

Quartz的一些函数将路径的子路径看成是闭合的。这些函数显示地添加一条直线来闭合 子路径,如同调用了CGContextClosePath函数。

在闭合一条子路径后,如果程序再添加直线、弧或曲线到路径,Quartz将在闭合的子路径的起点开始创建一个子路径。

椭圆

椭圆是一种特殊的圆。椭圆是通过定义两个焦点,在平面内所有与这两个焦点的距离之和相等的点所构成的图形。图3-9显示了一些独立的路径。每个路径都包含一个随机生成的椭圆;一些进行了填充,另一边进行了描边。

Figure 3-9 Multiple paths; each path contains a randomly generated ellipse

image

我们可以调用CGContextAddEllipseInRect函数来添加一个椭圆到当前路径。我们提供一个矩形来定义一个椭圆。Quartz利用一系列的Bezier曲线来模拟椭圆。椭圆的中心就是矩形的中心。如果矩形的宽与高相等,则椭圆变成了圆,且圆的半径为矩形宽度的一半。如果矩形的宽与高不相等,则定义了椭圆的长轴与短轴。

添加到路径中的椭圆开始于一个move-to操作,结束于一个close-subpath操作,所有的移动方向都是顺时针。

矩形

我们可以调用CGContextAddRect来添加一个矩形到当前路径中,并提供一个CGRect结构体(包含矩形的原点及大小)作为参数。

添加到路径的矩形开始于一个move-to操作,结束于一个close-subpath操作,所有的移动方向都是顺时针。

我们也可能调用CGContextAddRects函数来添加一系列的矩形到当前路径,并传递一个CGRect结构体的数组。图3-10显示了一些独立的路径。每个路径包含一个随机生成的矩形;一些进行了填充,另一边进行了描边。

Figure 3-10 Multiple paths; each path contains a randomly generated rectangle

image

创建路径

当我们需要在一个图形上下文中构建一个路径时,我们需要调用CGContextBeginPath来标记Quartz。然后,我们调用函数CGContextMovePoint来设置每一个图形或子路径的起始点。在构建起始点后,我们可以添加直线、弧、曲线。记住如下规则:

  1. 在开始绘制路径前,调用函数CGContextBeginPath;
  2. 直线、弧、曲线开始于当前点。空路径没有当前点;我们必须调用CGContextMoveToPoint来设置第一个子路径的起始点,或者调用一个便利函数来隐式地完成该任务。
  3. 如果要闭合当前子路径,调用函数CGContextClosePath。随后路径将开始一个新的子路径,即使我们不显示设置一个新的起始点。
  4. 当绘制弧时,Quartz将在当前点与弧的起始点间绘制一条直线。
  5. 添加椭圆和矩形的Quartz程序将在路径中添加新的闭合子路径。
  6. 我们必须调用绘制函数来填充或者描边一条路径,因为创建路径时并不会绘制路径。

在绘制路径后,将清空图形上下文。我们也许想保留路径,特别是在绘制复杂场景时,我们需要反复使用。基于此,Quartz提供了两个数据类型来创建可复用路径—CGPathRef和CGMutablePathRef。我们可以调用函数CGPathCreateMutable来创建可变的CGPath对象,并可向该对象添加直线、弧、曲线和矩形。Quartz提供了一个类似于操作图形上下文的CGPath的函数集合。这些路径函数操作CGPath对象,而不是图形上下文。这些函数包括:

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

如果想要添加一个路径到图形上下文,可以调用CGContextAddPath。路径将保留在图形上下文中,直到Quartz绘制它。我们可以调用CGContextAddPath再次添加路径。

绘制路径

我们可以绘制填充或描边的路径。描边(Stroke)是绘制路径的边框。填充是绘制路径包含的区域。Quartz提供了函数来填充或描边路径。描边线的属性(宽度、颜色等),填充色及Quartz用于计算填充区域的方法都是图形状态的一部分。

影响描边的属性

我们可以使用表3-1中的属性来决定如何对路径进行描边操作。这边属性是图形上下文的一部分,这意味着我们设置的值将会影响到后续的描边操作,直到我们个性这些值。

Table 3-1 Parameters that affect how Quartz strokes the current path

image

linewidth是线的总宽度,单位是用户空间单元。

linejoin属性指定如何绘制线段间的联接点。Quartz支持表3-2中描述的联接样式。

Table 3-2 Line join styles

image

linecap指定如何绘制直线的端点。Quartz支持表3-3所示的线帽类型。默认的是butt cap。

Table 3-3 Line cap styles

image

闭合路径将起始点看作是一个联接点;起始点同样也使用选定的直线连接方法进行渲染。如果通过添加一条连接到起始点的直线来闭合路径,则路径的两个端点都使用选定的线帽类型来绘制。

Linedash pattern(虚线模式)允许我们沿着描边绘制虚线。我们通过在CGContextSetLineDash结构体中指定虚线数组和虚线相位来控制虚线的大小及位置。

CGContextSetLineDash结构如下:

1
2
3
4
5
6
void CGContextSetLineDash (
CGContextRef ctx,
float phase,
const float lengths[],
size_t count,
);

其中lengths属性指定了虚线段的长度,该值是在绘制片断与未绘制片断之间交替。phase属性指定虚线模式的起始点。图3-11显示了虚线模式:

Figure 3-11 Examples of line dash patterns

image

描边颜色空间(stroke color space)定义了Quartz如何解析描边的颜色。我们同样也可以指定一个封装了颜色和颜色空间的CGColorRef数据类型。

路径描边的函数

Quartz提供了表3-4中的函数来描边当前路径。其中一些是描边矩形及椭圆的便捷函数。

Table 3-4 Functions that stroke paths

image

函数CGContextStrokeLineSegments等同于如下代码

1
2
3
4
5
6
7
CGContextBeginPath(context);
for(k = 0; k < count; k += 2)
{
CGContextMoveToPoint(context,s[k].x, s[k].y); CGContextAddLineToPoint(context,s[k+1].x, s[k+1].y);
}
CGContextStrokePath(context);

当我们调用CGContextStrokeLineSegments时,我们通过点数组来指定线段,并组织成点对的形式。每一对是由线段的起始点与终止点组成。例如,数组的第一个点指定了第一条直线的起始点,第二个点是第一条直线的终点,第三个点是第二条直线的起始点,依此类推。

填充路径

当我们填充当前路径时,Quartz将路径包含的每个子路径都看作是闭合的。然后,使用这些闭合路径并计算填充的像素。 Quartz有两种方式来计算填充区域。椭圆和矩形这样的路径其区域都很明显。但是如果路径是由几个重叠的部分组成或者路径包含多个子路径(如图3-12所示),我们则有两种规则来定义填充区域。

默认的规则是非零缠绕数规则(nonzero windingnumber rule)。为了确定一个点是否需要绘制,我们从该点开始绘制一条直线穿过绘图的边界。从0开始计数,每次路径片断从左到右穿过直线是,计数加1;而从右到左穿过直线时,计数减1。如果结果为0,则不绘制该点,否则绘制。路径片断绘制的方向会影响到结果。图3-13显示了使用非缠绕数规则对内圆和外圆进行填充的结果。当两个圆绘制方向相同时,两个圆都被填充。如果方向相反,则内圆不填充。

我们也可以使用偶数-奇数规则。为了确定一个点是否被绘制,我们从该点开始绘制一条直线穿过绘图的边界。计算穿过该直线的路径片断的数目。如果是奇数,则绘制该点,如果是偶数,则不绘制该点。路径片断绘制的方向不影响结果。如图3-12所示,无论两个圆的绘制方向是什么,填充结果都是一样的。

Figure 3-12 Concentric circles filled using different fill rules

image

Quartz提供了表3-5中的函数来填充当前路径。其中一些是填充矩形及椭圆的便捷函数。

Table 3-5 Functions that fill paths

image

设置混合模式

混合模式指定了Quartz如何将绘图绘制到背景上。Quartz默认使用普通混合模式(normal blend mode),该模式使用如下公式来计算前景绘图与背景绘图如何混合:

1
result = (alpha * foreground) + (1 - alpha) *background

“颜色与颜色空间”章节里面详细讨论了颜色值的alpha组件,该组件用于指定颜色的透明度。在本章的例子中,我们可以假设颜色值是完全不透明的(alpha = 1)。对于不透明的颜色值,当我们用普通混合模式时,所有绘制于背景之上的绘图都会遮掩住背景。

我们可以调用函数CGContextSetBlendMode并传递适当的混合模式常量值来设置混合模式来达到我们想到的效果。记住混合模式是图形状态的一部分。如果调用了函数CGContextSaveGState来改变混合模式,则调用函数CGContextRestoreGState来重新设置混合模式为普通混合模式。

接下来的内容例举了不同混合模式上将图3-13的矩形绘制到图3-14的矩形之上的效果。背景图使用普通混合模式来绘制。然后调用CGContextSetBlendMode函数来改变混合模式。最后绘制前景矩形。

Figure 3-13 The rectangles painted in the foreground

image

Figure 3-14 The rectangles painted in the background

image

注意:我们同样可以使用混合来组合两张图片或将图片与图形上下文中已有的内容进行混合。

普通混合模式

由于普通混合模式是默认的混合模式,所以在设置了其它混合模式后,可以调用CGContextSetBlendMode并传递kCGBlendModeNormal来将混合模式重设为默认。图3-15显示了普通混合模式上图3-13与图3-14混合的效果。

Figure 3-15 Rectangles painted using normal blend mode

image

正片叠底混合模式(Mutiply Blend Mode)

正片叠底混合模式指定将前景的图片采样与背景图片采样相乘。结果颜色至少与两个采样颜色之一一样暗。图3-16显示了混合结果。我们可以调用CGContextSetBlendMode并传递kCGBlendModeMultiply来设置这种混合模式。

Figure 3-16 Rectangles painted using multiply blend mode

image

屏幕混合模式(Screen Blend Mode)

屏幕混合模式指定将前景图片采样的反转色与背景图片的反转色相乘。结果颜色至少与两种采样颜色之一一样亮。图3-17显示了混合结果。我们可以调用CGContextSetBlendMode并传递kCGBlendModeScreen来设置这种混合模式。

Figure 3-17 Rectangles painted using screen blend mode

image

叠加混合模式(Overlay Blend Mode)

叠加混合模式是将前景图片与背景图片或者正片叠底,或者屏幕化,这取决于背景颜色。背景颜色值与前景颜色值以反映出背景颜色的亮度与暗度。图3-18显示了混合效果。我们可以调用CGContextSetBlendMode并传递kCGBlendModeOverlay来设置这种混合模式。

Figure 3-18 Rectangles painted using overlay blend mode

image

暗化混合模式(Darken Blend Mode)

通过选择前景图片与背景图片更暗的采样来混合图片采样。背景图片采样被前景图片采样更暗的部分取代,而其它部分不变。图3-19显示了混合效果。我们可以调用CGContextSetBlendMode并传递kCGBlendModeDarken来设置这种混合模式。

Figure 3-19 Rectangles painted using darken blend mode

image

亮化混合模式(Lighten Blend Mode)

通过选择前景图片与背景图片更亮的采样来混合图片采样。背景图片采样被前景图片采样更亮的部分取代,而其它部分不变。图3-20显示了混合效果。我们可以调用CGContextSetBlendMode并传递kCGBlendModeLighten来设置这种混合模式。

Figure 3-20 Rectangles painted using lighten blend mode

image

色彩减淡模式(ColorDodge Blend Mode)

加亮背景图片采样以反映出前景图片采样。被指定为黑色的前景图片采样值将不产生变化。图3-21显示了混合效果。我们可以调用CGContextSetBlendMode并传递kCGBlendModeColorDodge来设置这种混合模式。

Figure 3-21 Rectangles painted using color dodge blend mode

image

色彩加深模式(ColorBurn Blend Mode)

加深背景图片采样以反映出前景图片采样。被指定为白色的前景图片采样值将不产生变化。图3-21显示了混合效果。我们可以调用CGContextSetBlendMode并传递kCGBlendModeColorBurn来设置这种混合模式。

Figure 3-22 Rectangles painted using color burn blend mode

image

柔光混合模式(SoftLight Blend Mode)

根据前景采样颜色减淡或加深颜色值。如果前景采样颜色比50%灰度值更亮,则减淡背景,类似于Dodge模式。如果前景采样颜色比50%灰度值更暗,则加强背景,类似于Burn模式。纯黑或纯白的图片采样将产生更暗或更亮的区域。但是但是不产生纯白或纯黑的颜色。该效果类似于将一个漫射光源放于一个前景图前。该效果用于在场景中添加高光效果。图3-23显示了混合效果。我们可以调用CGContextSetBlendMode并传递kCGBlendModeSoftLight来设置这种混合模式。

Figure 3-23 Rectangles painted using soft light blend mode

image

强光混合模式(Hard Light Blend Mode)

根据前景图片采样颜色正片叠加或屏幕化颜色。如果前景采样颜色比50%灰度值更亮,则减淡背景,类似于screen模式。如果前景采样颜色比50%灰度值更暗,则加深背景,类似于multiply模式。如果前景采样颜色等于50%灰度,则前景颜色不变。纯黑与纯白的颜色图片采样将产生纯黑或纯白的颜色值。该效果类似于将一个强光源放于一个前景图前。该效果用于在场景中添加高光效果。图3-24显示了混合效果。我们可以调用CGContextSetBlendMode并传递kCGBlendModeHardLight来设置这种混合模式。

Figure 3-24 Rectangles painted using hard light blend mode

image

差值混合模式(Difference Blend Mode)

将前景图片采样颜色值与背景图片采样值相减,相减的前后关系取决于哪个采样的亮度值更大。黑色的前景采样值不发生变化;白色值转化为背景的值。图3-25显示了混合效果。我们可以调用CGContextSetBlendMode并传递kCGBlendModeDifference来设置这种混合模式。

Figure 3-25 Rectangles painted using difference blend mode

image

排除混合模式(Exclusion Blend Mode)

该效果类似于Difference效果,只是对比度更低。黑色的前景采样值不发生变化;白色值转化为背景的值。图3-26显示了混合效果。我们可以调用CGContextSetBlendMode并传递kCGBlendModeExclusion来设置这种混合模式。

Figure 3-26 Rectangles painted using exclusion blend mode

image

色相混合模式(Hue Blend Mode)

使用背景的亮度和饱和度与前景的色相混合。图3-27显示了混合效果。我们可以调用CGContextSetBlendMode并传递kCGBlendModeHue来设置这种混合模式。

Figure 3-27 Rectangles painted using hue blend mode

image

饱和度混合模式(Saturation Blend Mode)

混合背景的亮度和色相前景的饱和度。背景中没有饱和度的区域不发生变化。图3-28显示了混合效果。我们可以调用CGContextSetBlendMode并传递kCGBlendModeSaturation来设置这种混合模式。

Figure 3-28 Rectangles painted using saturation blend mode

image

颜色混合模式(Color Blend Mode)

混合背景的亮度值与前景的色相与饱和度。该模式保留图片的灰度级。我们可以使用该模式绘制单色图片或彩色图片。图3-29显示了混合效果。我们可以调用CGContextSetBlendMode并传递kCGBlendModeColor来设置这种混合模式。

Figure 3-29 Rectangles painted using color blend mode

image

亮度混合模式(Luminosity Blend Mode)

将背景图片的色相、饱和度与背景图片的亮度相混合。该模块产生一个与Color Blend模式相反的效果。图3-30显示了混合效果。我们可以调用CGContextSetBlendMode并传递kCGBlendModeLuminosity来设置这种混合模式。

Figure 3-30 Rectangles painted using luminosity blend mode

image

裁剪路径

当前裁剪区域是从路径中创建,作为一个遮罩,从而允许遮住我们不想绘制的部分。例如,我们有一个很大的图片,但只需要显示其中一小部分,则可以设置裁减区域来显示我们想显示的部分。

当我们绘制的时候,Quartz只渲染裁剪区域里面的东西。裁剪区域内的闭合路径是可见的;而在区域外的部分是不可见的。

当图形上下文初始创建时,裁减区域包含上下文所有的可绘制区域(例如,PDF上下文的media box)。我们可以通过设置当前路径来改变裁剪区域,然后使用裁减函数来取代绘制函数。裁剪函数与当前已有的裁剪区域求交集以获得路径的填充区域。因此,我们可以求交取得裁减区域,缩小图片的可视区域,但是不能扩展裁减区域。

裁减区域是图形状态的一部分。为了恢复先前的裁减区域,我们可以在裁减前保存图形状态,并在裁减绘制后恢复图形状态。

代码清单3-1显示了绘制圆形后设置裁减区域。该段代码使得绘图被裁减,效果类似于图3-3所示。

Listing 3-1 Setting up a circular clip area

1
2
3
4
CGContextBeginPath(context);
CGContextAddArc(context, w/2, h/2, ((w>h) ? h : w)/2, 0, 2*PI, 0);
CGContextClosePath(context);
CGContextClip(context);

Table 3-6 Functions that clip the graphics context

image

Quartz 2D编程指南之二:图形上下文(Graphics Contexts)

发表于 2014-11-11   |   分类于 翻译

一个Graphics Context表示一个绘制目标。它包含绘制系统用于完成绘制指令的绘制参数和设备相关信息。Graphics Context定义了基本的绘制属性,如颜色、裁减区域、线条宽度和样式信息、字体信息、混合模式等。

我们可以通过几种方式来获取Graphics Context:Quartz提供的创建函数、Mac OS X框架或IOS的UIKit框架提供的函数。Quartz提供了多种Graphics Context的创建函数,包括bitmap和PDF,我们可以使用这些Graphics Context创建自定义的内容。

本章介绍了如何为不同的绘制目标创建Graphics Context。在代码中,我们用CGContextRef来表示一个Graphics Context。当获得一个Graphics Context后,可以使用Quartz 2D函数在上下文(context)中进行绘制、完成操作(如平移)、修改图形状态参数(如线宽和填充颜色)等。

在iOS中的视图Graphics Context进行绘制

在iOS应用程序中,如果要在屏幕上进行绘制,需要创建一个UIView对象,并实现它的drawRect:方法。视图的drawRect:方法在视图显示在屏幕上及它的内容需要更新时被调用。在调用自定义的drawRect:后,视图对象自动配置绘图环境以便代码能立即执行绘图操作。作为配置的一部分,视图对象将为当前的绘图环境创建一个Graphics Context。我们可以通过调用UIGraphicsGetCurrentContext函数来获取这个Graphics Context。

UIKit默认的坐标系统与Quartz不同。在UIKit中,原点位于左上角,y轴正方向为向下。UIView通过将修改Quartz的Graphics Context的CTM[原点平移到左下角,同时将y轴反转(y值乘以-1)]以使其与UIView匹配。

在Mac OS X中创建一个窗口Graphics Context

在Mac OS X中绘制时,我们需要创建一个窗口Graphics Context。Quartz 2D API 没有提供函数来获取窗口Graphics Context。取而代之的是用Cocoa框架来获取一个窗口上下文。

我们可以在Cocoa应用程序的drawRect:中获取一个Quartz Graphics Context,如下代码所示:

1
CGContextRef myContext = [[NSGraphicsContext currentContext] graphicsPort];

currentContext方法在当前线程中返回NSGraphicsContext实例。graphicsPort方法返回一个低级别、平台相关的Graphics Context(Quartz Graphics Context)。

在获取到Graphics Context后,我们可以在Cocoa应用程序中调用任何Quartz 2D的绘制函数。我们同样可以将Quartz 2D与Cocoa绘制操作混合使用。如图2-1是一个在Cocoa视图中用Quartz 2D绘制的实例。绘图由两个长方形组成(一个不透明的红色长方形和半透明的蓝色长方形)。

Figure 2-1 A view in the Cocoa framework that contains Quartz drawing

image

为了实现图2-1实例,需要先创建一个Cocoa应用程序。在Interface Builder中,拖动一个Custom View到窗口中,并子类化。然后实现子类视图的,如代码清单2-1所示。视图的drawRect:包含了所有的Quartz绘制代码。

注:NSView的drawRect:方法在每次视图需要绘制时自动调用。

Listing 2-1 Drawing to a window graphics context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@implementation MyQuartzView
- (id)initWithFrame:(NSRect)frameRect
{
self = [super initWithFrame:frameRect];
return self;
}
- (void)drawRect:(NSRect)rec
{
CGContextRef myContext = [[NSGraphicsContext currentContext] graphicsPort]; //1
// ********** Your drawing code here ********** //2
CGContextSetRGBFillColor (myContext, 1, 0, 0, 1); //3
CGContextFillRect (myContext, CGRectMake (0, 0, 200, 100 )); //4
CGContextSetRGBFillColor (myContext, 0, 0, 1, .5); //5
CGContextFillRect (myContext, CGRectMake (0, 0, 100, 200)); //6
}
@end

代码说明:

  1. 为视图获取一个Graphics Context
  2. 插入绘图代码的地方。以下四行是使用Quartz 2D函数的例子
  3. 设置完全不透明的红色填充色。
  4. 填充一个长方形,其原点为(0, 0), 大小为(200, 100)
  5. 设置半透明的蓝色填充色。
  6. 填充一个长方形,其原点为(0, 0), 大小为(100, 200)

创建一个PDF Graphics Context

当创建一个PDF Graphics Context并绘制时,Quartz将绘制操作记录为一系列的PDF绘制命令并写入文件中。我们需要提供一个PDF输出的位置及一个默认的media box(用于指定页面边界的长方形)。图2-2显示了在PDF Graphics Context中绘制及在preview打开PDF的结果。

Figure 2-2 A PDF created by using CGPDFContextCreateWithURL

image

Quartz 2D API提供了两个函数来创建PDF Graphics Context:

  • CGPDFContextCreateWithURL:当你需要用Core Foundation URL指定pdf输出的位置时使用该函数。代码清单2-2显示了该函数的使用方法(代码2-2及后面代码的详细解释略):

Listing 2-2 Calling CGPDFContextCreateWithURL to create a PDF graphics context

1
2
3
4
5
6
7
8
9
10
11
CGContextRef MyPDFContextCreate (const CGRect *inMediaBox, CFStringRef path)
{
CGContextRef myOutContext = NULL;
CFURLRef url;
url = CFURLCreateWithFileSystemPath (NULL, path, kCFURLPOSIXPathStyle, false);
if (url != NULL) {
myOutContext = CGPDFContextCreateWithURL (url, inMediaBox, NULL);
CFRelease(url);
}
return myOutContext;
}
  • CGPDFContextCreate:当需要将pdf输出发送给数据用户时使用该方法。代码清单2-3显示了该函数的使用方法:

Listing 2-3 Calling CGPDFContextCreate to create a PDF graphics context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CGContextRef MyPDFContextCreate (const CGRect *inMediaBox, CFStringRef path)
{
CGContextRef myOutContext = NULL;
CFURLRef url;
CGDataConsumerRef dataConsumer;
url = CFURLCreateWithFileSystemPath (NULL, path, kCFURLPOSIXPathStyle, false);
if (url != NULL)
{
dataConsumer = CGDataConsumerCreateWithURL (url);
if (dataConsumer != NULL)
{
myOutContext = CGPDFContextCreate (dataConsumer, inMediaBox, NULL);
CGDataConsumerRelease (dataConsumer);
}
CFRelease(url);
}
return myOutContext;
}

代码清单2-4显示是如何调用MyPDFContextCreate程序及绘制操作。

Listing 2-4 Drawing to a PDF graphics context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CGRect mediaBox;
mediaBox = CGRectMake (0, 0, myPageWidth, myPageHeight);
myPDFContext = MyPDFContextCreate (&mediaBox, CFSTR("test.pdf"));
CFStringRef myKeys[1];
CFTypeRef myValues[1];
myKeys[0] = kCGPDFContextMediaBox;
myValues[0] = (CFTypeRef) CFDataCreate(NULL,(const UInt8 *)&mediaBox, sizeof (CGRect));
CFDictionaryRef pageDictionary = CFDictionaryCreate(NULL, (const void **) myKeys,
(const void **) myValues, 1,
&kCFTypeDictionaryKeyCallBacks,
& kCFTypeDictionaryValueCallBacks);
CGPDFContextBeginPage(myPDFContext, &pageDictionary);
// ********** Your drawing code here **********
CGContextSetRGBFillColor (myPDFContext, 1, 0, 0, 1);
CGContextFillRect (myPDFContext, CGRectMake (0, 0, 200, 100 ));
CGContextSetRGBFillColor (myPDFContext, 0, 0, 1, .5);
CGContextFillRect (myPDFContext, CGRectMake (0, 0, 100, 200 ));
CGPDFContextEndPage(myPDFContext);
CFRelease(pageDictionary);
CFRelease(myValues[0]);
CGContextRelease(myPDFContext);

我们可以将任何内容(图片,文本,绘制路径)绘制到pdf中,并能添加链接及加密。

创建位图Graphics Context

一个位图Graphics Context接受一个指向内存缓存(包含位图存储空间)的指针,当我们绘制一个位图Graphics Context时,该缓存被更新。在释放Graphics Context后,我们将得到一个我们指定像素格式的全新的位图。

注:位图Graphics Context有时用于后台绘制。CGLayer对象优化了后台绘制,因为Quartz在显卡上缓存了层。

iOS提示:iOS应用程序使用了UIGraphicsBeginImageContextWithOptions取代Quartz低层函数。如果使用Quartz创建一下后台bitmap,bitmap Graphics Context使用的坐标系统是Quartz默认的坐标系统。而使用UIGraphicsBeginImageContextWithOptions创建图形上下文,UIKit将会对坐标系统使用与UIView对象的图形上下文一样的转换。这允许应用程序使用相同的绘制代码而不需要担心坐标系统问题。虽然我们的应用程序可以手动调整CTM达到相同的效果,但这种做没有任何好处。

我们使用CGBitmapContextCreate来创建位图Graphics Context,该函数有如下参数:

  • data:一个指向内存目标的指针,该内存用于存储需要渲染的图形数据。内存块的大小至少需要(bytePerRow * height)字节。
  • width:指定位图的宽度,单位是像素(pixel)。
  • height:指定位图的高度, 单位是像素(pixel)。
  • bitsPerComponent:指定内存中一个像素的每个组件使用的位数。例如,一个32位的像素格式和一个rgb颜色空间,我们可以指定每个组件为8位。
  • bytesPerRow:指定位图每行的字节数。
  • colorspace:颜色空间用于位图上下文。在创建位图Graphics Context时,我们可以使用灰度(gray), RGB, CMYK, NULL颜色空间。
  • bitmapInfo:位图的信息,这些信息用于指定位图是否需要包含alpha组件,像素中alpha组件的相对位置(如果有的话),alpha组件是否是预乘的,及颜色组件是整型值还是浮点值。

代码清单2-5显示了如何创建位图Graphics Context。当向位图Graphics Context绘图时,Quartz将绘图记录到内存中指定的块中。

Listing 2-5 Creating a bitmap graphics context

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
CGContextRef MyCreateBitmapContext (int pixelsWide, int pixelsHigh)
{
CGContextRef context = NULL;
CGColorSpaceRef colorSpace;
void * bitmapData;
int bitmapByteCount;
int bitmapBytesPerRow;
bitmapBytesPerRow = (pixelsWide * 4);
bitmapByteCount = (bitmapBytesPerRow * pixelsHigh);
colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
bitmapData = calloc( bitmapByteCount );
if (bitmapData == NULL)
{
fprintf (stderr, "Memory not allocated!");
return NULL;
}
context = CGBitmapContextCreate (bitmapData, pixelsWide, pixelsHigh, 8, bitmapBytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast);
if (context== NULL)
{
free (bitmapData);
fprintf (stderr, "Context not created!");
return NULL;
}
CGColorSpaceRelease( colorSpace );
return context;
}

代码清单2-6显示了调用MyCreateBitmapContext 创建一个位图Graphics Context,使用位图Graphics Context来创建CGImage对象,然后将图片绘制到窗口Graphics Context中。绘制结果如图2-3所示:

Listing 2-6 Drawing to a bitmap graphics context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CGRect myBoundingBox;
myBoundingBox = CGRectMake (0, 0, myWidth, myHeight);
myBitmapContext = MyCreateBitmapContext (400, 300);
// ********** Your drawing code here **********
CGContextSetRGBFillColor (myBitmapContext, 1, 0, 0, 1);
CGContextFillRect (myBitmapContext, CGRectMake (0, 0, 200, 100 ));
CGContextSetRGBFillColor (myBitmapContext, 0, 0, 1, .5);
CGContextFillRect (myBitmapContext, CGRectMake (0, 0, 100, 200 ));
myImage = CGBitmapContextCreateImage (myBitmapContext);
CGContextDrawImage(myContext, myBoundingBox, myImage);
char *bitmapData = CGBitmapContextGetData(myBitmapContext);
CGContextRelease (myBitmapContext);
if (bitmapData) free(bitmapData);
CGImageRelease(myImage);

Figure 2-3 An image created from a bitmap graphics context and drawn to a window graphics context

image

支持的像素格式

表2-1总结了位图Graphics Context支持的像素格式,相关的颜色空间及像素格式支持的Mac OS X最早版本。像素格式用bpp(每像素的位数)和bpc(每个组件的位数)来表示。表格同时也包含与像素格式相关的位图信息常量。

表2-1:位图Graphics Context支持的像素格式

image

反锯齿

位图Graphics Context支持反锯齿,这一操作是人为的较正在位图中绘制文本或形状时产生的锯齿边缘。当位图的分辩率明显低于人眼的分辩率时就会产生锯齿。为了使位图中的对象显得平滑,Quartz使用不同的颜色来填充形状周边的像素。通过这种方式来混合颜色,使形状看起来更平滑。如图2-4显示的效果。我们可以通过调用CGContextSetShouldAntialias来关闭位图Graphics Context的反锯齿效果。反锯齿设置是图形状态的一部分。

可以调用函数CGContextSetAllowsAntialiasing来控制一个特定Graphics Context是否支持反锯齿;false表示不支持。该设置不是图形状态的一部分。当上下文及图形状态设置为true时,Quartz执行反锯齿。

Figure 2-4 A comparison of aliased and anti-aliasing drawing

image

获取打印的Graphics Context

Mac OS X中的Cocoa应用程序通过自定义的NSView子类来实现打印。一个视图通过调用print:方法来进行打印。然后视图以打印机为目标创建一个Graphics Context,并调用drawRect:方法。应用程序使用与在屏幕进行绘制相同的绘制代码。我们同样可以自定义drawRect: 方法将图形绘制到打印机。

Quartz 2D编程指南之一:概览

发表于 2014-11-10   |   分类于 翻译

前言

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

在Mac OS X中,Quartz 2D可以与其它图形图像技术混合使用,如Core Image、Core Video、OpenGL、QuickTime。例如,通过使用 QuickTime的GraphicsImportCreateCGImage函数,可以用 Quartz从一个 QuickTime图形导入器中创建一个图像。

Page

Quartz 2D在图像中使用了绘画者模型(painter’s model)。在绘画者模型中,每个连续的绘制操作都是将一个绘制层(a layer of ‘paint’)放置于一个画布(‘canvas’),我们通常称这个画布为Page。 Page上的绘图可以通过额外的绘制操作来叠加更多的绘图。Page上的图形对象只能通过叠加更多的绘图来改变。这个模型允许我们使用小的图元来构建复杂的图形。

图1-1展示了绘画者模型如何工作。从图中可以看出不同的绘制顺序所产生的效果不一样。

image

Page可以是一张纸(如果输出设备是打印机),也可以是虚拟的纸张(如果输出设备是PDF文件),还可以是bitmap图像。这根据实际使用的graphics context而定。

绘制目标:Graphics Context

Graphics Context是一个数据类型(CGContextRef),用于封装Quartz绘制图像到输出设备的信息。设备可以是PDF文件、bitmap或者显示器的窗口上。Graphics Context中的信息包括在Page中的图像的图形绘制参数和设备相关的表现形式。Quartz中所有的对象都是绘制到一个Graphics Context中。

我们可以将Graphics Context想像成绘制目标,如图1-2所示。当用Quartz绘图时,所有设备相关的特性都包含在我们所使用的Graphics Context中。换句话说,我们可以简单地给Quartz绘图序列指定不同的Graphics Context,就可将相同的图像绘制到不同的设备上。我们不需要处理任何设备相关的计算;这些都由Quartz替我们完成。

image

Quartz提供了以下几种类型的Graphics Context,详细的介绍将在后续章节说明。

  1. Bitmap Graphics Context
  2. PDF Graphics Context
  3. Window Graphics Context
  4. Layer Context
  5. Post Graphics Context

Quartz 2D 数据类型

除了 Graphics Context 之外,Quartz 2D API还定义一些数据类型。由于这些API就Core Graphics框架的一部分,所以这些数据类型都是以CG开头的。

Quartz 2D使用这些数据类型来创建对象,通过操作这些对象来获取特定的图形。图1-3例举了三个使用Quartz 2D的绘制操作所获得的图像。

image

下面列出了Quartz 2D包含的数据类型:

  1. CGPathRef:用于向量图,可创建路径,并进行填充或描画(stroke)
  2. CGImageRef:用于表示bitmap图像和基于采样数据的bitmap图像遮罩。
  3. CGLayerRef:用于表示可用于重复绘制(如背景)和幕后(offscreen)绘制的绘画层
  4. CGPatternRef:用于重绘图
  5. CGShadingRef、CGGradientRef:用于绘制渐变
  6. CGFunctionRef:用于定义回调函数,该函数包含一个随机的浮点值参数。当为阴影创建渐变时使用该类型
  7. CGColorRef, CGColorSpaceRef:用于告诉Quartz如何解释颜色
  8. CGImageSourceRef,CGImageDestinationRef:用于在Quartz中移入移出数据
  9. CGFontRef:用于绘制文本
  10. CGPDFDictionaryRef, CGPDFObjectRef, CGPDFPageRef, CGPDFStream, CGPDFStringRef, and CGPDFArrayRef:用于访问PDF的元数据
  11. CGPDFScannerRef, CGPDFContentStreamRef:用于解析PDF元数据
  12. CGPSConverterRef:用于将PostScript转化成PDF。在iOS中不能使用。

图形状态

Quartz通过修改当前图形状态(current graphics state)来修改绘制操作的结果。图形状态包含用于绘制程序的参数。绘制程序根据这些绘图状态来决定如何渲染结果。例如,当你调用设置填充颜色的函数时,你将改变存储在当前绘图状态中的颜色值。

Graphics Context包含一个绘图状态栈。当Quartz创建一个Graphics Context时,栈为空。当保存图形状态时,Quartz将当前图形状态的一个副本压入栈中。当还原图形状态时,Quartz将栈顶的图形状态出栈。出栈的状态成为当前图形状态。

可使用函数CGContextSaveGState来保存图形状态,CGContextRestoreGState来还原图形状态。

注意:并不是当前绘制环境的所有属性都是图形状态的元素。如,图形状态不包含当前路径(current path)。下面列出了图形状态相关的参数:

image

Quartz 2D 坐标系统

坐标系统定义是被绘制到Page上的对象的位置及大小范围,如图1-4所示。我们在用户空间坐标系统(user-space coordination system,简称用户空间)中指定图形的位置及大小。坐标值是用浮点数来定义的。

image

由于不同的设备有不同的图形功能,所以图像的位置及大小依赖于设备。例如,一个显示设备可能每英寸只能显示少于96个像素,而打印机可能每英寸能显示300个像素。如果在设备级别上定义坐标系统,则在一个设备上绘制的图形无法在其它设备上正常显示。

Quartz通过使用当前转换矩阵(current transformation matrix, CTM)将一个独立的坐标系统(user space)映射到输出设备的坐标系统(device space),以此来解决设备依赖问题。 CTM是一种特殊类型的矩阵(affine transform, 仿射矩阵),通过平移(translation)、旋转(rotation)、缩放(scale)操作将点从一个坐标空间映射到另外一个坐标空间。

CTM还有另外一个目的:允许你通过转换来决定对象如何被绘制。例如,为了绘制一个旋转了45度的盒子,我们可以在绘制盒子之前旋转Page的坐标系统。Quartz使用旋转过的坐标系统来将盒子绘制到输出设备中。

用户空间的点用坐标对(x, y)来表示,(0, 0)表示坐标原点。Quartz中默认的坐标系统是:沿着x轴从左到右坐标值逐渐增大;沿着y轴从下到上坐标值逐渐增大。

有一些技术在设置它们的graphics context时使用了不同于Quartz的默认坐标系统。相对于Quartz来说,这些坐标系统是修改的坐标系统(modified coordinate system),当在这些坐标系统中显示Quartz绘制的图形时,必须进行转换。最常见的一种修改的坐标系统是原点位于左上角,而沿着y轴从上到下坐标值逐渐增大。我们可以在如下一些地方见到这种坐标系统:

  1. 在Mac OS X中,重写过isFlipped方法以返回yes的NSView类的子类
  2. 在iOS中,由UIView返回的绘图上下文
  3. 在iOS中,通过调用UIGraphicsBeginImageContextWithOptions函数返回的绘图上下文

如果应用程序想以相同的绘制程序在一个UIView对象和PDF Graphics Context上进行绘制,需要做一个变换以使PDF Graphics Context使用与UIView相同的坐标系。要达到这一目的,只需要对PDF的上下文的原点做一个平移(移到左上角)和用-1对y坐标值进行缩放。图1-5显示了这种变换操作:

image

我们的应用程序负责调整Quartz调用以确保有一个转换应用到上下文中。例如,如果你想要一个图片或PDF正确的绘制到一个Graphics Context中,你的应用程序可能需要临时调整Graphics Context的CTM。在iOS中,如果使用UIImage对象来包裹创建的CGImage对象,可以不需要修改CTM。UIImage将自动进行补偿以适用UIKit的坐标系统。

重要:如果你打算在iOS上开发与Quartz相关的程序,了解以上所讨论的是很有用的,但不是必须的。在iOS 3.2及后续的版本中,当UIKit为你的应用程序创建一个绘图上下文时,也对上下文进行了额外的修改以匹配UIKit的约定。特别的,patterns和shadows(不被CTM影响)单独进行调整以匹配UIKit坐标系统。在这种情况下,没有一个等价的机制让CTM来转换Quartz和UIKit的上下文。我们必须认识到在什么样的上下文中进行绘制,并调整行为以匹配上下文的预期。

内存管理:对象所有权

Quartz使用Core Foundation内存管理模型(引用计数)。所以,对象的创建与销毁与通常的方式是一样的。在Quartz中,需要记住如下一些规则:

  1. 如果创建或拷贝一个对象,你将拥有它,因此你必须释放它。通常,如果使用含有”Create”或“Copy”单词的函数获取一个对象,当使用完后必须释放,否则将导致内存泄露。
  2. 如果使用不含有”Create”或“Copy”单词的函数获取一个对象,你将不会拥有对象的引用,不需要释放它。
  3. 如果你不拥有一个对象而打算保持它,则必须retain它并且在不需要时release掉。可以使用Quartz 2D的函数来指定retain和release一个对象。例如,如果创建了一个CGColorspace对象,则使用函数CGColorSpaceRetain和CGColorSpaceRelease来retain和release对象。同样,可以使用Core Foundation的CFRetain和CFRelease,但是注意不能传递NULL值给这些函数。

参考

  1. Quartz 2D Programming Guide

Objective-C Runtime 运行时之六:拾遗

发表于 2014-11-09   |   分类于 Objective-C

前面几篇基本介绍了runtime中的大部分功能,包括对类与对象、成员变量与属性、方法与消息、分类与协议的处理。runtime大部分的功能都是围绕这几点来实现的。

本章的内容并不算重点,主要针对前文中对Objective-C Runtime Reference内容遗漏的地方做些补充。当然这并不能包含所有的内容。runtime还有许多内容,需要读者去研究发现。

super

在Objective-C中,如果我们需要在类的方法中调用父类的方法时,通常都会用到super,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface MyViewController: UIViewController
@end
@implementation MyViewController
- (void)viewDidLoad {
[super viewDidLoad];
// do something
...
}
@end

如何使用super我们都知道。现在的问题是,它是如何工作的呢?

首先我们需要知道的是super与self不同。self是类的一个隐藏参数,每个方法的实现的第一个参数即为self。而super并不是隐藏参数,它实际上只是一个”编译器标示符”,它负责告诉编译器,当调用viewDidLoad方法时,去调用父类的方法,而不是本类中的方法。而它实际上与self指向的是相同的消息接收者。为了理解这一点,我们先来看看super的定义:

1
struct objc_super { id receiver; Class superClass; };

这个结构体有两个成员:

  1. receiver:即消息的实际接收者
  2. superClass:指针当前类的父类

当我们使用super来接收消息时,编译器会生成一个objc_super结构体。就上面的例子而言,这个结构体的receiver就是MyViewController对象,与self相同;superClass指向MyViewController的父类UIViewController。

接下来,发送消息时,不是调用objc_msgSend函数,而是调用objc_msgSendSuper函数,其声明如下:

1
id objc_msgSendSuper ( struct objc_super *super, SEL op, ... );

该函数第一个参数即为前面生成的objc_super结构体,第二个参数是方法的selector。该函数实际的操作是:从objc_super结构体指向的superClass的方法列表开始查找viewDidLoad的selector,找到后以objc->receiver去调用这个selector,而此时的操作流程就是如下方式了

1
objc_msgSend(objc_super->receiver, @selector(viewDidLoad))

由于objc_super->receiver就是self本身,所以该方法实际与下面这个调用是相同的:

1
objc_msgSend(self, @selector(viewDidLoad))

为了便于理解,我们看以下实例:

1
2
3
4
5
6
7
8
9
10
11
12
@interface MyClass : NSObject
@end
@implementation MyClass
- (void)test {
NSLog(@"self class: %@", self.class);
NSLog(@"super class: %@", super.class);
}
@end

调用MyClass的test方法后,其输出是:

1
2
2014-11-08 15:55:03.256 [824:209297] self class: MyClass
2014-11-08 15:55:03.256 [824:209297] super class: MyClass

从上例中可以看到,两者的输出都是MyClass。大家可以自行用上面介绍的内容来梳理一下。

库相关操作

库相关的操作主要是用于获取由系统提供的库相关的信息,主要包含以下函数:

1
2
3
4
5
6
7
8
// 获取所有加载的Objective-C框架和动态库的名称
const char ** objc_copyImageNames ( unsigned int *outCount );
// 获取指定类所在动态库
const char * class_getImageName ( Class cls );
// 获取指定库或框架中所有类的类名
const char ** objc_copyClassNamesForImage ( const char *image, unsigned int *outCount );

通过这几个函数,我们可以了解到某个类所有的库,以及某个库中包含哪些类。如下代码所示:

1
2
3
4
5
6
7
8
9
NSLog(@"获取指定类所在动态库");
NSLog(@"UIView's Framework: %s", class_getImageName(NSClassFromString(@"UIView")));
NSLog(@"获取指定库或框架中所有类的类名");
const char ** classes = objc_copyClassNamesForImage(class_getImageName(NSClassFromString(@"UIView")), &outCount);
for (int i = 0; i < outCount; i++) {
NSLog(@"class name: %s", classes[i]);
}

其输出结果如下:

1
2
3
4
5
6
7
8
9
10
2014-11-08 12:57:32.689 [747:184013] 获取指定类所在动态库
2014-11-08 12:57:32.690 [747:184013] UIView's Framework: /System/Library/Frameworks/UIKit.framework/UIKit
2014-11-08 12:57:32.690 [747:184013] 获取指定库或框架中所有类的类名
2014-11-08 12:57:32.691 [747:184013] class name: UIKeyboardPredictiveSettings
2014-11-08 12:57:32.691 [747:184013] class name: _UIPickerViewTopFrame
2014-11-08 12:57:32.691 [747:184013] class name: _UIOnePartImageView
2014-11-08 12:57:32.692 [747:184013] class name: _UIPickerViewSelectionBar
2014-11-08 12:57:32.692 [747:184013] class name: _UIPickerWheelView
2014-11-08 12:57:32.692 [747:184013] class name: _UIPickerViewTestParameters
......

块操作

我们都知道block给我们带到极大的方便,苹果也不断提供一些使用block的新的API。同时,苹果在runtime中也提供了一些函数来支持针对block的操作,这些函数包括:

1
2
3
4
5
6
7
8
// 创建一个指针函数的指针,该函数调用时会调用特定的block
IMP imp_implementationWithBlock ( id block );
// 返回与IMP(使用imp_implementationWithBlock创建的)相关的block
id imp_getBlock ( IMP anImp );
// 解除block与IMP(使用imp_implementationWithBlock创建的)的关联关系,并释放block的拷贝
BOOL imp_removeBlock ( IMP anImp );
  • imp_implementationWithBlock函数:参数block的签名必须是method_return_type ^(id self, method_args …)形式的。该方法能让我们使用block作为IMP。如下代码所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@interface MyRuntimeBlock : NSObject
@end
@implementation MyRuntimeBlock
@end
// 测试代码
IMP imp = imp_implementationWithBlock(^(id obj, NSString *str) {
NSLog(@"%@", str);
});
class_addMethod(MyRuntimeBlock.class, @selector(testBlock:), imp, "v@:@");
MyRuntimeBlock *runtime = [[MyRuntimeBlock alloc] init];
[runtime performSelector:@selector(testBlock:) withObject:@"hello world!"];

输出结果是

1
2014-11-09 14:03:19.779 [1172:395446] hello world!

弱引用操作

1
2
3
4
5
// 加载弱引用指针引用的对象并返回
id objc_loadWeak ( id *location );
// 存储__weak变量的新值
id objc_storeWeak ( id *location, id obj );
  • objc_loadWeak函数:该函数加载一个弱指针引用的对象,并在对其做retain和autoreleasing操作后返回它。这样,对象就可以在调用者使用它时保持足够长的生命周期。该函数典型的用法是在任何有使用__weak变量的表达式中使用。

● objc_storeWeak函数:该函数的典型用法是用于__weak变量做为赋值对象时。

这两个函数的具体实施在此不举例,有兴趣的小伙伴可以参考《Objective-C高级编程:iOS与OS X多线程和内存管理》中对__weak实现的介绍。

宏定义

在runtime中,还定义了一些宏定义供我们使用,有些值我们会经常用到,如表示BOOL值的YES/NO;而有些值不常用,如OBJC_ROOT_CLASS。在此我们做一个简单的介绍。

布尔值

1
2
#define YES (BOOL)1
#define NO (BOOL)0

这两个宏定义定义了表示布尔值的常量,需要注意的是YES的值是1,而不是非0值。

空值

1
2
#define nil __DARWIN_NULL
#define Nil __DARWIN_NULL

其中nil用于空的实例对象,而Nil用于空类对象。

分发函数原型

1
#define OBJC_OLD_DISPATCH_PROTOTYPES 1

该宏指明分发函数是否必须转换为合适的函数指针类型。当值为0时,必须进行转换

Objective-C根类

1
#define OBJC_ROOT_CLASS

如果我们定义了一个Objective-C根类,则编译器会报错,指明我们定义的类没有指定一个基类。这种情况下,我们就可以使用这个宏定义来避过这个编译错误。该宏在iOS 7.0后可用。

其实在NSObject的声明中,我们就可以看到这个宏的身影,如下所示:

1
2
3
4
5
6
7
8
__OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0)
OBJC_ROOT_CLASS
OBJC_EXPORT
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}

我们可以参考这种方式来定义我们自己的根类。

局部变量存储时长

1
#define NS_VALID_UNTIL_END_OF_SCOPE

该宏表明存储在某些局部变量中的值在优化时不应该被编译器强制释放。

我们将局部变量标记为id类型或者是指向ObjC对象类型的指针,以便存储在这些局部变量中的值在优化时不会被编译器强制释放。相反,这些值会在变量再次被赋值之前或者局部变量的作用域结束之前都会被保存。

关联对象行为

1
2
3
4
5
6
7
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};

这几个值在前面已介绍过,在此不再重复。

总结

至此,本系列对runtime的整理已完结。当然这只是对runtime的一些基础知识的归纳,力图起个抛砖引玉的作用。还有许多关于runtime有意思东西还需要读者自己去探索发现。

参考

  1. Objective-C Runtime Reference
  2. iOS:Objective-C中Self和Super详解
  3. Objective-C的动态特性

Objective-C Runtime 运行时之五:协议与分类

发表于 2014-11-08   |   分类于 Objective-C

Objective-C中的分类允许我们通过给一个类添加方法来扩充它(但是通过category不能添加新的实例变量),并且我们不需要访问类中的代码就可以做到。

Objective-C中的协议是普遍存在的接口定义方式,即在一个类中通过@protocol定义接口,在另外类中实现接口,这种接口定义方式也成为“delegation”模式,@protocol声明了可以呗其他任何方法类实现的方法,协议仅仅是定义一个接口,而由其他的类去负责实现。

在本章中,我们来看看runtime对分类与协议的支持。

基础数据类型

Category

Category是表示一个指向分类的结构体的指针,其定义如下:

1
2
3
4
5
6
7
8
9
typedef struct objc_category *Category;
struct objc_category {
char *category_name OBJC2_UNAVAILABLE; // 分类名
char *class_name OBJC2_UNAVAILABLE; // 分类所属的类名
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE; // 实例方法列表
struct objc_method_list *class_methods OBJC2_UNAVAILABLE; // 类方法列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 分类所实现的协议列表
}

这个结构体主要包含了分类定义的实例方法与类方法,其中instance_methods列表是objc_class中方法列表的一个子集,而class_methods列表是元类方法列表的一个子集。

Protocol

Protocol的定义如下:

1
typedef struct objc_object Protocol;

我们可以看到,Protocol其中实就是一个对象结构体。

操作函数

Runtime并没有在<objc/runtime.h>头文件中提供针对分类的操作函数。因为这些分类中的信息都包含在objc_class中,我们可以通过针对objc_class的操作函数来获取分类的信息。如下例所示:

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
@interface RuntimeCategoryClass : NSObject
- (void)method1;
@end
@interface RuntimeCategoryClass (Category)
- (void)method2;
@end
@implementation RuntimeCategoryClass
- (void)method1 {
}
@end
@implementation RuntimeCategoryClass (Category)
- (void)method2 {
}
@end
#pragma mark -
NSLog(@"测试objc_class中的方法列表是否包含分类中的方法");
unsigned int outCount = 0;
Method *methodList = class_copyMethodList(RuntimeCategoryClass.class, &outCount);
for (int i = 0; i < outCount; i++) {
Method method = methodList[i];
const char *name = sel_getName(method_getName(method));
NSLog(@"RuntimeCategoryClass's method: %s", name);
if (strcmp(name, sel_getName(@selector(method2)))) {
NSLog(@"分类方法method2在objc_class的方法列表中");
}
}

其输出是:

1
2
3
4
2014-11-08 10:36:39.213 [561:151847] 测试objc_class中的方法列表是否包含分类中的方法
2014-11-08 10:36:39.215 [561:151847] RuntimeCategoryClass's method: method2
2014-11-08 10:36:39.215 [561:151847] RuntimeCategoryClass's method: method1
2014-11-08 10:36:39.215 [561:151847] 分类方法method2在objc_class的方法列表中

而对于Protocol,runtime提供了一系列函数来对其进行操作,这些函数包括:

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
// 返回指定的协议
Protocol * objc_getProtocol ( const char *name );
// 获取运行时所知道的所有协议的数组
Protocol ** objc_copyProtocolList ( unsigned int *outCount );
// 创建新的协议实例
Protocol * objc_allocateProtocol ( const char *name );
// 在运行时中注册新创建的协议
void objc_registerProtocol ( Protocol *proto );
// 为协议添加方法
void protocol_addMethodDescription ( Protocol *proto, SEL name, const char *types, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 添加一个已注册的协议到协议中
void protocol_addProtocol ( Protocol *proto, Protocol *addition );
// 为协议添加属性
void protocol_addProperty ( Protocol *proto, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount, BOOL isRequiredProperty, BOOL isInstanceProperty );
// 返回协议名
const char * protocol_getName ( Protocol *p );
// 测试两个协议是否相等
BOOL protocol_isEqual ( Protocol *proto, Protocol *other );
// 获取协议中指定条件的方法的方法描述数组
struct objc_method_description * protocol_copyMethodDescriptionList ( Protocol *p, BOOL isRequiredMethod, BOOL isInstanceMethod, unsigned int *outCount );
// 获取协议中指定方法的方法描述
struct objc_method_description protocol_getMethodDescription ( Protocol *p, SEL aSel, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 获取协议中的属性列表
objc_property_t * protocol_copyPropertyList ( Protocol *proto, unsigned int *outCount );
// 获取协议的指定属性
objc_property_t protocol_getProperty ( Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty );
// 获取协议采用的协议
Protocol ** protocol_copyProtocolList ( Protocol *proto, unsigned int *outCount );
// 查看协议是否采用了另一个协议
BOOL protocol_conformsToProtocol ( Protocol *proto, Protocol *other );
  • objc_getProtocol函数,需要注意的是如果仅仅是声明了一个协议,而未在任何类中实现这个协议,则该函数返回的是nil。
  • objc_copyProtocolList函数,获取到的数组需要使用free来释放
  • objc_allocateProtocol函数,如果同名的协议已经存在,则返回nil
  • objc_registerProtocol函数,创建一个新的协议后,必须调用该函数以在运行时中注册新的协议。协议注册后便可以使用,但不能再做修改,即注册完后不能再向协议添加方法或协议

需要强调的是,协议一旦注册后就不可再修改,即无法再通过调用protocol_addMethodDescription、protocol_addProtocol和protocol_addProperty往协议中添加方法等。

小结

Runtime并没有提供过多的函数来处理分类。对于协议,我们可以动态地创建协议,并向其添加方法、属性及继承的协议,并在运行时动态地获取这些信息。

参考

  1. Objective-C Runtime Reference

Objective-C Runtime 运行时之四:Method Swizzling

发表于 2014-11-06   |   分类于 Objective-C

理解Method Swizzling是学习runtime机制的一个很好的机会。在此不多做整理,仅翻译由Mattt Thompson发表于nshipster的Method Swizzling一文。

Method Swizzling是改变一个selector的实际实现的技术。通过这一技术,我们可以在运行时通过修改类的分发表中selector对应的函数,来修改方法的实现。

例如,我们想跟踪在程序中每一个view controller展示给用户的次数:当然,我们可以在每个view controller的viewDidAppear中添加跟踪代码;但是这太过麻烦,需要在每个view controller中写重复的代码。创建一个子类可能是一种实现方式,但需要同时创建UIViewController, UITableViewController, UINavigationController及其它UIKit中view controller的子类,这同样会产生许多重复的代码。

这种情况下,我们就可以使用Method Swizzling,如在代码所示:

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
#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// When swizzling a class method, use the following:
// Class class = object_getClass((id)self);
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}
@end

在这里,我们通过method swizzling修改了UIViewController的@selector(viewWillAppear:)对应的函数指针,使其实现指向了我们自定义的xxx_viewWillAppear的实现。这样,当UIViewController及其子类的对象调用viewWillAppear时,都会打印一条日志信息。

上面的例子很好地展示了使用method swizzling来一个类中注入一些我们新的操作。当然,还有许多场景可以使用method swizzling,在此不多举例。在此我们说说使用method swizzling需要注意的一些问题:

Swizzling应该总是在+load中执行

在Objective-C中,运行时会自动调用每个类的两个方法。+load会在类初始加载时调用,+initialize会在第一次调用类的类方法或实例方法之前被调用。这两个方法是可选的,且只有在实现了它们时才会被调用。由于method swizzling会影响到类的全局状态,因此要尽量避免在并发处理中出现竞争的情况。+load能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。相比之下,+initialize在其执行时不提供这种保证–事实上,如果在应用中没为给这个类发送消息,则它可能永远不会被调用。

Swizzling应该总是在dispatch_once中执行

与上面相同,因为swizzling会改变全局状态,所以我们需要在运行时采取一些预防措施。原子性就是这样一种措施,它确保代码只被执行一次,不管有多少个线程。GCD的dispatch_once可以确保这种行为,我们应该将其作为method swizzling的最佳实践。

选择器、方法与实现

在Objective-C中,选择器(selector)、方法(method)和实现(implementation)是运行时中一个特殊点,虽然在一般情况下,这些术语更多的是用在消息发送的过程描述中。

以下是Objective-C Runtime Reference中的对这几个术语一些描述:

  1. Selector(typedef struct objc_selector *SEL):用于在运行时中表示一个方法的名称。一个方法选择器是一个C字符串,它是在Objective-C运行时被注册的。选择器由编译器生成,并且在类被加载时由运行时自动做映射操作。
  2. Method(typedef struct objc_method *Method):在类定义中表示方法的类型
  3. Implementation(typedef id (*IMP)(id, SEL, ...)):这是一个指针类型,指向方法实现函数的开始位置。这个函数使用为当前CPU架构实现的标准C调用规范。每一个参数是指向对象自身的指针(self),第二个参数是方法选择器。然后是方法的实际参数。

理解这几个术语之间的关系最好的方式是:一个类维护一个运行时可接收的消息分发表;分发表中的每个入口是一个方法(Method),其中key是一个特定名称,即选择器(SEL),其对应一个实现(IMP),即指向底层C函数的指针。

为了swizzle一个方法,我们可以在分发表中将一个方法的现有的选择器映射到不同的实现,而将该选择器对应的原始实现关联到一个新的选择器中。

调用_cmd

我们回过头来看看前面新的方法的实现代码:

1
2
3
4
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", NSStringFromClass([self class]));
}

咋看上去是会导致无限循环的。但令人惊奇的是,并没有出现这种情况。在swizzling的过程中,方法中的[self xxx_viewWillAppear:animated]已经被重新指定到UIViewController类的-viewWillAppear:中。在这种情况下,不会产生无限循环。不过如果我们调用的是[self viewWillAppear:animated],则会产生无限循环,因为这个方法的实现在运行时已经被重新指定为xxx_viewWillAppear:了。

注意事项

Swizzling通常被称作是一种黑魔法,容易产生不可预知的行为和无法预见的后果。虽然它不是最安全的,但如果遵从以下几点预防措施的话,还是比较安全的:

  1. 总是调用方法的原始实现(除非有更好的理由不这么做):API提供了一个输入与输出约定,但其内部实现是一个黑盒。Swizzle一个方法而不调用原始实现可能会打破私有状态底层操作,从而影响到程序的其它部分。
  2. 避免冲突:给自定义的分类方法加前缀,从而使其与所依赖的代码库不会存在命名冲突。
  3. 明白是怎么回事:简单地拷贝粘贴swizzle代码而不理解它是如何工作的,不仅危险,而且会浪费学习Objective-C运行时的机会。阅读Objective-C Runtime Reference和查看<objc/runtime.h>头文件以了解事件是如何发生的。
  4. 小心操作:无论我们对Foundation, UIKit或其它内建框架执行Swizzle操作抱有多大信心,需要知道在下一版本中许多事可能会不一样。
1…456…9
南峰子

南峰子

86 日志
7 分类
© 2017 南峰子
由 Hexo 强力驱动
主题 - NexT.Pisces