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

模式(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));
}