CodeV

6.5-状态和透明层

在继续深入渐变之前,本章需要退后一步,覆盖一个重要的Quartz绘图功能。 此特性在本章中的示例中使用,值得解释。

如果您使用Photoshop(或类似的图像合成和编辑应用程序),您可能熟悉图层。 图层将图形封装到不同的单个容器中。 您可以堆叠这些图层以构建复杂的绘图,并应用图层效果以向每个图层的内容添加阴影,高光和其他装饰。 重要的是,这些效果一次适用于整个图层,而不考虑创建图层内容的各个绘图操作。

Quartz提供了类似的功能,称为透明层。 这些层使您能够将多个绘图操作合并到单个缓冲区中。 图6-11说明了为什么要在应用程序中使用图层。

图6-11-1
图6-11-2

图6-11使用透明层确保绘制效果一次应用于整个绘图操作集合,而不是单个绘图请求。 Clarus的DogCow来自苹果的TechNote 31。

此图形被渲染为启用阴影的上下文。 在顶部图像中,阴影出现在图形的所有部分下,包括DogCow的“内部”。 这是因为这张照片是使用三个Bezier填充操作创建的:

  • 第一个操作填充了粉红色的DogCow乳房。 (Clarus纯粹主义者,请原谅异端邪说,我想要一个更复杂的形状来处理这个例子。)
  • 第二个填充图中的白色背景。
  • 第三个画出了斑点,眼睛和那个背景上的轮廓。

左下角的图像显示了用于这些绘图任务的Bezier路径的轮廓。 当这些路径作为三个操作执行时,上下文将阴影应用于每个绘制请求。 要创建单个复合图形,如图6-11右下角所示,您将改用Quartz透明层。 阴影仅应用于复合图形的边缘,而不是在组件的边缘。

透明层将绘制请求分组到独立的缓冲区中,与图形上下文分开。 在启动一个图层(通过调用CGContextBeginTransparencyLayer()),这个缓冲区用一个完全透明的背景初始化。 它的阴影被禁用,全局alpha设置为1。只有在完成绘制(通过调用CGContextEndTransparencyLayer())后,才会将图层的内容呈现给父上下文。

透明度块

与大多数其他Quartz和UIKit绘图请求一样,图层声明很快变得混乱:难以跟随,难以阅读,难以维护。 请参考示例6-3,其中介绍了在图6-11中创建最终DogCow的代码。 传递给PushLayerDraw()的块确保在绘制之前设置的阴影应用于整个组。

示例6-3使用块绘制透明层

1
2
3
4
5
6
SetShadow(shadowColor, CGSizeMake(4, 4), 4);
PushLayerDraw(^{
[udder fill:pinkColor];
[interior fill:whiteColor];
[baseMoof fill:blackColor];
});

清单6-4给出了PushLayerDraw()函数。 它在透明层中执行绘图操作块。 这种方法使您能够在易于使用的块中组合图形,确保基于图层的渲染。

清单6-4使用透明层绘图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef void (^DrawingStateBlock)();

void PushLayerDraw(DrawingStateBlock block)
{
if (!block) return; // Nothing to do

CGContextRef context =UIGraphicsGetCurrentContext();
if (context == NULL)
{
NSLog(@"Error: No context to draw to");
return;
}

CGContextBeginTransparencyLayer(context, NULL);
block();
CGContextEndTransparencyLayer(context);
}

透明层的优点是显而易见的:它们使您能够将绘图操作视为组。 缺点是,由于(需要)额外的绘图缓冲区,它是非常耗费内存的。 (可)通过在使用图层之前剪切上下文来缓解这种情况。 如果你知道你的组将只绘制你的上下文的一部分,在开始图层之前添加剪切。 这迫使图层仅绘制到裁剪区域,从而减少缓冲区大小和相关的内存开销。

但要小心,使用阴影的时候。 向剪裁区域添加阴影补偿,因为透明层结束时将立即绘制阴影。 作为经验法则,您想要允许阴影大小加上模糊。 因此,对于偏移为(2,4)和模糊为4的阴影,至少应向剪裁区域添加(6,8)点。

状态块

无论何时使用临时剪辑或任何其他特定于上下文的状态,您都可以通过使用基于块的方法来简化工作,如清单6-5所示。 与清单6-4类似,此PushDraw()函数在保存和恢复上下文状态的调用之间执行块。

清单6-5使用状态块绘图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void PushDraw(DrawingStateBlock block)
{
if (!block) return; // Nothing to do

CGContextRef context = UIGraphicsGetCurrentContext();
if (context == NULL)
{
NSLog(@"Error: No context to draw to");
return;
}

CGContextSaveGState(context);
block();
CGContextRestoreGState(context);
}

示例6-4使用清单6-4和6-5的函数显示用于创建图6-11中最终图像的完整序列。 它执行上下文剪切,设置上下文阴影,并将所有三个Bezier路径做为一个组绘制。 在该块执行之后,上下文完全返回到其预绘制条件。 剪切或阴影状态改变都不会超出此示例。

示例6-4使用状态和透明度块剪切

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CGRect clipRect = CGRectInset(destinationRect, -8, -8);
PushDraw(^{
// Clip path to bounds union with shadow allowance
// to improve drawing performance
[[UIBezierPath bezierPathWithRect:clipRect] addClip];

// Set shadow
SetShadow(shadowColor, CGSizeMake(4, 4), 4);

// Draw as group
PushLayerDraw(^{
[udder fill:pinkColor];
[interior fill:whiteColor];
[baseMoof fill:blackColor];
});
});

本文翻译自《iOS Drawing Practical UIKit Solutions》作者:Erica Sadun,翻译:Cheng Dong。如果觉得本书不错请购买支持正版:亚马逊购买传送门,本书所有源代码可在GitHub上下载。译者虽然力求做到信,达,雅,但是由于时间仓促加之译者水平十分有限,文中难免会出现不正确,不准确,词不达意,难于理解的地方,还望各位批评指正,共同进步,谢谢。转载请注明出处。