0%

CoreAnimation编程指南翻译(三):设置图层对象

图层对象是Core Animation中所有操作的核心, 负责应用内容的可视化呈现. 在iOS中, 所有视图都是图层支持视图, 但在OS X中, 需要开发者为视图手动开启图层支持功能. 要使用图层支持视图, 就必须了解如何设置图层来实现想要的效果.

为应用开启Core Animation支持

在iOS应用中, Core Animation是默认开启的, 所有视图都是图层支持视图. 在OS X中需要以下步骤为应用启动Core Animation支持:

  • 链接QuartzCore框架(iOS应用只在显式调用Core Animation接口时才需要链接此框架).
  • 通过以下步骤为一个或多个NSView对象开启图层支持:
    • 在nib文件中, 使用View Effects检查器开启图层支持.
    • 对代码创建的视图, 调用视图的setWantsLayer:方法并确保入参为YES.

以上两种方法都可以在OS X中创建图层支持视图. 另外在OS X中还可以创建图层托管视图, 需要开发者手动创建和管理底层的图层对象(iOS中不能创建图层托管视图). 更多关于创建图层托管视图的信息请参阅下文.

更改视图关联的图层对象

图层支持视图默认创建的CALayer对象已经可以满足大部分需求. 不过Core Animation还提供了丰富的图层类型, 每种图层类型都有特定的能力. 选择使用合适的图层类型可以很方便的提升性能或显示特定的内容. 比如CATiledLayer可以被用来高效率的展示大图片.

更改UIView使用的图层类型

可以通过重写UIView类的layerClass方法来返回一个指定的图层类型. 默认情况下, iOS中的视图会创建一个CALayer类型的图层对象, 并且使用这个图层对象作为内容的后备存储(backing store). 但在下面的情况下, 可以选择更合适的图层类:

  • 视图需要使用Metal或OpenGL ES绘制内容, 此时应该使用CAMetalLayerCAEAGLLayer对象,
  • 有特定的图层类型可以提供更好的性能.
  • 需要使用Core Animation封装好的图层类, 比如CAEmitterLayerCAReplicatorLayer.

如Listing 2-1所示, 改变一个视图的图层类型十分简单. 视图在展示之前, 会先调用layerClass方法, 然后根据其返回的图层类型创建一个图层对象. 视图的图层对象一旦被创建, 就无法再被更改.

  • Listing 2-1 更改UIView的图层类型
1
2
3
4
+ (Class) layerClass {
return [CAMetalLayer class];
}

更改NSView使用的图层类型

可以通过重写NSView类的makeBackingLayer方法来返回一个指定图层类型. 在该方法的实现中需要创建并返回一个图层对象.

在OS X中使用图层托管视图

图层托管视图是一个NSView对象, 由开发者创建管理其底层的图层对象.

当调用setLayer:方法并传入一个图层对象时, AppKit便不再对图层对象进行管理. 正常情况下, AppKit会负责更新图层对象, 但在图层托管时, 大部分属性都不在由AppKit维护.

Listing 2-2 展示了如何创建图层托管视图. 此外, 除了设置图层对象之外, 别忘了还需要调用setWantsLayer:开启视图的图层支持.

  • Listing 2-2 创建图层托管视图
1
2
3
4
5
6
7
8
// Create myView...

[myView setWantsLayer:YES];
CATiledLayer* hostedLayer = [CATiledLayer layer];
[myView setLayer:hostedLayer];

// Add myView to a view hierarchy.

如果选择使用图层托管, 则必须设置contentsScale属性, 并在合适的时机提供高分辨率的内容. 更多关于高分辨率内容和内容缩放的信息参阅下文处理高分辨率图像.

不同图层类的使用场景

Core Animation中定义了许多不同的图层类, 每个图层类都有其适用场景. CALayer是所有图层类的父类, 定义了所有图层类都必须支持的行为. Table 2-1中介绍了不同图层类及其用途.

  • Table 2-2 CALayer的子类及用途
Class 使用
CAEmitterLayer Used to implement a Core Animation–based particle emitter system. The emitter layer object controls the generation of the particles and their origin.
CAGradientLayer Used to draw a color gradient that fills the shape of the layer (within the bounds of any rounded corners).
CAMetalLayer Used to set up and vend drawable textures for rendering layer content using Metal.
CAEAGLLayer/CAOpenGLLayer Used to set up the backing store and context for rendering layer content using OpenGL ES (iOS) or OpenGL (OS X).
CAReplicatorLayer Used when you want to make copies of one or more sublayers automatically. The replicator makes the copies for you and uses the properties you specify to alter the appearance or attributes of the copies.
CAScrollLayer Used to manage a large scrollable area composed of multiple sublayers.
CAShapeLayer Used to draw a cubic Bezier spline. Shape layers are advantageous for drawing path-based shapes because they always result in a crisp path, as opposed to a path you draw into a layer’s backing store, which would not look as good when scaled. However, the crisp results do involve rendering the shape on the main thread and caching the results.
CATextLayer Used to render a plain or attributed string of text.
CATiledLayer Used to manage a large image that can be divided into smaller tiles and rendered individually with support for zooming in and out of the content.
CATransformLayer Used to render a true 3D layer hierarchy, rather than the flattened layer hierarchy implemented by other layer classes.
QCCompositionLayer Used to render a Quartz Composer composition. (OS X only).

提供图层内容

图层是负责管理应用内容的数据对象. 图层的内容可以理解为待展示数据的可视化位图. 有三种方式可以为这个位图提供内容:

  • 将image对象直接赋值给图层的contents属性(适用于图层内容不会被频繁更改的情况).
  • 设置图层delegate, 并由delegate负责绘制图层内容(适用于图层内容会周期性变化, 并且需要由外部对象提供图层内容的情况. 这里的外部对象通常是一个视图).
  • 定义一个图层子类, 通过重写其绘制方法来提供内容(适用于必须自定义图层修改其基本绘图行为的情况).

只有创建独立图层时才涉及到为图层提供内容. 如果应用只包含图层支持视图, 则不必用到上面的方法. 图层支持视图会以最高效的方式自动为其关联的图层提供内容。

使用image对象提供图层内容

由于图层可以理解为一个管理位图的容器, 所以可以直接将一个图片赋值给图层的contents属性, 将其显示在屏幕上. 图层会直接使用提供的图片, 而不会创建其拷贝. 尤其是需要在多处展示同一张图片时, 这样做可以节约内存.

赋值给图层使用的图片必须是一个CGImageRef类型(OS X 10.6之后, 也可以是NSImage类型). 提供的图片的分辨率必须与设备的分辨率相匹配. 对Retina设备来说, 可能还会需要调整图片的contentsScale属性. 更多关于高分辨率内容和内容缩放的信息参阅下文处理高分辨率图像.

使用Delegate提供图层内容

如果图层内容需要动态变化, 可以使用delegate对象来提供和更新内容. 在实际展示之前, 图片会调用delegate的方法获取内容:

  • 如果delegate实现了displayLayer:方法, 则由该方法负责创建位图并赋值给contents属性.
  • 如果delegate实现了drawLayer:inContext:方法, Core Animation会创建一个位图, 以及一个图形上下文用于在位图中绘制内容, 然后调用该方法在图形上下文中绘制内容.

delegate对象必须实现上面两种方法中的其中一种. 如果同时实现了两种方法, 则只会调用displayLayer:方法.

重写displayLayer:方法适用于由应用负责来加载或创建位图的情况. Listing 2-3是displayLayer:方法的一个简单实现. 示例代码中, delegate通过一个helper对象来加载图片, 并且根据一个内部状态值判断需要加载哪张图片.

  • Listing 2-3 直接设置图层内容
1
2
3
4
5
6
7
8
9
10
11
12
- (void)displayLayer:(CALayer *)theLayer {
// Check the value of some state property
if (self.displayYesImage) {
// Display the Yes image
theLayer.contents = [someHelperObject loadStateYesImage];
}
else {
// Display the No image
theLayer.contents = [someHelperObject loadStateNoImage];
}
}

如果不需要对图片进行预渲染, 或者不需要一个helper对象, 也可以通过drawLayer:inContext:方法动态绘制内容. Listing 2-4 是drawLayer:inContext:的一个简单实现. 示例代码中使用固定宽度和当前颜色绘制了一个简单的曲线路径.

  • Listing 2-4 绘制图层内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)drawLayer:(CALayer *)theLayer inContext:(CGContextRef)theContext {
CGMutablePathRef thePath = CGPathCreateMutable();

CGPathMoveToPoint(thePath,NULL,15.0f,15.f);
CGPathAddCurveToPoint(thePath,
NULL,
15.f,250.0f,
295.0f,250.0f,
295.0f,15.0f);

CGContextBeginPath(theContext);
CGContextAddPath(theContext, thePath);

CGContextSetLineWidth(theContext, 5);
CGContextStrokePath(theContext);

// Release the path
CFRelease(thePath);
}

对于使用自定义内容的图层支持视图, 可以重写视图的drawRect:方法绘制内容, 视图会自动成为其图层的delegate, 并实现所需的绘制方法.

在OS X 10.8及更高版本中, 通过重写视图的wantsUpdateLayerupdateLayer方法提供位图. 在wantsUpdateLayer返回YES时, NSView会遵循备用渲染路径. updateLayer方法的实现中必须将一个位图赋值给图层的contents属性.

通过子类提供图层内容

使用自定义图层类型时, 可以通过重写图层的绘制方法来进行内容绘制. 虽然这种由图层对象自身生成内容的情况并不多见, 但是图层确实能直接管理内容的绘制. 比如CATiledLayer类通过将一张大图分解为多个小图来进行管理和独立渲染. 这是因为只有图层才知道在指定的时刻将要渲染哪些独立的小图.

当使用图层子类时, 可以选择使用下面的方法之一进行内容绘制:

  • 重写图层的display方法, 并在方法中设置图层的contents属性.
  • 重写图层的drawInContext:方法, 在方法提供的图形上下文中进行绘制.

具体选择哪种方法取决于对绘制过程的控制程度. display方法是图层绘制的入口点, 重写该方法意味着将由开发者完成整个的绘制过程, 也就是说由开发者负责创建CGImageRef对象, 并赋值给contents属性. 如果只需要绘制内容(或者让图层管理绘制操作), 可以重写drawInContext:方法, 这样图层会负责创建后备存储(backing store).

调整图层内容

当把一个位图赋值给图层的contents属性时, 图层的contentsGravity属性决定了如何操作图片以适应图层的内容边界. 默认情况下, 如果图片比当前边界更大或者更小, 图层会缩放图片来适应当前边界. 所以如果图层的宽高比与图片宽高比不一致, 可能会导致图片失真. 此时可以使用contentsGravity属性进行调整.

contentsGravity属性可用的常量参数可以分为两类:

  • 基于位置的常量参数可以将图片固定在指定的边角, 图片不会失真.
  • 基于缩放的常量参数可以对图片进行拉伸, 使用其中的一些参数可能导致图片的宽高比可能会发生变化.

Figure 2-1 展示了设置基于位置的常量参数对图片的影响. 除了kCAGravityCenter常量会使图片居中之外, 其他常量都会将图片固定到某个边或角. 所有这些常量参数都不会导致图片失真, 图片将始终以原始大小进行渲染. 如果图片尺寸大于边界尺寸, 部分图片可能会被裁减, 如果图片尺寸小于边界尺寸, 如果设置了图层的背景色, 那么图层的一部分将展示图层的背景色.

  • Figure 2-1 基于位置的常量参数

基于位置的常量参数

Figure 2-2 展示了设置基于缩放的常量参数对图片的影响. 如果图片尺寸与边界尺寸不一致, 所有这些常量参数都会缩放图片, 区别在于如果处理图片的宽高比. 默认情况下, 图层的contentsGravity属性被设置为kCAGravityResize, 只有这个常量参数会改变图片的原始宽高比.

  • Figure 2-2 基于缩放的常量参数

基于缩放的常量参数

处理高分辨率图像

图层并不知道底层设备的屏幕分辨率, 只是存储一个指向位图的指针, 并以最佳的形式展示位图. 设置contents时, 还必须同时设置contentsScale属性, 告知Core Animation所使用图片的分辨率. contentsScale属性默认值为1.0, 适用于非Retina屏幕. 要在Retina屏幕上显示, 需要将contentsScale属性设置为2.0.

只有手动设置contents时才需要同时设置contentsScale. UIKit和AppKit中的图层支持视图会自动根据屏幕分辨率和视图内容自动设置图片的缩放比例.

调整图层的视觉样式和外观

图层对象还内置了一些视觉装饰属性, 比如边框和背景颜色, 用于对图层内容的补充. 这些视觉装饰只需要设置图层的对应属性, 然后由Core Animation完成必要的渲染或动画. 更多关于视觉装饰相关的信息, 参阅图层样式动画.

图层的背景色和边框

除了基于位图内容之外, 图层还可以显示背景色和边框. 如Figure 2-3所示, 背景色在图层图像内容的下一层进行渲染, 边框则在图层图像内容的上一层进行渲染. 如果图层包含子图层, 子图层也显示在父图层边框的下面. 由于背景色渲染在图像内容之下, 所以背景色会穿透图像的透明部分.

  • Figure 2-3 图层的背景色和边框

图层的背景色和边框

Listing 2-5 展示了设置背景色和边框的代码. 这些属性都是可动画的.

  • Listing 2-5 设置图层的背景色和边框
1
2
3
4
myLayer.backgroundColor = [NSColor greenColor].CGColor;
myLayer.borderColor = [NSColor blackColor].CGColor;
myLayer.borderWidth = 3.0;

注意: 图层的背景色支持任意类型的颜色, 包括具有透明度的颜色或是模式图像(pattern image). 当使用模式图像时, Core Graphics会使用标准坐标系渲染图像. 标准坐标系与iOS默认坐标系不同, 因此, iOS上渲染的图像默认会上下颠倒, 除非翻转坐标系.

如果给图层设置了一个不透明的背景色, 可以考虑将图层的opaque属性设置为YES. 这样在屏幕进行图层合成时可以提升性能, 并且无需图层的后备存储来管理alpha通道. 但是当图层的圆角半径不为0时, 就不能将图片标记为不透明.

图层的圆角半径

可以设置图层的圆角半径来实现圆角矩形的效果. 圆角半径是图层视觉装饰属性之一, 可以遮挡图层边界矩形的部分角, 如Figure 2-4所示. 由于使用了透明蒙版, 所以圆角半径不会影响图层的contents属性中设置的图像, 除非图层的masksToBounds被设置为YES. 但是圆角半径始终会影响图层的背景色和边框.

要设置图层的圆角半径, 需要设置图层的cornerRadius属性. 半径值以点为单位, 并且同时应用到图层的四个角.

  • Figure 2-4 图层的圆角半径

图层的圆角半径

图层的阴影

CALayer类包含了配置图层阴影效果的属性, 可以分别设置阴影的颜色, 相对位置, 不透明度以及形状. 阴影看起来好像浮在图层内容之上, 从而增加了图层的深度. 图层阴影也是一种十分有用的视觉装饰属性.

阴影默认的不透明度为0, 即隐藏阴影. 如果阴影的不透明度被设置为非0值, Core Animation就会绘制阴影. 阴影处于图层内容的下一层, 所以可能还需要更改阴影的偏移量才能看到阴影. 需要注意的是, 阴影的偏移量设置使用的是设备的坐标系, 这在iOS 和OS X上有所区别. Figure 2-5展示了带阴影的图层, 阴影向下延展到图层右侧. 在iOS中, 这需要设置y轴方向为正值, 在OS X中, 这需要设置y轴方向为负值.

  • Figure 2-4 图层的阴影

图层的阴影

当给图层添加阴影时, 阴影实际上是图层内容的一部分, 并且会延展到图层边界之外. 所以如果设置了masksToBounds属性为YES, 那么阴影也会被裁减. 如果图层包含了透明内容, 图层正下方的阴影仍然可见, 但超出图层的阴影不可见. 如果既想使用阴影, 也想使用蒙版, 可以考虑使用两个图层进行嵌套, 将蒙版应用于显示内容的图层, 然后将这个图层嵌入到另一个同样大小并且设置了阴影的图层之中.

更多设置图层阴影的示例, 请参阅阴影属性.

为图层添加视觉效果(OS X)

在iOS应用中, 可以直接在图层内容上使用Core Image滤镜. 可以通过滤镜实现图像模糊, 锐化, 颜色修改以及更多其他操作. 由于滤镜直接作用于底层硬件, 所以可以十分迅速和平滑的进行渲染.

注意: 在iOS中不能给图层对象添加滤镜.

给定一个图层, 滤镜可以同时应用到图层的前景内容和背景内容. 前景内容包含图层本身的所有内容, contents图像, 背景色, 边框以及子图层. 背景内容是直接位于图层内容之下的内容, 但不包括图层本身. 大多数图层的背景内容是其直接父图层, 背景内容可能会被前景内容部分或完全遮挡. 比如想要将用户注意力聚焦在前景内容时, 可以对背景内容使用模糊滤镜.

可以将CIFilter对象赋值给图层的下列属性:

  • filters属性包含一个滤镜数组, 仅影响图层的前景内容.
  • backgroundFilters属性包含一个滤镜数组, 仅影响图层的背景内容.
  • compositingFilter属性指定前景内容和背景内容如何合成在一起.

在将滤镜添加到图层之前, 最好先对滤镜的参数进行配置. 因为一旦添加到图层后, 就不能再修改CIFilter对象, 但是可以使用setValue:forKeyPath:方法修改滤镜的值.

Listing 2-6 展示了如何创建添加一个收缩失真滤镜(凹面镜)并添加到图层上. 注意并不需要给滤镜指定图像, 滤镜会自动使用图层的内容图像.

  • Listing 2-6 给图层添加滤镜
1
2
3
4
5
6
7
CIFilter* aFilter = [CIFilter filterWithName:@"CIPinchDistortion"];
[aFilter setValue:[NSNumber numberWithFloat:500.0] forKey:@"inputRadius"];
[aFilter setValue:[NSNumber numberWithFloat:1.25] forKey:@"inputScale"];
[aFilter setValue:[CIVector vectorWithX:250.0 Y:150.0] forKey:@"inputCenter"];

myLayer.filters = [NSArray arrayWithObject:aFilter];

更多关于滤镜的信息, 请参阅Core Image Filter Reference.

图层重绘策略对性能的影响(OS X)

在OS X中, 图层支持视图可以使用不同的重绘策略来更新内容. 由于AppKit和Core Animation中的绘图模型存在差异, 这些重绘策略可以更方便的将旧代码迁徙到Core Animation. 可以为每个视图配置不同的重绘策略, 以达到最佳性能.

可以使用视图的layerContentsRedrawPolicy方法获得当前视图关联图层的重绘策略, 可以使用setLayerContentsRedrawPolicy:设置重绘策略. 为了兼容传统绘图模型, AppKit默认将重绘策略设置为NSViewLayerContentsRedrawDuringViewResize. 可以根据需要选择使用Table 2-2中的重绘策略.

  • Table 2-2 OS X支持的重绘策略
Policy Usage
NSViewLayerContentsRedrawOnSetNeedsDisplay This is the recommended policy. With this policy, view geometry changes do not automatically cause the view to update its layer’s contents. Instead, the layer’s existing contents are stretched and manipulated to facilitate the geometry changes. To force the view to redraw itself and update the layer’s contents, you must explicitly call the view’s setNeedsDisplay: method. This policy most closely represents the standard behavior for Core Animation layers. However, it is not the default policy and must be set explicitly.
NSViewLayerContentsRedrawDuringViewResize This is the default redraw policy. This policy maintains maximum compatibility with traditional AppKit drawing by recaching the layer’s contents whenever the view’s geometry changes. This behavior results in the view’s drawRect: method being called multiple times on your app’s main thread during the resize operation.
NSViewLayerContentsRedrawBeforeViewResize With this policy, AppKit draws the layer at its final size prior to any resize operations and caches that bitmap. The resize operation uses the cached bitmap as the starting image, scaling it to fit the old bounds rectangle. It then animates the bitmap to its final size. This behavior can cause the view’s contents to appear stretched or distorted at the beginning of an animation and is better in situations where the initial appearance is not important or not noticeable.
NSViewLayerContentsRedrawNever With this policy, AppKit does not update the layer at all, even when you call the setNeedsDisplay: method. This policy is most appropriate for views whose contents never change and where the size of the view changes infrequently if at all. For example, you might use this for views that display fixed-size content or background elements.

视图的重绘策略在OS X 10.6中引入, 现在建议给视图设置合适的重绘策略, 而不是另外创建独立图层来展示不需要重绘的部分.

图层的自定义属性

CAAnimationCALayer支持通过KVC扩充自定义属性. 可以通过健值对的形式给图层添加自定义数据. 甚至可以将操作与自定义属性关联, 在属性更改时, 就能自动执行动画.

更多关于自定义属性的信息, 请参阅Key-Value Coding Compliant Container Classes.
更多关于给图层添加操作的信息, 请参阅Changing a Layer’s Default Behavior.

打印图层内容

打印期间, 图层会重绘内容以适应打印环境. Core Animation在屏幕渲染时依赖于缓存的位图, 但在打印时会重绘内容. 如果图层支持视图使用了drawRect:方法提供内容, Core Animation会再次调用该方法生成要打印的内容.