CodeV

5.9-绘制阴影

Quartz上下文支持阴影绘制作为可选的特定上下文功能。绘制在您指定的路径偏移处,阴影绘制模拟物理对象上光源的效果。图5-8演示了阴影在绘制到上下文时的外观。绘制阴影需要频繁的CPU计算,但是它们为你的界面增加了美丽的细节。

图5-8

图5-8 您可以添加阴影到上下文

与所有其他上下文状态更改一样,阴影会影响任何后续绘制操作。如果要在应用阴影之后重置状态,请确保执行保存并恢复上下文图形状态(GState)。设置阴影颜色为透明([UIColor clearColor].CGColor)以“禁用”应用程序阴影。

清单5-10用Objective-C颜色参数包装CGContextSetShadowWithColor()函数。您可以指定颜色,偏移量(大小)和模糊量。该函数更新上下文状态,应用这些值。关于阴影这里有几点需要知道的:

  • 每个阴影以相对于任何绘图操作的x和y偏移量添加。你可以通过CGSize指定它。
  • 浮点模糊参数指示硬(0)或软(大于0)的程度来绘制边缘。
  • 您可以通过调用CGContextSetShadow()来跳过颜色值。此函数默认为半透明黑色,具有0.33 alpha值。清单5 - 10使用这种颜色,如果调用SetShadow()则使用的是nil颜色参数。

清单5-10 Specifying a Context Shadow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void SetShadow(UIColor *color, CGSize size, CGFloat blur)
{
CGContextRef context = UIGraphicsGetCurrentContext();

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

if (color)
CGContextSetShadowWithColor(context,size, blur, color.CGColor);
else
CGContextSetShadow(context, size, blur);
}

表5-1显示了阴影参数对绘图的影响。

Application Result Result
当存储到上下文状态时,阴影适用于任何绘图操作。您通过填充(左)和描边(右)创建阴影。 图5.9.1 图5.9.2
增加模糊半径可以柔化模糊。左图像使用模糊半径4.将其与右侧设置为0的硬模糊比较。硬阴影创建斜角和挤压效果,赋予形状3D外观。 图5.9.3 图5.9.4
您可以应用任何阴影颜色。这里的示例是紫色的。左图像使用0.5的alpha级别,右侧为1.0的alpha。 图5.9.5 图5.9.6
调整偏移量来移动影子。左图像将先前图像的阴影从(4,4)切换到(-4,4)。阴影向左移动,但保持在形状下。正确的图像使用(-4,-4)。 阴影出现在形状的左侧和顶部。 图5.9.7 图5.9.8

(绘制)阴影的代价

无可争辩的事实,阴影给你的绘图计算增加了高负荷。虽然视觉上华丽,它们不一定是您要用于实时高性能界面元素的功能。

只要有可能,在开发过程中分析你的绘图操作,以了解他们的成本。这里有一个quick-and-dirty的方法来使用,你建立你的方法,以跟踪已用时间:

1
NSDate *start = [NSDate date];
// Perform drawing operations
NSLog(@"%f seconds", [[NSDate date] timeIntervalSinceDate:start]);

绘制内阴影

图5-9显示了通过用添加的内部阴影填充Bezier路径创建的图像。内部阴影,如名称所示,添加在路径的边界内绘制的阴影。取决于你的大脑如何处理它,在这一刻,阴影看起来好像外面的形状是投射的一个阴影,或者,如果你可以让你的头脑做一个触发器,形状表示一个浮雕的边缘。

图5-9

图5-9可以创建带有内阴影的路径。

图5-10显示了组合创建内部阴影的绘图操作。第一个操作是无光泽填充操作。第二个操作是建立阴影,掩盖在形状内部。

图5-10

图5-10左:原始路径。中间:反转路径。右:在路径边界内反转。

如清单5-9所示,构建阴影需要反转路径。允许上下文为该倒置形状绘制阴影,该阴影自然落在原始路径的剩余部分中。剪切上下文可确保在该路径之外不绘制任何内容。

清单5-11显示了绘制内部阴影的步骤。首先设置上下文阴影状态。此函数将此应用于GState堆栈中,确保绘图操作后状态恢复。它还将绘图区域剪切到作为参数传递的路径。这确保所有绘图都在路径边界内发生。

接下来,它设置绘图颜色。由于所有绘图都是在剪辑区域外完成的,因此理论上任何非透明颜色都可以奏效。然而,在实践中,在路径边界经历高曲率时,您将遇到轻微的限幅误差。这是一个已知的问题。使用阴影颜色避免了视觉不连续。

最后,此函数用该颜色填充路径的反转。由于剪辑,创建的唯一的图形是阴影,它显示在图5-10的右侧。

清单5-11 绘制内阴影

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void DrawInnerShadow(UIBezierPath *path,
UIColor *color, CGSize size, CGFloat blur)
{
CGContextRef context = UIGraphicsGetCurrentContext();
if (context == NULL)
{
NSLog(@"Error: No context to draw to");
return;
}

// Build shadow
CGContextSaveGState(context);
SetShadow(color,
CGSizeMake(size.width, size.height), blur);

// Clip to the original path
[path addClip];

// Fill the inverted path
[path.inverse fill:color];

CGContextRestoreGState(context);
}

清单5-12提供了另一个内部阴影。我称这种方法“PaintCode”解决方案,因为它最初的灵感来自PixelCut(http://pixelcut.com)从他们的PaintCode应用程序导出的代码。它比清单5-11中的解决方案更加繁琐,但它避免了边缘情况,它将反转路径填充颜色的小部分与高度变形曲线的绘制混合。它的工作原理是轻微地扭转反向路径,以削减一个微小的位,远离那边缘。这导致更清晰的阴影呈现。

清单5-12绘制(更好的)内部阴影

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
53
54
55
56
57
58
59
- (void) drawInnerShadow: (UIColor *) color
size: (CGSize) size blur: (CGFloat) radius
{
if (!color)
COMPLAIN_AND_BAIL(@"Color cannot be nil", nil);

CGContextRef context = UIGraphicsGetCurrentContext();

if (context == NULL)
COMPLAIN_AND_BAIL(@"No context to draw into", nil);

CGContextSaveGState(context);

// Originally inspired by the PaintCode guys
// http://paintcodeapp.com

// Establish initial offsets
CGFloat xOffset = size.width;
CGFloat yOffset = size.height;

// Adjust the border
CGRect borderRect =
CGRectInset(self.bounds, -radius, -radius);

borderRect =
CGRectOffset(borderRect, -xOffset, -yOffset);

CGRect unionRect =
CGRectUnion(borderRect, self.bounds);

borderRect = CGRectInset(unionRect, -1.0, -1.0);

// Tweak the size a tiny bit
xOffset += round(borderRect.size.width);

CGSize tweakSize = CGSizeMake(
xOffset + copysign(0.1, xOffset),
yOffset + copysign(0.1, yOffset));

// Set the shadow and clip

CGContextSetShadowWithColor(context, tweakSize, radius, color.CGColor);

[self addClip];

// Apply transform

CGAffineTransform transform =CGAffineTransformMakeTranslation(
-round(borderRect.size.width), 0);

UIBezierPath *negativePath = [self inverseInRect:borderRect];

[negativePath applyTransform:transform];

// Any color would do, use red for testing
[negativePath fill:color];

CGContextRestoreGState(context);
}

压花形状

在左下方应用一个黑色的内部阴影,在右上方应用一个浅色的内部阴影可以创建图5-11中所示的浮雕效果。这种效果使用柔和的模糊效果来创建平滑过渡:

1
2
3
4
5
6
DrawInnerShadow(path, [[UIColor whiteColor]
colorWithAlphaComponent:0.3f],
CGSizeMake(-4, 4), 4);
DrawInnerShadow(path, [[UIColor blackColor]
colorWithAlphaComponent:0.3f],
CGSizeMake(4, -4), 4);

图5-11

图5-11您可以将明暗的内部阴影结合起来,以“镶嵌”贝塞尔路径。

您可以将较柔和的内部阴影与尖锐的外部阴影结合起来,为您的形状创建一个斜角效果,如图5-12所示:

1
2
3
4
DrawInnerShadow(bunny, WHITE_LEVEL(0, 0.5),
CGSizeMake(-4, 4), 2);
DrawShadow(bunny, WHITE_LEVEL(0, 0.5),
CGSizeMake(2, -2), 0);

图5-12

图5-12通过组合内部阴影和外部阴影以及锐利的边缘来“斜切”(bevel)一条路径。


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