layer对象是你进行CoreAnimation操作的核心. layer对象管理着界面中显示的内容, 并提供机会让你去修改内容的风格和外观. 虽然iOS中的view默认有一个layer支持, 但是开发OS X系统上的APP时需要自己显示启用layer来使用它. 当你启用layer时,你需要了解如何去设置layer, 以便让layer发挥应有的功能.
让你APP使用CoreAnimation
iOS中的App总是开启了layer支持, 所以CoreAnimation也是开启的. OS X中的App需要自己通过下面操作来显示开启:
- 链接到QuartzCore框架(iOS App要链接这个框架需要显示的调用CoreAnimation中的API)
- 如果要让layer支持NSView, 需要完成如下操作:
- 在nib文件中, 通过View Effects检测器来开启layer支持. 在该检测器中会显示checkbox来表示是否选择view或者它的subview. 强烈推荐开启window中content view的layer支持
- 如果通过代码创建的view, 你可以调用
setWantsLayer:
(传YES)方法来开启layer支持.
使用前面方法开启layer支持, 会创建layer-backed视图, 对于layer-backed view系统会负责创建底层layer, 并同步view信息. 在OS X中, 你还可以创建layer-hosting view, 这就意味着APP需要自己负责创建底层支持的layer, 并同步view的信心, 另外在iOS中是无法创建layer-hosting view的, 因为iOS中的view都是layer-backed.
修改和view关联的layer对象
layer-backed view会默认创建一个CALayer
实例, 而且大多数情况下, 都是使用这个类来创建. 但是CoreAnimation提供了其他类型的Layer, 在某些情况下, 你可以发现某些类型的layer还是蛮有用的, 比如, 当你的view需要展示一张超大的图片是, 你可以使用CATiledLayer
, 这样可以提高性能.
修改UIView使用的layer
你可以通过重写view中的layerClass
类方法来修改view背后layer的类型. iOS中大部分view背后的layer都是CALayer
类型. 大多数情况下, 使用默认类型的layer是好的, 但是某些情况下, 你需要使用其他类型的layer, 比如下面列举的几种情况:
- 如果你使用Metal或OpenGL ES技术来绘制view中的内容, 你需要使用CAMetalLayer和CAEAGLLayer类型的layer对象.
- 有几种layer是用来改善性能的, 比如
CATiledLayer
- 你需要使用某种layer的特性, 比如粒子发射器和复制器.
修改view的layer类型很简单, 如代码2-1所示. 你只需要重写layerClass
类方法, 在方法中返回一个你想要的layer类型即可. 该方法会在view显示之前调用, 来获取layer的类型, 然后通过该类创建一个新的layer对象. 当layer创建后, view的layer对象就不能被改变了.
代码清单2-1 设置iOS中view的layer
+ (Class) layerClass {
return [CAMetalLayer class];
}
在OS X中通过layer-hosting技术来修改layer对象
如果你的NSView
对象是一个layer-hosting view. 那么, 需要App自己管理view底层的layer对象. 在你需要控制与view关联的layer对象类型的情况下, 你可以使用hosting layer. 比如, 你创建一个layer-hosting视图, 以便可以分配除默认CALayer
类型外的layer. 你还可能在为了用单个view管理一个独立的layer树的情况下使用layer-hosting技术.
当你调用view的setLayer:
方法设置一个layer时, AppKit就不会干涉这个layer了. 正常情况下, AppKit会更新view的layer对象, 但是在layer-hosting情况下, AppKit不会更新layer.
为了创建一个layer-hosting视图, 你需要在view显示之创建一个layer, 并将layer和view关联起来, 如代码2-2所示. 另外, 为了设置layer对象, 你必须调用setWantsLayer:
方法, 让view知道要开启layer支持了.
代码清单2-2 创建layer-hosting视图
// Create myView...
[myView setWantsLayer:YES];
CATiledLayer* hostedLayer = [CATiledLayer layer];
[myView setLayer:hostedLayer];
// Add myView to a view hierarchy.
如果你使用host layer时, 你需要给layer设置好contentScale
, 并在高分辨的屏幕上提供高分辨率的内容.
不同类型的layer拥有不同的特性
CoreAnimation定义了好几种layer类, 每个类都是为了不同需求而定义的. CALayer
是这些类的root类, 该类定义了多种layer类的共有的特性, 并支持layer的基本特性. 下面列举了CALayer
几个子类的特性, 和适用场景, 如表2-1
表2-1 CALayer
子类和使用场景
Class | 使用 |
---|---|
CAEmitterLayer | 用于使用CoreAnimation中粒子发射器系统. emitter layer负责创建粒子和控制这些粒子的origin |
CAGradientLayer | 用于绘制渐变颜色的layer |
CAMetalLayer | 用于使用Metal技术绘制文本内容的layer |
CAEAGLLayer/CAOpenGLLayer | 用于使用OpenGL ES(iOS)或OpenGL(OS X)技术绘制内容的layer |
CAReplicatorLayer | 如果你想自动复制sublayer. replicator为你创建副本,并提供属性供你修改外观和其他属性. |
CAScrollLayer | 用于管理大片可滚动的由大量sublayer组成的区域 |
CAShapeLayer | 用于绘制三次bezierpath, shape layer对于绘制基于路径的图形是有利的. |
CATextLayer | 用于渲染纯文本, 或者attributed string |
CATiledLayer | 用于显示和管理大型图片, 它会将大图切割成小图,然后逐个渲染, 而且支持缩放. 能提高性能 |
CATransformLayer | 用于渲染真正的3D图形 |
QCCompositionLayer | 用于渲染Quartz Composer中的合成内容(仅OS X) |
设置layer中的内容
layer是管理App内容的数据对象. layer的内容由一个bitmap组成, bitmap包含了你想显示的数据信息. 你可以通过下面三种方式来为layer提供bitmap:
- 直接将一个图像设置到layer的
contents
属性中. (这种方式适合当layer的内容不变, 或者很少改变情况) - 给layer设置一个delegate, 然后让delegate去绘制layer中内容. (这种方式适合layer的内容会周期性地改变并且layer的内容可以由外部对象提供, 比如view)
- 定义一个layer子类, 并重写layer中的绘制方法来自己提供layer的content. (这种方式适合你需要创建一个自定义的layer, 并且你想改变layer的底层绘制行为)
当你自己创建layer对象时, 你唯一需要操心的是为layer提供内容. 但是如果你的App只包含layer-backed视图时, 这个操心你也省了, 因为UIView会以最好的方式为view关联的layer提供内容.
使用一个image来作为layer的content
因为layer对象是一个管理bitmap图像的容器, 所以你可以直接将一个image对象赋值给layer的contents
属性. 这种操作比较简单, 也能让你直接将image显示在屏幕上, 而不需要UIImageView来显示. layer对象会直接使用这个image对象, 而不是创建一个副本image(普通内容话, 最终会创建内容image副本), 这样也可以节省内存, 因为App可能在多个地方使用同一个image.
你给layer赋值的image类型必须是CGImageRef
类型. (在 OS X v10.6及以后, 你也可以使用NSImage
来赋值). 在使用image最为layer的content时, 注意图像的分辨率要和设备屏幕分辨率相匹配. 如果设备屏幕时retina屏幕, 那么你需要调整image的contentsScale
属性.
使用一个delegate对象来为layer提供内容
如果layer中的内容会经常改变, 那么你可以使用一个delegate对象来提供并且更新layer中的内容. 在准备显示之前, layer会调用delegate中的方法来获取所需的内容:
- 如果你的delegate实现了
displayLayer:
方法, 该方法就可以layer创建一个bitmap, 然后将其赋值给layer的contents
属性. - 如果你的delegate实现了
drawLayer:inContext:
方法, CoreAnimation会创建一个bitmap绘图上下文, 你需要在该方法中将你想要的内容绘制到bitmap中.
layer的delegate必须实现上面两个方法中的一个, 如果两个都实现了的话, layer只会调用displayLayer:
方法.
重写displayLayer:
方法适用于App偏向创建或加载要显示的的内容的bitmap. 代码2-3是displayLayer:
方法的一个实现例子. 在该例中, delegate会使用一个工具类来帮忙加载并且显示想要的image. delegate根据内部的一个状态displayYesImage
来选择要展示的image.
代码清单2-3 直接设置layer的contents
- (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];
}
}
如果你没有渲染好的image图像, 也没有工具类帮助你创建一个bitmap, 那么delegate可以通过drawLayer:inContext:
自己绘制想要的内容. 代码2-4是一个drawLayer:inContext:
实现的一个例子. 在该例子中的delegate使用固定的线宽和描边颜色来绘制一条曲线.
代码清单2-4 绘制layer中的内容
- (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);
}
对于自定义内容的layer-backed视图来说, 需要重写view的方法来完成绘制. layer-backed视图会自动将自己设置为它的layer的delegate并实现了delegate方法, 而你需要做的, 就是实现drawRect:
方法, layer的配置你不能动.
在OS X v10.8及以后, 有一种自己绘制的替代方法, 就是重写view的wantsUpdateLayer
和updateLayer
方法. 在wantsUpdateLayer
中返回YES, 这回告诉NSView使用替代流程. 然后你再实现updateLayer
方法, 在该方法中, 你需要使用bitmap赋值给layer的contents
属性. 这是AppKit希望直接设置视图的layer内容的一种情况
通过子类来提供layer的内容
如果打算自定义layer, 那么你可以通过实现layer中的绘制方法来绘制任何你需要的内容. layer通常不会自己生产定制的内容, 但是layer可以管理要显示的内容. 像CATiledLayer
类就可以将一个大图分解许多小图(tile), 然后单独将这些小图渲染出来. 因为只有layer知道这些小图的渲染需要的信息, 所以layer直接参与管理绘制的过程.
如果你使用继承的方式, 那么你可以使用技术之一来绘制layer中的内容:
- 重写layer的
display
方法, 然后直接给属性contents
赋值. - 重写layer的
drawInContext:
方法, 使用该方法在绘图上下文中绘制内容.
根据需要控制layer绘制内容的程度来选择上面的两个方法. display
方法是layer更新内容的主入口, 所以你重写该方法可以完全控制layer的内容绘制. 另外, 你重写display
方法的话, 你要负责创建CGImageRef
对象并负责给contents
属性. 如果你只是向控制layer的内容的绘制操作, 你可以重写drawInContext:
, 这样可以让layer为你创建后备缓存.
对你提供的layer内容进行微调
当你给contents
属性赋值时, layer会根据属性contentsGravity
来控制content来适配layer的bounds的. 默认情况下, 如果图片大于或小于layer的bounds, layer会缩放图片来适配当前的bounds. 如果图片的宽高比和bounds的宽高比不一致的话, 这样会导致图片变形. 所以你可以使用contentsGravity
来控制contents在layer中显示适当.
contentsGravity
的值可以分为两类:
- 基于位置的gravity常量让你将图片对齐layer的bounds区域内的边或顶角, 不会进行缩放.
- 基于缩放的gravity常量让你可以将图片进行拉伸, 某些配置可以让拉伸按照宽高比来进行, 也有些配置不会.
图2-1展示了基于位置的gravity设置图片的影响. 除了kCAGravityCenter
常量外, 其它常量都是让图片紧靠bounds的四边或四角. kCAGravityCenter
常量让图片处于layer的中心位置, 图片不会拉伸, 如果图片的size大于layer的bounds, 那么图片超出bounds的部分将会被剪切; 如果图片的size小于bounds, 那么图片外的layer区域会显示背景颜色(如果layer设置了背景颜色).
图2-3展示了缩放gravity设置对layer内容(图片)的影响. 这些基于缩放的gravity常量会根据当前图片是否适配layer的bounds来缩放图片, 区别是-这些常量对宽高比的处理方式不一样, 有些保持原宽高比不变, 有些会改变宽高比. 属性contentsGravity
的默认值为kCAGravityResize
常量, 这个常量是唯一不会维持图片的宽高比不变的.
使用高分辨率图片
layer并不知道设备屏幕分辨率. layer只是保存了一个指向bitmap的指针然后将其用合适的方式将bitmap显示出来. 如果你给layer的contents
属性赋值了, 你必须通过layer的属性contentsScale
来告诉CoreAnimation关于image的分辨率的. layer的contentsScale
属性的默认值是1.0, 适用在普通屏幕上显示图像. 如果你的图片是retina版本的, 那么该属性的值应该设置为2.0.
当且仅当你直接给layer设置一个bitmap时, 你才改变contentsScale
的值. 像UIKit和AppKit中的layer-backed视图, 你是不需要关注contentsScale
的, 因为layer-backed视图会自动更加屏幕分辨率来给layer设置合适的scale. 比如, 在OS X中, 你将一个NSImage
对象赋值给layer的contents
属性, AppKit会检测image的分辨率, 然后使用合适的值来设置layer的contentsScale
.
在OS X中, 基于位置的gravity常量会影响赋值给layer的NSImage
选择表现的方式. 因为这些常量都不会对image进行缩放, CoreAnimation根据contentsScale
属性来选择合适的像素密度来展示image.
在OS X中, layer的delegate可以通过实现方法layer:shouldInheritContentsScale:fromWindow:
来响应scale因子的改变. 当window的分辨率改变(可能因为window的分辨率在普通和高倍之间切换)时, AppKit会调用这个方法, 在方法中, 如果delegate支持image的分辨率的变化, 你应该返回YES, 然后你还应该更新layer的内容以适配新的分辨率.
调整layer视觉风格和外观
layer对象内置了许多视觉上的修饰, 比如边界(border)/背景颜色, 通过修改这些来装饰layer中的主体内容. 因为这些视觉装饰部分由layer自己控制, 所以这些效果都是通过layer的属性来设置的, 你不需要你自己进行绘制, 包括部分动画. 关于如何使用这些视觉装饰来影响layer的外观的详细知识, 请看后续系列文章-补充中的Layer Style Property Animations
layer有自己的背景和边界
layer除了可以展示基于图像的内容外, 还可以显示背景和描边. layer中, 背景在主体内容后面, 但描边显示在主体内容前面, 如图2-3所示. 如果layer包含子layer, 它们也会出现在描边的下面. 因为背景是在内容的下面, 如果内容有透明部分, 将会透过内容看到背景.
图2-3 往layer中添加描边和背景
代码2-5展示如何设置layer的背景和描边, 下面涉及的属性都是可动画的.
代码清单2-5 设置layer的背景颜色和描边颜色
myLayer.backgroundColor = [NSColor greenColor].CGColor;
myLayer.borderColor = [NSColor blackColor].CGColor;
myLayer.borderWidth = 3.0;
注意:layer的背景可使用任何类型的颜色, 比如渐变, 图案等都可以, 在使用图案最为背景时, 记得CoreAnimation能使用硬件加速能很好的处理背景图案, 但是要记得CoreAnimation中的坐标系和iOS中默认坐标系不一样, 可能需要翻转坐标系.
如果你的layer的背景是透明的颜色, 考虑可以将layer的opaque属性设置为YES, 这样做免去layer内容和layer背后的图片进行合成, 也可以省去layer开启后备存储来处理alpha通道(alpha channel). 但是你layer有个非零的圆角,那么layer不能设置为opaque.
layer支持圆角
你可以给layer叫上圆角(corner radius)来创建圆角矩形. 一个圆角是通过在layer的bounds矩形的四个角上加一个一个圆形遮罩(mask), 让layer四角上的内容透过遮罩显示, 如图2-4所示. 因为涉及使用的是透明遮罩, 所以corner radius对layer中的contents
不会影响, 除非你将属性masksToBounds
设置为YES. 但一定会影响layer的背景和border的绘制.
layer提供
cornerRadius
属性让你来设置圆角, 该属性值为圆角半径, 单位是点.
layer的内置阴影
类CALayer
包含好几个属性用来设置layer的阴影效果. 阴影效果使的layer中的内容好像悬浮界面上其他内容之上, 让界面看起来又具有深度. 这种效果在某些场景比较有用. 通过layer, 你可以控制阴影的颜色, 便宜位置, 透明度, 和形状.
默认情况下, 阴影的opacity为0, 完全隐藏的, 所以你需要将印象的opacity设置为非零值. 另外, 阴影是直接位于内容的正下方, 如果想看到阴影, 你需要设置阴影的offset, 这样才能看到阴影. 你在设置阴影时, 需要记得这两点. 在设置阴影的offset时, iOS和OS X是有区别的, 因为坐标系不一样. 如图2-5, 同样的效果, 在iOS中y的值为正, 而在OSX中为负.
图2-5 设置layer的阴影
当你给layer加上阴影后, 阴影也属于内容的一部分, 但是阴影的区域实际上是超出layer的bounds区域的. 如果masksToBuounds
设置为YES的话, 阴影会被bounds裁剪. 如果你即想要阴影, 又想要bounds masking的话, 你可以使用两个相同内容的layer叠加在一起, 上面那个开启masksToBounds
, 下面那个设置阴影. 也可以将第一层添加到第二层中.
关于如何设置shadow的列子, 可以看补充中的Shadow Properties
使用过滤器(filters)向OS X视图中添加视觉效果
在OS X应用程序中, 你可以直接向layer中的content上应用一个Core Image过滤器. 这样你可以进行模糊或者锐化layer中的内容, 修改颜色, 扭曲内容等一些操作. 比如, 一个图片处理进程可能使用过滤器来无损地修改图像, 而视频编辑程序可以使用它来实现不同类型视频切换小姑. 并且由于过滤器会使用硬件加速, 所以渲染时快速且平滑的.
注意:你不能往iOS中的layer对象中添加过滤器
对于给定的layer, 你可以将过滤添加到layer的foreground content和background content中. foreground content包括layer本身所包含的所有内容, 包括属性contents
中的image, 背景颜色, border, 和sublayer中的内容. background content是直接在layer的下面, 实际上并不是layer本身的一部分. 大多数layer的background content是其直接super layer的内容, 这些内容部分或者全部地被遮住. 例如, 当你希望用户应该关注layer的foreground部分时, 你可以将模糊过滤器加入background content中.
你可以通过下面的一些属性来设置一个CIFilter过滤器:
- 属性
filters
包含一组过滤器, 加入的是layer的foreground content. - 属性
backgroundFilters
包含的一组过滤器, 加入是layer的background content中. - 属性
compositingFilter
中的过滤器定义如何将foreground content和background content组合在一起.
为了往layer中添加过滤, 第一步是创建一个CIFilter
对象, 之后配置该对象. CIFilter
类提供了好几个类方法用来配置CIFilter
对象, 比如filterWithName:
. 所以创建过滤器对象只是第一步, 好多过滤器需要许多参数来定义如何修改一个图像. 比如, 一个box blur过滤器与一个输入半径的参数, 定义了模糊的程度. 所以, 添加过滤器之前你总是要配置好这些参数. 而且这些过滤器的有一个共同的参数不需要你设置, 那就是输入的image, 这个参数由layer自己来设置.
所以在添加过滤器之前, 你需要配置好过滤器, 因为, 一旦过滤器添加到layer后, 你是不能通过CIFilter
提供的API来修改过滤器的. 但是, 你可以通过layer的setValue:forkeyPath:
方法来改变过滤器的值.
代码2-6展示了如何创建一个pinch distortion过滤器并添加到layer对象中. 该过滤可以通过向内捏合使中心点像素失真. 注意, 在这个示例中, 不需要为过滤器指定输入图像, 因为layer中的图像会被自动使用.
代码清单2-6 往layer中添加过滤器
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中view的重绘策略会影响性能
在OS X中,layer-backed视图支持几个不同的策略来确定何时更新底层layer的内容。因为原生AppKit绘图模型和CoreAnimation引入的模型之间存在差异,所以这些策略使得将旧代码迁移到CoreAnimation上来更加容易。您可以在逐个视图的基础上配置这些策略,以确保每个视图的最佳性能。
每个视图定义了一个layerContentsRedrawPolicy
方法,该方法返回视图layer的重绘策略。使用setLayerContentsRedrawPolicy:
方法设置策略。为了保持与传统绘图模型的兼容性,AppKit默认将重绘策略设置为NSViewLayerContentsRedrawDuringViewResize
。但是,可以将策略更改为表2-2中的任何值。请注意,推荐的重绘策略不是默认策略。
表2-2 layer的重绘策略
策略 | 使用 |
---|---|
NSViewLayerContentsRedrawOnSetNeedsDisplay | 推荐使用,具体见The Layer Redraw Policy for OS X Views Affects Performance |
NSViewLayerContentsRedrawDuringViewResize | 默认,见The Layer Redraw Policy for OS X Views Affects Performance |
NSViewLayerContentsRedrawBeforeViewResize | 见The Layer Redraw Policy for OS X Views Affects Performance |
NSViewLayerContentsRedrawNever | 见The Layer Redraw Policy for OS X Views Affects Performance |
给layer添加自定义属性
CAAnimation和CALayer这两个类扩展了KVC(key-value coding)方便支持自定义属性. 你使用这种特性往layer中添加数据并用你定义的key来获取该数据. 你也可以将一个动作和自定义属性关联起来, 以便在更改属性时执行相应的动画. 有关如何设置和获取自定义属性的知识, 请参考Key-Value Coding Compliant Container Classes, 有关如何向layer对象添加action的知识, 请参考Changing a Layer’s Default Behavior
打印layer-backed视图中的内容
在打印期间,layer根据需要重新绘制其内容以适应打印环境。虽然CoreAnimation通常依赖于缓存的位图,当渲染到屏幕上时,它会重画打印时的内容。特别地,如果layer-backed视图使用drawRect:
方法提供层内容,那么CoreAnimation在打印期间会再次调用drawRect:
来生成打印的layer内容。
网友评论