Layer对象的设置

周末第二弹, 继续Core Animation的基础内容复习.

Layer对象是Core Animation中的核心概念. layer管理着app中的可视化内容, 并且为你提供了改变这些内容样式的选择. iOS app是自动支持layer的, OS X的开发者们则需要手动去设置是否支持layer(默认是不支持的). 一旦开始支持layer, 那么你就需要理解如何配置和操作app中的各个layer以此来达到你想得到的效果.

App中支持Core Animation

在iOS app中, Core Animation是自动支持的, 并且每一个view都自带一个相对应的layer. 在OS X中, 想要使用Core Animation则需要手动去设置, 步骤如下:

  • 添加QuartzCore框架.
  • 想要NSView对象支持layer需要做以下几步:
    • 在nib文件中, 在View Effects inspector中勾选想要支持layer的view.

    • 如果是用代码创建的view, 调用view的setWantsLayer:方法, 传入YES来指定view会使用layer.

上面的任何一种方法都会创建一个基于layer的view. 之后系统会自动创建layer对象, 并且保持其更新.

改变View对应的layer对象

默认情况下, 基于layer的view已经创建了一个CALayer类的实例, 并且在绝大多数情况下, 你可能并不需要改变已经创建好的这个layer实例对象. 然而, Core Animation是提供了不同的layer类供你选择的, 每种layer类都有自己特殊的能力, 如果你用的到, 你可能就会改变默认情况下创建出来的layer类. 选择不同的layer类可以让你以一种简单的方式提高内容的展示效果或是支持特定类型的内容展示. 比如CATiledLayer类就能够以一种更有效的方式来展示大图.

使用UIView来改变layer所属的类

你可以通过重写view的layerClass方法, 返回一个不同的类对象以此来改变layer的类型. 绝大数多iOS中的view都创建了CALayer对象, 并用其来存储自身的内容. 对于你自己创建的view来说, 默认的选择就够了, 你也不需要去修改它. 但是在某些特殊的情景下, 你可能会发现其他的layer类会更加适合当前的场景. 比如, 在以下场景下, 你可能就会想改变默认的layer类:

  • 你的view使用Metal或是OpenGL ES来绘图, 这时你大概会使用CAMetalLayerCAEAGLLayer对象.
  • 有一个特定的layer类能够提供更好的表现.

改变一个view的layer所属的类方法很直接, 代码如下. 只需要重写layerClass方法, 并且返回要替换的类就可以了. 在view展示之前, 首先就会调用layerClass方法, 使用该方法返回的类来创建layer对象, 一旦创建, layer对象就不能改变.

+ (Class)layerClass {
   return [CAMetalLayer class];
}

注: 想要查看有哪些layer类并且如何使用它们, 请看Different Layer Classes Provide Specialized Behaviors

使用NSView来改变layer所属的类

通过重写NSView对象的makeBackingLayer方法也可以改变默认的layer对象所属的类. 在这个方法的实现中, 创建和返回你想要AppKit用来存储你view的layer对象. 当你想使用一个可定制的layer时可能就会重写这个方法.

不同的layer类提供不同的能力

Core Animation定义了许多标准的layer类. 每一个类都是有其特定的使用场景的. 具体列表如下:

类名 用法
CAEmitterLayer 用来实现一个基于Core Animation的粒子发射系统. 发射layer控制粒子的产生.
CAGradientLayer 用来绘制一个颜色梯度, 以此来填充layer的形状.
CAMetalLayer 用来设置一个可以绘图的纹理, 以此使用Metal来渲染layer上的内容.
CAEAGLLayer/CAOpenGLLayer 用来设置一个可以使用OpenGL的layer对象
CAReplicatorLayer 当你想能够自动复制子layer的时候使用该类.
CAScrollLayer 用来管理由多个子layer组成的大片可以滚动的区域.
CAShapeLayer 用来绘制贝塞尔曲线. 优点就是可以绘制基于路径的形状.
CATextLayer 用来渲染纯文本或者是属性文本.
CATiledLayer 用来管理大图.
CATransformLayer 用来渲染3D layer层级.
QCCompositionLayer 用来渲染Quartz Composer composition.(只针对OS X)

给layer填充内容

layer是数据对象, 其存储着app想要展现出来的内容. 一个layer的内容是由一个位图所构成, 位图中包含着你想要展示的视觉数据. 你可以通过以下三种方式来为位图提供内容:

  • 直接给layer对象的contents属性赋值一个image对象. (这种方式适用于layer的内容在之后不变, 或是很少改变的情况下使用)
  • 给layer对象指定一个代理对象, 然后让代理来绘制layer的内容. (这种方式适用于layer上的内容可能会周期性的改变或是内容又外部对象来提供, 比如view)
  • 自定义一个layer的子类, 然后重写它的绘图方法. (这种方式适用于当你必须要自定义layer的子类, 或者你想要改变layer自带的基本的绘制功能时使用)

只有当你是自己手动创建layer对象的时候你才需要关注给layer对象提供内容. 如果你的app只包含基于layer的view, 那你就不需要关注上面的三种能够给layer对象提供内容的方法, 因为系统会以最有效的方式为基于layer的view提供layer所需要的内容.

用Image作为layer的内容

因为一个layer恰恰就是一个管理位图的容器, 所以你可以直接把图片赋值到layer的contents属性上. 直接赋值图片到layer上是非常简单的, 这样你可以精确的指定任何图片. layer对象会直接使用你赋值过来的图片, 并且不会去创建图片的副本. 当你的app在多个不同的地方使用同一张图片的时候, 这样的处理方式可以有效的节约内存.

赋值到layer上的图片必须是CGImageRef类型的(OS X v10.6及之后的版本, 也可以赋值NSImage类型的图片). 当赋值图片的时候, 需要注意的就是图片的分辨率要和显示图片的设备的分辨率匹配. 如果是配备了Retina屏幕的设备, 你可能还需要调整图片的contentsScale属性. 关于分辨率更多的内容, 可以查看Working with High-Resolution Images.

使用代理为layer提供内容

如果layer上的内容是动态展示的, 即需要经常变化的, 这种情况下你就可以使用代理来为layer提供所需要的内容. 在要展示内容的时候, layer就会呼叫代理让其来展示内容:

  • 如果代理实现了displayLayer:方法, 那么就需要在该方法中创建位图, 并且把位图赋值到layer的contents属性上.
  • 如果代理实现了drawLayer:inContext:方法, Core Animation会创建位图和一个图形上下文, 用来绘制位图, 然后会调用代理的方法来填充位图. 代理方法所需要做的就是在图形上下文上面绘制内容.

代理对象必须实现下列方法中的任意一个, displayLayer:drawLayer:inContext:. 如果两个方法全部都实现了的话, layer也只会调用displayLayer:方法.

当你的app想载入或是创建它想要展示的内容时, 适合重写displayLayer:方法. 下面的代码就简单的实现了该方法. 其中代理使用了一个帮助对象来加载和展示它所需要的图片. 代理方法是基于内部状态来选择到底展示哪张图片, 这里的内部状态指的是一个自定义的属性displayYesImage.

- (void)displayLayer:(CALayer *)theLayer {
    // 检查内部状态
    if (self.displayYesImage) {
        // 展示相应的图片
        theLayer.contents = [someHelperObject loadStateYesImage];
    }
    else {
        // 展示其它的图片
        theLayer.contents = [someHelperObject loadStateNoImage];
    }
}

如果你还没有预先渲染好的图片或是一个帮助对象来帮你创建位图, 你的代理可以动态的使用drawLayer:inContext:方法绘制内容. 看不懂就看下面的例子, 代理使用drawLayer:inContext:方法绘制了一条曲线.

- (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);
 
    // 释放路径
    CFRelease(thePath);
}

对于拥有自定义的内容且基于layer的view来说, 你就应该重写view的方法来进行绘制. 基于layer的view会自动成为其代理, 并且实现所需要的代理方法, 你不应该改变这个配置. 相反, 你应该实现你自己view的drawRect:方法来绘制你的内容.

通过子类给layer提供内容

如果你实现了一个自定义的layer类, 那么你就可以重写绘图的方法. 对于一个layer对象来说, 为自己创建自定义的内容是挺不常见的, 但是确实可以这样做. 比如, CATiledLayer类可以通过将大图分解成小部分然后再进行管理和渲染. 因为只有layer知道何时渲染哪个部分, 所以layer直接管理着绘图行为.

当使用子类的时候, 可以使用下列方法来给layer绘制内容:

  • 重写layer的display方法, 用它直接设置layer的contents属性.
  • 重写layer的drawInContext:方法, 用它直接在上下文中绘制内容.

你要重写哪个方法取决于你想如何控制绘制的过程. display方法是更新layer上内容的主要入口, 所以重写该方法意味着你可以对绘制过程进行全权掌控. 并且重写该方法也意味着你需要创建CGImageRef并且将其赋值给contents属性. 如果你只是想绘制, 或是只是想让你的layer管理绘制操作, 你可以重写drawInContext:方法.

调整你提供的内容

当把图片赋值给layer的contents属性时, layer的contentsGravity属性会决定图片如何操作以适应当前的bounds. 默认情况下, 如果图片比现在的bounds大或者小, layer对象会相应的缩放图片. 如果layerbounds的宽高比和图片的宽高比不同, 图片可能就会扭曲. 所以你必须使用contentsGravity属性来确保你的内容能以最佳的方式展现.

能赋值给contentsGravity属性的值分为两类:

  • 基于位置的重力常量允许你在没有缩放图片的情况下可以把图片摆放在layer所在的矩形区域里的特定边或角上.
  • 基于尺度的重力常量允许你可以拉伸图片, 并且可以选择维持图片的宽高比或不维持.

下图展示了基于位置的重力设置是如何影响图片的. 除了kCAGravityCenter常量, 剩下的都可以指定图片到任意边角. 而kCAGravityCenter会将图片相对于layer居中. 这些选择都不会改变图片的大小, 所以图片永远都是按照它的原始尺寸进行渲染. 如果图片大于layerbounds的大小, 那么多出来的部分就裁剪掉了, 如果小的话, 就覆盖不完layer, 如果layer存在背景色的话, 就会看见.

下图展示了基于尺度的重力设置是如何影响图片的. 如果图片和layer所在的矩形区域不匹配, 所有的选项都缩放图片. 选项间的区别在于图片的原始宽高比有没有改变. 默认情况下, layer的contentsGravity属性是被设置成kCAGravityResize的, 这也是唯一一个不保存图片原始宽高比的一个选项.

调整layer的视觉样式和表现

layer对象有自己内置的视觉装饰, 比如边界和背景色, 你可以用来填充layer的内容. 因为这些视觉装饰不需要渲染, 所以在一些情景中可以将layer作为一个独立的实体来使用. 你需要做的就是给属性赋值, 然后layer就会处理必要的绘制, 包括动画. 关于layer其他的视觉装饰, 可以看Layer Style Property Animations.

layer有自己的背景色和边界

一个layer除了其自身上的图片外, 其还可以填充背景色和边框. 背景色在图片的下面, 边框则在图片的上面, 如下图. 如果layer包含子layer, 其也会显示在边框的下面. 因为背景色是显示在图片的下面, 所以背景色会对图片产生一些影响.

给layer设置背景色和边框的示例代码如下:

myLayer.backgroundColor = [NSColor greenColor].CGColor;
myLayer.borderColor = [NSColor blackColor].CGColor;
myLayer.borderWidth = 3.0;

如果你把layer的背景色设置成了一个不透明的颜色, 那么最好把layer的opaque属性值设置成YES. 这样做是因为当把layer显示到屏幕上时, 可以提高显示效果. 如果layer有一个非0的圆角半径, 那么你也可以不设置opaque属性.

layer支持圆角半径

通过添加圆角半径可以给layer创建一个圆角矩形的效果. 圆角半径是一个视觉装饰, 它可以掩盖layer所处的矩形区域的角, 如下图所示. 因为它涉及到透明掩盖, 所以圆角半径并不会影响显示在layer上的图片, 除非masksToBounds属性设置成了YSE. 然而, 圆角半径总是会影响layer的背景色和边框的绘制.

在layer上应用圆角半径的话, 需要指定cornerRadius属性的值. 你指定的这个半径的值是以点为单位计量的, 并且会被应用到这个layer的所有4个角上.

layer支持内置的阴影

CALayer类包含了一些属性来设置阴影效果. 阴影效果在一些情景下对你的app也是十分有必要的, 它也属于是视觉装饰. 通过layer, 你可以控制阴影的颜色, 相对于layer上内容的位置, 透明度以及形状.

不透明度的值默认是0, 这就是完全看不见阴影效果. 当不透明度的值变为非0时, Core Animation就会开始绘制阴影. 因为默认情况下, 阴影的位置是在layer下面的, 所以在你想看见阴影效果之前, 你需要改变阴影的偏移量. 并且需要牢记的一点是, 偏移量的值是基于layer自身所处的坐标系统内而言的. 并且在iOS和OS X中是不同的, 下图展示了下边缘和右边缘带阴影效果的样子. 在iOS中, 在y轴方向的偏移量需要指定正值; OS X中则是负值.

当给layer添加阴影时, 阴影就是layer内容的一部分, 但实际上它是延伸在layer所处的矩形以外的. 所以, 如果你设置了layer的masksToBounds属性, 阴影效果就会从边缘被裁剪掉. 而如果你的layer包含了任何透明的内容, 那么就会导致一种奇怪的效果, 即处在layer下面的阴影部分是可以看见的, 但是延伸在外面的却看不见了. 如果你又想看见阴影, 又想设置masksToBounds, 你可以使用两个layer而不是一个. 把掩盖加到包含内容的layer上, 然后再把这个layer镶嵌到第二个同样大小并且带有阴影效果的layer里.

至于阴影如何应用到layer上, 看一下Shadow Properties.

给layer添加一个自定义的属性

CAAnimationCALayer这两个类支持使用KVC来设置自定义的属性. 你可以通过KVC给layer添加数据, 然后通过特定的键来检索. 关于具体如何设置和获得自定义的属性, 请看Key-Value Coding Compliant Container Classes.

推荐阅读更多精彩内容