Core Animation的使用

关于Core Animation

Core Animation是IOS和OS X的图形渲染和动画的基础设施,你可以使用它来进行动画绘制视图和其他APP的可视元素。Core Animation为你完成大量动画帧的描绘工作。你所要做的是设置一些动画参数(例如开始和结束点)和启动Core Animation。Core Animation会完成剩下并把大量的实际绘制工作转移到图形硬件以加速渲染。这种自动的图形加速会形成高帧率和平滑的动画而且不会加重CPU和变慢APP。

如果你编写IOS APP,不管你知道不,你正在使用Core Animation。如果你编写OS X APP,你只需要进行少许操作就可以利用Core Animation。Core Animation位于APPKit和UIKit之下而且紧密集成到视图的Cocoa和Cocoa Touch工作流。当然,Core Animation会有接口去扩展已存在的视图动画功能并允许你更细粒度的控制APP动画。


Core Animation 基础

Core Animation提供一个通用的动画绘制视图和其他APP可视元素的系统。Core Animation不会替代你的APP视图。它和视图紧密集成并提供更好的性能和动画绘制内容。它通过缓存视图内容为能直接被图形硬件使用的位图来实现这些功能。在某些情况下,这种缓存行为可能需要你重新考虑怎样呈现和管理APP内容,但大多数情况下,在你使用Core Animation时,不用知道这些内容。除了缓存视图内容,Core Animation也定义一种方式来指定任意内容,集成这些内容到你的视图并和其他任何内容一起动画。

你使用Core Animation去动画改变APP的视图和可视对象。大多数变化是与修改可视对象相关。例如,你可能使用Core Animation来动画改变视图位置,大小或者透明度。当你进行这样的改变,Core Animation会在属性的当前值和你设置的新值之间进行动画。你通常不会使用Core Animation去每秒60次替换视图内容,例如,卡通。而是,你使用Core Animation来移动、渐入、渐出屏幕的视图内容,应用任意图形转换到视图,或者改变视图的其他可视属性。

图层提供基本的绘制和动画

图层对象是3D空间的2D表面,它是Core Animation的中心对象。图层像视图一样管理几何、内容和表面的可视属性的信息。图层不像视图会定义自己的外观。层仅仅管理位图的状态信息。位图可能是视图绘制或者设置图像的结果。由于这个原因,APP的主层被认为是模型对象,因为它们主要管理数据。这个概念很重要,因为这影响动画的行为。

基于图层的绘制模型

大多数图层不会做实际的绘制工作。而是,图层计算APP提供的内容并缓存成位图,这有时候也叫做备用存储。当你进行一系列图层属性改变,你所做的就是改变关联图层对象的状态信息。当一个改变触发动画,Core Animation传递图层的位图和状态信息到图形硬件,图形硬件会使用新的状态信息来渲染位图。在硬件处理位图会比软件更快。


因为它操作静态位图,基于图层的绘制与传统的基于视图绘制非常不同。基于视图绘制会经常调用视图的drawRect:方法来使用新的参数并重新绘制内容。使用这种方式是昂贵的,因为它在主线程使用CPU。Core Animation会尽量通过硬件操作缓存位图来实现相同或者相似的结果以避免这种昂贵操作。

虽然Core Animation尽可能使用缓存内容,你的APP必须提供初始内容并时常更新它。

基于图层的动画

图层的数据和状态信息是和屏幕内的图层内容的可视呈现解耦。这种解耦让Core Animation可以插入它们并动画改变旧状态到新状态。例如,改变层的位置属性会导致Core Animation移动层从当前位置到新的位置。相似改变其他属性也会造成适当的动画。


Core Animation会在硬件完成所有的帧绘制。你只需要指定动画的开始点和结束点并让Core Animation完成剩下的工作。你可以指定自定义时间信息和动画参数。而然,如果你不指定这些信息,Core Animation会提供默认值。

图层对象定义自己的几何

图层的其中一个工作就是管理自己内容的可视几何。不管图层是否已经被旋转、缩放、转换,可视几何封装了内容的边界信息。图层像视图一样有框架和边界矩形,你可以使用它来放置图层和图层的内容。图层也有视图没有的其他属性,例如锚点,它定义了操作的点。你指定图层的几何的方式也和视图不一样。

图层使用两种类型的座标系统

图层利用基于点和单位的座标系统来指定内容的放置。使用哪个座标系统依赖于传达的类型信息。当指定的值可以直接映射到屏幕座标或者值必须相对另一个图层,例如图层的点属性,基于点的座标系统会被使用。当值不应该绑定到屏幕座标,因为它接收其他一些值,单位座标会被使用。例如,图层的anchorPoint属性指定相对图层本身边界的点而且它能被改变。

通常使用点座标来指定图层的大小和位置,你使用图层的bounds和position属性来设置。bounds定义图层本身的座标系统和包含图层在屏幕的大小。position属性定义图层相对于父座标系统的位置。虽然层有frame属性,这个属性实际是从bounds和position属性的值衍生出来并且它很少使用。

图层的bounds和frame矩形的方向总是匹配默认底层平台的方向。


上面的例子显示position位于图层的中心点。这个属性是其中一个会被图层的anchorPoint属性改变的属性。

锚点是其中一个使用单位座标系统的属性。Core Animation使用单位座标系统代表哪些当图层的大小改变时,值也会改变的属性。你可以认为单位座标是一个百分比的值。单位座标空间的座标在0.0到1.0范围内。


所有座标值,无论是点座标还是单位座标都是浮点型。使用浮点型允许你指定精确的位置。

锚点影响几何操作

图层的几何操作是相对于图层的锚点。当操作图层的position或者transform属性,锚点的影响是最明显的。position属性总是和图层的锚点相关的而且你应用到图层的transformations也于锚点相关。



图层可以在3维上操作

每个图层都有2个转换矩阵,你可以使用它们来操作图层和图层的内容。CALayer的transform属性指定你想应用在图层和图层的子图层的转换。通常,当你想要改变图层本身时,你会使用这个属性。例如,你可能使用这个属性来缩放或者旋转图层或者临时地改变位置。sublayerTransform属性定义额外的转换,它只应用于子图层并且通常用于给场景的内容添加一个透视效果。

转换是通过矩阵与座标值相乘得到新的座标值,这个新的座标值代表原始点的转换版本。因为Core Animation的值能够在3维上指定,每个座标点都有4个值,它需要乘于一个4*4的矩阵。在Core Animation,这个转换用CATransform3D代表。幸运地是,你不用直接修改这个结构体的成员来实现标准的转换。Core Animation提供一套综合的函数来创建缩放、转换、旋转矩阵和矩阵比较。除了使用这些函数来操作转换,Core Animation扩展KVC支持来允许你使用键路径修改转换。


下面展示常用的转换矩阵设置。任何座标乘于一个恒等转换(identity transform),返回相同的座标。对于其他转换,座标的改变依赖于你改变哪个矩阵的组件。例如,只是在x轴平移,你为转换矩阵的tx组件提供一个非0值并且让ty和tz的值为0。对于旋转,你提供适当的正弦和余弦值的目标旋转角度。


图层树反映了动画状态的不同方面

Core Animation有3组图层对象。

• 模型图层树(或者简称“图层树”)的对象是其中一个交互最多的对象。树的对象都是模型对象并且存储任何动画的目标值。不管什么时候改变图层的属性,你都使用这些对象。

• 展示树的对象包含任何运行动画的动态值。然而这个图层树的对象包含动画的动态值,展示树的对象反映屏幕出现的当前值。你不应该修改这个树的对象。而是,你使用这些对象读取当前动画值,可能使用这些值来创建新的动画。

• 渲染树的对象执行实际的动画而且在Core Animation是私有的。

每组图层对象都组织成像视图一样的层次结构。实际上,APP会激活所有视图的图层,每个树的初始结构完全匹配视图的层次结构。而然,APP能添加额外的图层对象-也就是说,根据需求,图层不关联一个视图的层次结构。在为不需要全部视图开销的APP内容进行优化的情况下,你可能进行这种操作。


每个图层树的对象,都有一个匹配的对象在展示和渲染树。就像前面所提到的,APP主要和图层数的对象工作,但是可能同时访问展示树的对象。具体的说,获取图层对象的presentationLayer属性,会返回对应的展示树的对象。你可能想要访问这些对象来读取动画中的当前属性值。


注意:你应该在动画执行中访问展示树的对象。当动画正在进行,展示树的对象值是它们在屏幕上的即时值。这种行为不同于图层树,它总是反映代码最后设置的值而且等价于动画的最终状态。

图层与视图的关系

图层不会替代APP的视图,也就是说,你不能单独依赖一个图层对象来创建可视的界面。图层为视图提供基础设施。具体的说,图层使得描绘和动画显示视图内容更加简单和有效而且维持高帧率。而且,这里有很多图层不能做的事情。图层不能处理事件,描绘内容,参与响应链或者做其他事情。由于这个原因,所有APP任然必须有一个或者多个视图来处理这些交互。

在IOS,每个视图都存储对应的图层,但在OS X,你必须决定哪个视图有图层。在OS X v10.8之后,可以为每个视图添加图层。而且,你不必做这些事情并且可以在你不需要这些开销时失效图层。图层会有一些内存开销,但是它们的好处大于这些内存开销。所以在你失效图层支持前,进行性能测试。

当你支持视图的图层支持,你创建的是一个存储图层视图(layer-backed)。在一个存储图层视图,系统会创建底层的图层对象并保存和同步到视图。所以的IOS视图和大多数OS X的视图都是存储图层视图。然而,在OS X,你可以创建主持图层视图(layer-hosting),这个视图需要你自己提供图层对象。对于主持图层视图,AppKit不会进行图层处理而且不会在视图改变时修改它。

注意:对于存储图层视图,建议你尽可能操作视图而不是图层。在IOS,视图只是简单地包装图层对象,所以你进行的任何操作,通常对图层都能很好的工作。

除了图层与视图关联,你可以创建不关联视图的图层对象。你可以嵌入这些独立的图层对象到APP的其他图层对象,包括那些关联视图的图层对象。你可以使用独立的图层对象作为特殊优化的一部分。例如,如果你使用相同的图像在多个地方,你可以加载一次图像并关联到单独的独立的图层而且添加到图层树。每个图层都引用图像源而不是在内存中创建图像的副本。

建立图层对象

图层对象是Core Animation的中心对象。图层管理APP的可视内容并提供选项去修改样式和内容的可视外观。虽然IOS自动激活图层支持。OS X APP需要在充分利用性能优势前明确的激活它。一旦激活,你需要理解怎样设置和操作图层来获取你想要的效果。

改变关联视图的图层对象

存储图层视图默认创建CALayer实例,大多数情况下,你不需要不同类型的图层对象。而且,Core Animation提供不同的图层类别,每一个类都提供特别的功能。选择不同的图层类别可能确保提高性能或者支持特殊类型的内容在相同的方式。例如,CATiledLayer类是为展示大图像进行有效优化的图层。

使用UIView改变图层的类

你可以通过重写视图的layerClass方法并返回不同的类对象来改变视图的图层对象类型。大多数IOS视图创建CALayer对象并使用这个图层来存储内容。对于大多数视图,默认的选择也是好的一个而且你应该不需要改变它。但是你可能发现不等的图层类别在某些情况下会更适合。例如,你可能在下面一些情况下,改变图层的类别:

• 你的视图使用Metal或者OpenGL ES描绘视图,在这种情况下,你应该使用CAMetalLayer或者CAEAGLLayer对象。

• 有性能更好的图层类别。

• 你想要利用一些特别的Core Animation图层类别,例如特别的发射器和替换器。

改变视图的图层类别是非常直接的。你所要做的是重新layerClass方法并返回你想要使用的类对象。在展示之前,视图会调用layerClass方法并使用这个类来创建新的图层对象。一旦创建,视图的图层对象不能被改变。

+ (Class) layerClass {
   return [CAMetalLayer class];
}
不同的图层类别提供特殊的行为

Core Animation定义更多的标准图层类别,每个图层类别为特殊用途定义的。CALayer类是所有的图层对象的根类。它定义所有图层对象必须支持的行为而且也是存储图层视图的默认类型。然而,你可以定义下面其中一个图层类别。


提供图层内容

图层是数据对象并且管理APP提供的内容。图层内容是由包含需要展示的可视数据的位图组成。你可以通过3种方式提供内容的位图:

• 直接赋值图像对象(image)给图层对象的contents属性。(图层内容不会或者很少改变)

• 赋值代理对象(delegate)给图层对象并让代理描绘图层的内容。(图层内容周期性的改变和可能由外部的对象提供,例如,视图)

• 定义图层的子类并重写其中一个描绘方法来自己提供图层内容。(如果你必须创建自定义图层子类或者你想要改变基本的图层绘制行为)

只有在你自己创建图层对象才需要考虑图层内容。如果你的APP除了存储图层视图外不包含其它内容。你不必考虑使用之前提到的方式来提供图层内容。存储图层视图会自动地为关联的图层对象提供内容而且这可能是最有效的方式。

使用图像来提供图层内容

因为图层只是个容器而且关联图像位图,你可以直接赋值图像到图层的contents属性。赋值图像给图层是容易的并且允许你指定展示在屏幕上的图像。这个图像你可以直接提供而且不需要创建图像的副本。当你使用相同的图像在多个地方,这种方式可以节约内存。

你赋值的图像必须是CGImageRef类型。(在OS X v10.6之后,你可以赋值NSImage对象)当赋值图像,记住提供的图像需要匹配本地设备的分别率。对于设备的视网膜展示,需要你调整图像的contentsScale属性。

使用代理来提供图层内容

如果你的图层内容是动态变化的,你可以在需要时使用代理对象来提供和更新内容。在展示时,图层调用代理的方法来提供需要的内容。

• 如果你的代理对象实现displayLayer: 方法,方法的实现需要创建一个位图并赋值给图层的contents属性。

• 如果你的代理对象实现drawLayer:inContext: 方法,Core Animation创建位图和描绘位图的图像上下文,之后,会调用代理方法来填充位图。你的代理方法所要做的是把内容描绘到图形上下文。

代理对象必须实现displayLayer: 或者drawLayer:inContext: 方法。如果代理对象都实现这两个方法,图层只会调用displayLayer: 方法。

重写displayLayer: 方法非常适用于在需要加载和创建位图。

- (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];
    }
}
如果你没有预先渲染好的图像或者有帮助对象创建位图,你的代理可以使用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);
 
    // Release the path
    CFRelease(thePath);
}

对于有自定义内容的存储图层视图,你应该继续重写视图的方法来完成你的描绘。存储图层视图自动成为图层的代理对象并实现必要的方法,你不应该改变这些设置。你应该实现视图的drawRect: 方法来描绘你的内容。

提供子类来提供图层的内容

如果你实现自定义图层类别,你可以重写图层的描绘方法来完成任何描绘。自己生成自定义内容对于图层对象不常用,但是图层可以管理展示的内容。例如,CATiledLayer类通过把分隔成小图片并独立地渲染它们来管理大图片。因为只有图层有小图片(在给定时间需要被渲染)的信息而且直接管理描绘行为。

在创建子类时,你可以使用下面的一个方式来描绘图层内容:

• 重写图层的display方法并使用它来直接设置图层的contents属性。

• 重写图层的drawInContext: 方法并使用它来直接描绘到图形上下文。

你需要重写哪个方法依赖于你需要怎样控制描绘操作。display方法是主要的入口来更新图层内容,所以重写这个方法来让你可以完全控制这个过程。重写这个方法也意味着你需要创建CGImageRef并赋值到contents属性。如果你只是描绘内容(或者让图层管理描绘操作),你可以重写drawInContext: 方法并让图层创建内容存储。

调整你提供的内容

当你赋值图像到图层的contents属性,图层的contentsGravity属性决定图像怎样被操作来适应当前边框。默认,如果图像比当前边框大或者小,图层对象会缩放图形来适应可用空间。如果图层的边框的宽高比不同于图像的宽高比,这可能造成图像扭曲。你可以使用contentsGravity属性来确保你的内容以最好的方式展示。

contentsGravity属性的值可分为2类:

• 基于点的重力常量允许你固定图像到图层边框的边或者拐角而且没有缩放图像。

• 基于缩放的重力常量允许你使用其中一个选项来拉伸图像,其中一些会存储宽高比,有些不会。

下面展示基于点的重力设置是怎样影响图像。kCAGravityCenter常量,每个常量会固定图像到图层的边框矩形的边或者拐角。kCAGravityCenter常量会居中图层的图像。这些常量并都不会造成图像缩放,所以图像总是按原始大小描绘。如果图像大于图层的边框,这可能会造成图像的部分内容被裁剪,如果图像小于边框,部分图层内容不会被图像覆盖而是显示图层的背景颜色(如果设置的话)。


下面展示的是基于缩放的重力常量是怎样影响图像。如果图像不符合图层的边框矩形,那么所有这些常量都会缩放图像。这些模式的不同点在于怎样处理图像原始的宽高比。一些模式会保留,一些不会。默认,图层的contentsGravity属性被设置为kCAGravityResize常量,它是唯一的不会保留图像宽高比的模式。


使用高分辨率的图像

图层不会知道设备屏幕的分辨率。图层简单地存储位图的点并总是尽可能用可用的像素来最好地展示它。如果你赋值图像到图层的contents属性,你必须通过设置图层的contentsScale属性来告诉Core Animation图像像素。这个属性的默认值是1.0,对应使用标准的屏幕分辨率来展示图像。如果你打算使用视网膜展示图像,设置这个值为2.0。

仅在直接赋值位图到图层时,才需要改变contentsScale属性。UIKit和AppKit的存储图层视图会根据屏幕的分辨率和视图的内容自动设置图层的缩放因素为适当的值。

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

图层对象会构建可视的装饰,例如,边框和背景颜色,你可以使用装饰来补充图层的主要内容。因为这些可视的装饰不需要你进行渲染,在一些情况下,它们可能使用图层作为单独的实体。你所要做的是设置图层的属性,图层会进行必要的描绘,包括任何动画。

图层有自己的边框和背景颜色

图层可以在图像内容中展示填充的背景和绘画的边框。背景颜色在图层的图像内容后面渲染,边框在图像的前面渲染。如果图层包含子图层,它们也会出现在边框的子图层。因为背景颜色在图像的后面,这个颜色会在图像的透明部分显示出来。


 下面展示设置图层的背景颜色和边框。

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

注意:你可以使用任何类型的颜色来设置背景颜色,包括有透明度的颜色或者一个图案图像。当使用图案,尽管如此,请注意Core Animation会处理图案的渲染并使用标准的座标系统;这个座标系统不同于IOS的默认的座标系统。因此,在IOS渲染的图像会出现上下颠倒的问题,除非你翻转座标系。

如果你设置图层的背景颜色为不透明的颜色,考虑设置图层的不透明属性为YES。这样会提高性能并消除了图层对透明信号的存储管理。当拐角的弧度不为0时,你不能标记图层为不透明。

图层支持拐角弧度
你可以添加拐角弧度来创建圆角矩形。拐角弧度是一个可视的圆角装饰。因为它涉及到透明遮盖,拐角弧度不会影响图层内容的图形,除非设置masksToBounds属性为YES。然而,拐角弧度总是影响图层的背景颜色和边框的描绘。

为了应用拐角弧度到图层,设置图层的cornerRadius属性。弧度的值会以点来测量并应用到所有的四个拐角。
图层支持阴影嵌入

CALayer类包含几个属性来设置阴影。阴影通过使图层看起来好像是浮动在它的底图层内容之上来增加深度。这是另外的可视装饰类型。你可以控制阴影颜色、相对图层的内容放置位置、不透明度、形状。

不阴影的透明度默认为0;它可以高效地隐藏阴影。改变不透明度的值为非0值会造成Core Animation描绘阴影。因为默认直接放置在图层的下面,你可能需要改变阴影的偏移量才能看见它。你设置的阴影偏移量是基于图层本地的座标系统,它和IOS、OS X不同。


当添加阴影到图层,阴影成为图层内容的一部分并扩展到图层边框的外面。结果,如果你激活图层的masksToBounds属性,阴影会被裁剪周围的边。如果图层包含任何透明内容,这会造成那部分在图层下面的阴影显示出来并扩展到图层没有显示的地方。如果你想要使用阴影和边框遮盖,你可以使用两个图层而不是一个。把遮盖应用到包含内容的图层并嵌入到同样大小的、带有阴影效果的图层的里面。

添加图层的自定义属性

CAAnimation和CALayer类扩展了KVC约定并支持自定属性。你可以使用KVC来添加图层的数据并通过自定义键来获取这个数据。你甚至可以为你的自定义属性添加关联的动作,以便当你改变这个属性,对应的动画会触发。

打印存储图层视图的内容

在打印过程中,图层会在需要适应打印环境时重绘自己的内容。然而,当绘制到屏幕,Core Animation通常依赖缓存的位图,它也会在打印时重绘内容。特别是,如果存储图层视图使用drawRect: 方法来提供图层内容,Core Animation会再次调用drawRect: 方法来打印生成打印图层内容。

动画展示图层内容

Core Animation提供基础设施来简单地创建图层和拥有图层的视图的复杂动画。

简单改变图层的属性造成的动画

你可以根据你的需要来隐式或者显式地展示简单动画。隐式动画使用默认的时间和动画属性来展示动画,而显式的动画需要你使用动画对象来设置这些属性。所以隐式动画适合你不需要编写很多代码和使用默认时间来执行动画变化。

简单动画涉及到改变图层的属性和Core Animation执行这些变化的时间。图层定义很多能够影响图层的可视外观的属性。改变这些属性的其中一个,是一个动画变化外观的方式。例如,改变图层的不透明度从1.0到0.0会造成图层渐出并透明。

为了触发隐式动画,你必须做的是更新图层对象的属性。当改变图层树的图层对象,你的改变会通过这些对象马上反映出来。然而,图层对象的可视外观不会马上改变。Core Animation会使用你的改变来作为触发器来创建和安排一个或者多个隐式动画执行。因此,下面展示的例子会造成Core Animation创建一个动画对象并安排这个动画在下一个更新循环执行。

theLayer.opacity = 0.0;

为了让显式动画实现相同的效果,创建CABasicAnimation动画对象并使用这个对象来设置动画参数。你可以在添加动画对象到图层之前,设置动画的开始和结束值,改变持续时间或者改变任何其他动画参数。下面展示怎样使用动画对象来渐出一个图层。当创建这个对象,你指定需要动画的属性的键路径并设置动画参数。为了执行动画,你使用addAnimation:forKey: 方法来添加到你想要动画的图层。

CABasicAnimation* fadeAnim = [CABasicAnimation animationWithKeyPath:@"opacity"];
fadeAnim.fromValue = [NSNumber numberWithFloat:1.0];
fadeAnim.toValue = [NSNumber numberWithFloat:0.0];
fadeAnim.duration = 1.0;
[theLayer addAnimation:fadeAnim forKey:@"opacity"];
 
// Change the actual data value in the layer to the final value.
theLayer.opacity = 0.0;

提示:当创建显式动画,建议你总是设置动画对象的formValue属性值。如果你没有指定这个属性值,Core Animation会使用当前值来作为开始值。如果你已经更新属性到最终值,可能并不是你想要的结果。

不像隐式动画会更新图层对象的数据值,显式动画不会修改图层树的数据。显式动画只是提供动画。在动画结束后,Core Animation移除动画并使用当前数据值来重绘图层。如果你想要显式动画的改变能够永久,你必须像上面的例子更新图层的属性值为最终值。

隐式和显式的动画通常在当前运行循环结束后执行而且当前线程必须有运行循环安排动画执行。如果你改变多个属性或者如果你添加多个动画对象到图层,所有这些属性改变会在同一时间动画改变。例如,你可以在同时设置两个动画来实现在图层移出屏幕时渐退(fade)图层。然而,你也可以设置动画对象的开始时间。

使用关键帧动画来改变图层属性

相对于基于属性动画会从开始值到结束值来改变属性,CAKeyframeAnimation对象允许你指定一组线性或者非线性的目标值。关键帧动画会保存一组目标值和对应每一个值执行的时间。使用简单的设置,使用一个数组来同时指定值和时间。为了改变图层的位置,你可以根据路径来进行改变。动画对象使用你提供的关键帧并在指定的时间段内从一个值到下一个值之间插入值来构建动画。

下面展示图层position属性的5秒动画。position会跟随路径进行动画,这个路径使用CGPathRef数据类型。 

// create a CGPath that implements two arcs (a bounce)
CGMutablePathRef thePath = CGPathCreateMutable();
CGPathMoveToPoint(thePath,NULL,74.0,74.0);
CGPathAddCurveToPoint(thePath,NULL,74.0,500.0,
                                   320.0,500.0,
                                   320.0,74.0);
CGPathAddCurveToPoint(thePath,NULL,320.0,500.0,
                                   566.0,500.0,
                                   566.0,74.0);
 
CAKeyframeAnimation * theAnimation;
 
// Create the animation object, specifying the position property as the key path.
theAnimation=[CAKeyframeAnimation animationWithKeyPath:@"position"];
theAnimation.path=thePath;
theAnimation.duration=5.0;
 
// Add the animation to the layer.
[theLayer addAnimation:theAnimation forKey:@"position"];
指定关键帧值

关键帧的值是关键帧动画的最重要部分。这些值定义了动画执行过程中的行为。重要方式指定关键帧值是一个包含CGPoint数据类型的数组对象,你也可以指定CGPathRef数据类型。

当指定一个数组值,数组里面的值类型依赖于属性。你可以直接向数组添加对象;然而,一些对象必须强转换为id类型,所有的数值类型或者结构体必须包装为对象。

• CGRect类型需要包装为NSValue对象。

• 对于转换属性,CATransform3D矩阵需要包装为NSValue对象。动画这个属性会造成关键帧动画会依次应用转换矩阵。

• 对于borderColor属性,强转每一个CGColorRef类型为id类型。

• CGFloat类型需要包装为NSNumber对象。

• 动画图层的contents属性,指定一组CGImageRef数组。

对于CGPoint类型,你可以创建一组NSNumber数组或者创建CGPathRef对象来指定路径。当你指定一组点数组,关键帧动画对象描绘一条线段在每个连续的点之间并跟随这个路径。当你指定CGPathRef对象,动画开始在路径的开始点并跟随轮廓,包括在任何曲面。你可以使用闭合和开放的路径。

指定关键帧动画的时间

关键帧动画的时间和节奏比基本动画更加复杂而且你还可以使用下面的几个属性来控制它:

• calculationMode属性定义计算动画时间的算法。这个属性值会影响其他时间相关属性的使用。

    • 线性和立方动画,也就是说,calculationMode属性被设置为kCAAnimationLinear或者kCAAnimationCubic;使用给定的时间信息来生成动画。这些模式给你最大的控制动画时间。

   • 节奏动画,也就是说,calculationMode属性被设置为kCAAnimationPaced或者kCAAnimationCubicPaced;不会依赖keyTimes或者timingFunctions属性提供的外部时间。而是时间值被隐式地计算并提供给恒定速度的动画。

    • 离散动画,也就是说,calculationMode属性被设置为kCAAnimationDiscrete;会造成动画属性从一个关键帧跳到下一个关键帧而且不会在中间插值。这种计算模型使用keyTimes属性而不会使用timingFunctions属性。

• keyTimes属性指定每个关键帧值的时间。这个属性只有在计算模型被设置为kCAAnimationLinear、kCAAnimationDiscrete、kCAAnimationCubic时使用。它不会使用节奏动画。

• timingFunctions属性为每个关键帧片段指定时间曲线。

如果你想要自己处理动画时间,使用kCAAnimationLinear或kCAAnimationCubic模型和keyTimes以及timingFunctions属性。keyTimes定义每个关键帧值的时间点。所有时间的中间值会被时间函数控制;它允许你应用渐入或者渐出曲线到每个片段。如果你没有指定时间函数,默认是线性函数。

停止正在运行的显式动画

动画通常是一直运行到结束,但是你可以使用下面的方式来提前停止它:

• 为了从图层移除一个动画对象,调用图层的removeAnimationForKey:方法来移除你的动画对象。这个方法使用addAnimation:forKey: 方法的key来标记动画。这个key你不能设置为nil。

• 为了移除图层的所以动画,调用图层的removeAllAnimations方法。这个方法会马上移除所以正在进行的动画并使用当前状态信息来重绘图层。

注意:你不能直接移除隐式动画。

当你从图层移除动画,Core Animation提供使用当前值重绘图层来响应。因为当前值通常是动画的结束值,这会造成图层的外观突然变化。如果你想要图层保留在动画的最后一帧,你可以使用展示树的对象来获取最终值来设置图层树的对象。

多个动画改变

如果你同时应用多个动画到图层,你可以使用CAAnimationGroup对象组合它们。使用这个组合对象并提供一个单独的配置点来简化多个动画对象管理。应用于动画组的时间和时长会重写到动画中。

// Animation 1
CAKeyframeAnimation* widthAnim = [CAKeyframeAnimation animationWithKeyPath:@"borderWidth"];
NSArray* widthValues = [NSArray arrayWithObjects:@1.0, @10.0, @5.0, @30.0, @0.5, @15.0, @2.0, @50.0, @0.0, nil];
widthAnim.values = widthValues;
widthAnim.calculationMode = kCAAnimationPaced;
 
// Animation 2
CAKeyframeAnimation* colorAnim = [CAKeyframeAnimation animationWithKeyPath:@"borderColor"];
NSArray* colorValues = [NSArray arrayWithObjects:(id)[UIColor greenColor].CGColor,
            (id)[UIColor redColor].CGColor, (id)[UIColor blueColor].CGColor,  nil];
colorAnim.values = colorValues;
colorAnim.calculationMode = kCAAnimationPaced;
 
// Animation group
CAAnimationGroup* group = [CAAnimationGroup animation];
group.animations = [NSArray arrayWithObjects:colorAnim, widthAnim, nil];
group.duration = 5.0;
 
[myLayer addAnimation:group forKey:@"BorderChanges"];

更先进的方式来组合动画是使用事务对象。事务提供更加灵活地创建嵌套的动画并为每个动画设置不同的参数。

检测动画结束

Core Animation提供检测动画开始和结束。这些通知是执行与动画相关的任何管理任务的好时机。例如,你可以使用开始通知来建立一些相关的状态信息并使用对应的结束通知来移除这个状态。

这里有两种不同的方式来通知动画的状态:

• 使用setCompletionBlock: 方法来为当前事务添加完成代码块。当事务的所有动画完成,事务执行你的完成代码块。

• 向CAAnimation对象提供代理对象并实现animationDidStart: 和animationDidStop:finished: 代理方法。

如果你想要连接2个动画以便一个结束后另一个就会开始,不要使用动画通知。而是,使用动画对象的beginTime属性来设置开始时间。为了连接2个动画,设置开始时间为另一个动画的结束时间。

怎样动画存储图层视图

如果图层属于存储图层视图,建议使用UIKit或者AppKit的基于视图动画接口来创建动画。这里有直接使用Core Animation接口来动画图层,但是怎样创建这些动画依赖于目标平台。

在IOS修改图层的规则

因为IOS视图总是有内部的图层,UIView的大多数数据都是直接从图层对象派生来的。结果,你对图层进行的改变会自动地反映到视图对象。这种行为意味着你可以使用Core Animation或者UIView接口来进行改变。

如果你使用Core Animation来初始化动画,你必须在基于视图的动画代码块中执行所有Core Animation调用。UIView类默认失效图层动画但会在动画代码块中重新激活它。所以任何你在动画代码块外面的改变不会造成动画。下面展示怎样隐式改变图层的不透明度并显式地放置它。在这个例子中,myNewPosition变量已经在之前计算好并被代码块捕获。2个动画在同一时间开始但是透明度以默认时间进行而位置动画以动画对象指定的时间进行。

[UIView animateWithDuration:1.0 animations:^{
   // Change the opacity implicitly.
   myView.layer.opacity = 0.0;
 
   // Change the position explicitly.
   CABasicAnimation* theAnim = [CABasicAnimation animationWithKeyPath:@"position"];
   theAnim.fromValue = [NSValue valueWithCGPoint:myView.layer.position];
   theAnim.toValue = [NSValue valueWithCGPoint:myNewPosition];
   theAnim.duration = 3.0;
   [myView.layer addAnimation:theAnim forKey:@"AnimateFrame"];
}];
记住把更新视图约束作为动画的一部分

如果你使用基于约束布局规则来管理你的视图位置,你必须移除任何可能干扰动画的约束并把这个操作作为配置动画的一部分。约束影响你对视图位置和大小的任何改变。它也影响视图和子视图的关系。如果你正在动画改变任何这些项目,你可以移除约束并进行改变,之后重新添加必要的约束。

构建图层的层次结构

大多时候,最好的方式使用层是和视图对象结合。然而,有时候你需要通过添加额外的图层对象来增加视图的层次结构。你可以这样做在可能获得更好的性能或者单独使用视图很难实现某个功能。在这种情况下,你必须知道怎样管理你创建的图层的层次结构。

注意:在OS X v10.8之后,建议你减少使用图层的层次结构并只使用存储图层视图。在这个版本的图层的重绘政策会让你自定义存储图层视图的行为并还会和单独使用图层一样有相同的性能。

安排图层到图层的层次结构

图层的层次结构在很多方面和视图的层次结构相似。你嵌入一个图层到另外一个来构建父子关系。(嵌入的图层为子图层,被嵌入的图层为父图层)。父子关系会影响子图层的各方面。例如,它的内容会放在父图层的上面,它的位置会相对于父图层的座标系统而且父图层的转换也会影响子图层。

添加、插入和移除子图层

当你处理你创建的图层对象,你可以使用上面的方法。你不能使用这些方法来安排存储图层视图的图层对象。然而,存储图层视图可以作为你创建的图层对象的父图层。

设置子图层的位置和大小

当添加和插入子图层,在图层出现在屏幕前,你必须设置图层的位置和大小。你可以在添加子图层到图层的层级结构后修改大小的位置,但你应该在图层创建时设置这些值。

你使用bounds属性来设置子图层的大小和使用position属性来设置位置。bounds的原点总是为(0,0),大小为你设置的大小。position属性是与锚点相关的;锚点默认是在图层的中心点。如果你没有为这些属性赋值,Core Animation默认初始化图层的宽高为0,位置为(0,0)。

myLayer.bounds = CGRectMake(0, 0, 100, 100);
myLayer.position = CGPointMake(200, 200);

注意:总是设置图层的宽高为整数值。

图层的层次结构是怎样影响动画

一些父图层的属性可能影响子图层的动画行为。其中一个属性就是speed属性,它是动画的速度的倍增器(multiplier)。这个属性的值默认为1.0,但改变成2.0会造成动画以2倍的速度进行。这个属性不仅影响被设置的图层,还有它的子图层。这种改变也是乘积的。如果子图层和父图层都设置速度为2.0,子图层的动画会以4倍速度执行。

大多数图层对子图层的影响都在预料的方式进行。例如,应用旋转转换到图层会造成图层和子图层都会旋转。相似地,改变图层的不透明属性也会改变子图层的不透明属性。

调整图层的层次结构布局

Core Animation支持调整子图层的大小和位置来响应父图层变化的选项。在IOS,普遍使用存储图层视图使得创建图层的层次结构不那么重要;只有手动的布局更新被支持。对于OS X,其他几个可用选项使得管理图层的层次结构更加容易。

图层级别的布局只在你使用独立的图层对象来构建图层的层次结构时相关。如果你的图层都与视图相关,使用基于视图的布局支持更新视图的位置和大小来响应变化。

使用约束来管理图层的层次结构在OS X

约束让你使用一组详细的图层和父图层以及兄弟图层的关系来指定图层的位置和大小。定义约束需要下面的步骤:

1.创建一个或者多个CAConstraint对象。使用这些对象来定义约束参数。

2.添加你的约束对象到图层;图层的属性会被约束修改。

3.获取共享的CAConstraintLayoutManager对象并赋值给直属父图层。

下面展示的属性用来定义约束并影响图层的方面。通过基于它的中点边缘相对于另一图层的位置,你可以使用约束来改变图层的位置。你也可以使用约束来改变图层的大小和位置。你所做的改变可以和父图层或者其他图层成比例。你甚至可以添加缩放因子或者约束来形成变化。这种额外的灵活性使得使用简单的规则来精确地控制图层的大小和位置成为可能。


每个约束对象封装了使用同一个座标轴的2个图层的几何关系。2个约束对象的最大值可能赋值给每个座标轴而且这2个约束决定哪个属性是可变的。例如,如果你指定图层的左边和右边的约束;图层的大小就会改变。如果你指定图层的左边和宽度约束;图层的右边的位置就会改变。如果你指定图层的单独一个边的约束,Core Animation会创建一个隐式的约束来保持图层的大小并适应给定的尺寸。

当你创建约束,你必须总是指定3个信息:

• 图层的方面。

• 图层的引用。

• 引用图层的方面比较。

下面展示简单的约束来固定图层和父图层的垂直中点。当引用父图层,使用字符串"superlayer"。这个字符串会保留父图层的引用。使用这种方式消除了需要拥有这个图层的指针和名称。它也允许你改变父图层和让约束自动应用到父图层。(当你创建相对兄弟图层的约束,你必须使用name属性来指定兄弟图层的名称。)

[myLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMidY
                                                 relativeTo:@"superlayer"
                                                  attribute:kCAConstraintMidY]];

为了在运行时使用约束,你必须附加共享的CAConstraintLayoutManager对象到直属父图层。每个图层都会管理自己子图层的布局。赋值这个布局管理者到父图层是告诉Core Animation应用子图层定义的约束。布局管理者对象自动应用这些约束。在赋值管理者给父图层后,你不需要去告诉Core Animation更新约束。

为了查看约束在特定场景是怎样工作的,考虑下面图片。在这个例子中,需要layerA的宽高不改变而且在父图层的中心点。另外,layerB的宽必须匹配layerA,layerB的顶边必须低于layerA底边10个点,layerB的底边必须高于父图层底边10个点。


// Create and set a constraint layout manager for the parent layer.
theLayer.layoutManager=[CAConstraintLayoutManager layoutManager];
 
// Create the first sublayer.
CALayer *layerA = [CALayer layer];
layerA.name = @"layerA";
layerA.bounds = CGRectMake(0.0,0.0,100.0,25.0);
layerA.borderWidth = 2.0;
 
// Keep layerA centered by pinning its midpoint to its parent's midpoint.
[layerA addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMidY
                                                 relativeTo:@"superlayer"
                                                  attribute:kCAConstraintMidY]];
[layerA addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMidX
                                                 relativeTo:@"superlayer"
                                                  attribute:kCAConstraintMidX]];
[theLayer addSublayer:layerA];
 
// Create the second sublayer
CALayer *layerB = [CALayer layer];
layerB.name = @"layerB";
layerB.borderWidth = 2.0;
 
// Make the width of layerB match the width of layerA.
[layerB addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintWidth
                                                 relativeTo:@"layerA"
                                                  attribute:kCAConstraintWidth]];
 
// Make the horizontal midpoint of layerB match that of layerA
[layerB addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMidX
                                                 relativeTo:@"layerA"
                                                  attribute:kCAConstraintMidX]];
 
// Position the top edge of layerB 10 points from the bottom edge of layerA.
[layerB addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxY
                                                 relativeTo:@"layerA"
                                                  attribute:kCAConstraintMinY
                                                     offset:-10.0]];
 
// Position the bottom edge of layerB 10 points
//  from the bottom edge of the parent layer.
[layerB addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinY
                                                 relativeTo:@"superlayer"
                                                  attribute:kCAConstraintMinY
                                                     offset:+10.0]];
 
[theLayer addSublayer:layerB];

在上面的例子中,layerB没有明确设置大小。因为已定义的约束,layerB的宽高会在布局更新时自动设置。因此,设置边框的大小是不必要的。

警告:当创建约束,不要在你的约束中创建循环引用。循环约束是不可能计算约束信息的。当遇到这样的循环引用,布局行为是不确定的。

建立自动布局规则

自动布局规则是OS X中另外一种调整大小和位置的方式。在自动布局规则,图层的边是固定还是相对父图层边变化的。你可以相似地设计图层的宽高是固定还是可变的。这种关系通常是在父子图层中。你不能使用自动布局规则在兄弟图层。

为了建立图层的自动布局规则,你必须设置适当的常量到图层的autoresizingMask属性。默认的,图层被设置为有固定的宽高。在布局中,图层的精确的大小和位置被Core Animation自动地计算;这涉及一组基于多个因素的复杂计算。在询问你的代理对象做任何手动布局更新前,Core Animation会应用自动布局行为,所以你可以在代理中调整自动布局结果。

手动布局图层的层级结构

在IOS和OS X,你可以在父图层的代理对象中实现layoutSublayersOfLayer: 方法来手动处理布局。你使用这个方法来调整任何子图层的大小和位置。当进行手动布局更新,你需要执行必要的计算每个图层的位置。

如果你使用的自定义子类,你的子类可以重写layoutSublayers方法并使用这个方法(而不是代理方法)来处理任何布局任务。你应该只在需要完整地控制子图层位置时,重写这个方法。替换默认实现会阻止Core Animation应用约束或者自动布局规则在OS X。

子图层和裁剪

不像视图,父图层不会自动裁剪子图层内容;这些内容会放在边框矩形之外。而是,父图层默认允许子图层展示它们的全部内容。但是,你可以提供设置masksToBounds属性为YES来重新激活裁剪。

图层的裁剪遮盖(mask)的形状包括图层的圆角。下面展示一个图层来论述masksToBounds属性是怎样影响带有圆角的图层。当这个属性设置为NO,子图层会展示全部内容,即使它超出父图层的边界。改变这个属性为YES会造成它们的内容被裁剪。


图层之间转换座标值

偶尔,你可能需要转换一个图层的座标值到同一屏幕的不同图层的座标值。CALayer类提供提供一组简单的转换方法来达到这一目的:

• convertPoint:fromLayer:

• convertPoint:toLayer:

• convertRect:fromLayer:

• convertRect:toLayer:

除了转换点和矩形值,你还可以使用convertTime:fromLayer: 和convertTime:toLayer: 方法来转换图层之间的时间值。每个图层都定义了自己的本地时间空间并使用这个时间空间来与系统的其余部分同步动画的开始和结束。这些时间空间默认被同步;然而,如果你改变动画的速度,相应的时间空间也会改变。你可以使用这些时间转换方法来分析这些问题并保证2个图层的时间都同步。

进阶的动画技巧

过渡动画支持图层的可视变化

正如名字所暗示的,过渡动画对象创建图层的动画视觉过渡。大多数使用过渡对象动画显示一个图层并消失另一个图层。不像基于属性的动画,当动画改变图层的一个属性,过渡动画会操作图层的缓存图像并创建可视效果;这个效果通过单独改变属性是很困难或者不可能实现。过渡的标准类型允许你实现透露(reveal)、推进(push)、移动(move)或者淡入淡出(crossfade)动画。在OS X,你可以使用Core Image的过滤器来创建过渡;使用另一效果类型,例如,擦除(wipes)、翻页(page curls)、涟漪(ripples)或者你设计的自定义效果。

为了实现过渡动画,你创建CATransition对象并添加到图层。你使用这个过渡动画并指定过渡类型和开始点,结束点。你不必使用整个过渡动画。过渡动画允许你指定开始和结束进程值。这些值让你可以在动画中途开始或者结束。

下面展示创建2个视图的推进(push)过渡动画。在这个例子中,myView1和myView2都在相同父视图的相同位置,但是只有myView1是可视的。这个推进过渡会造成myView1滑出到左边并淡出直到隐藏,同时myView2从右边滑进并可见。同时更新2个视图的隐藏属性确保在动画结束后,2个视图的可见是正确的。

CATransition* transition = [CATransition animation];
transition.startProgress = 0;
transition.endProgress = 1.0;
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromRight;
transition.duration = 1.0;
 
// Add the transition animation to both layers
[myView1.layer addAnimation:transition forKey:@"transition"];
[myView2.layer addAnimation:transition forKey:@"transition"];
 
// Finally, change the visibility of the layers.
myView1.hidden = YES;
myView2.hidden = NO;

当2个图层都涉及相同过渡,你可以使用同一个过渡对象。使用相同的过渡对象会简化你的代码。然而,你可以使用不同的过渡对象并在对每个图层的过渡参数不同时才这样做。

下面展示在OS X,怎样使用Core Image过滤器来实现过渡效果。在你设置过滤器的参数后,赋值过渡对象的filter属性。之后,应用动画的过程和其他类型的动画对象相同。

// Create the Core Image filter, setting several key parameters.
CIFilter* aFilter = [CIFilter filterWithName:@"CIBarsSwipeTransition"];
[aFilter setValue:[NSNumber numberWithFloat:3.14] forKey:@"inputAngle"];
[aFilter setValue:[NSNumber numberWithFloat:30.0] forKey:@"inputWidth"];
[aFilter setValue:[NSNumber numberWithFloat:10.0] forKey:@"inputBarOffset"];
 
// Create the transition object
CATransition* transition = [CATransition animation];
transition.startProgress = 0;
transition.endProgress = 1.0;
transition.filter = aFilter;
transition.duration = 1.0;
 
[self.imageView2 setHidden:NO];
[self.imageView.layer addAnimation:transition forKey:@"transition"];
[self.imageView2.layer addAnimation:transition forKey:@"transition"];
[self.imageView setHidden:YES];

注意:当使用Core Image过滤器到动画,棘手的是设置过滤器。例如,条形滑动过渡(bar swipe transition),指定输入角度为太高或者太低会造成看起来没有过渡发生。如果你没有看到你期望的动画,尝试调整过滤器的参数为不同的值。

自定义动画的时间设置

时间设置(Timing)是动画重要的一部分,在Core Animation,你可以通过CAMediaTiming协议的方法和属性为动画设置精确的时间设置信息。有2个Core Animation类符合这个协议。CAAnimation类符合这个协议,以便你可以设置时间设置信息到你的动画对象。CALayer也实现这个协议,以便你能为隐式动画设置一些时间相关的功能,虽然包装了浙西动画的隐式过渡对象通常提供默认的时间设置信息。

当考虑时间设置和动画,这是很重要的明白图层是怎样和时间设置工作的。每个图层都有自己的本地时间;这个时间用来管理动画的时间设置。通常,2个不同图层的本地时间是足够接近的;你可以为每个图层指定相同时间值而且用户不会注意到。然而,通过父图层或者自己的时间设置参数,图层的本地时间可以被修改。例如,改变图层的speed属性会造成动画的持续时间按比例地变化。

为了帮助你确保图层的时间值是适合的,CALayer类定义convertTime:fromLayer: 和 convertTime:toLayer: 方法。你可以使用这些方法转换固定时间到图层的本地时间或者转换图层的时间值到另一个图层。这些方法考虑到媒体时间属性(可能影响图层的本地时间)并返回你想要使用到另一个图层的值。下面的例子展示你应该定期地获取图层的本地时间。CACurrentMediaTime函数是一个返回计算机当前时钟时间的便利函数,这个时间会被获取并转换为图层的本地时间。

CFTimeInterval localLayerTime = [myLayer convertTime:CACurrentMediaTime() fromLayer:nil];

一旦你获取图层的本地时间值,你可以使用这个值来更新动画对象的时间相关属性或者图层。你还可以实现一些有趣的动画行为:

• 使用beginTime属性来设置动画的开始时间。通常,动画会在下一个更新循环开始。你可以使用beginTime参数开延时动画开始时间机秒钟。连接2个动画的方式是设置动画的开始时间来匹配另一个动画的结束时间。

如果你延时动画的开始时间,你可能想要设置fillMode属性为kCAFillModeBackwards。这个fillMode会造成图层展示动画的开始值,即使图层树的图层对象包含不同的值。没有这个fillMode,在动画执行前,你可能会立即看到最终值。其他fillMode也是可用的。

• autoreverses属性造成动画执行一段持续时间之后,返回动画的开始值。这个属性可以和repeatCount属性结合来实现动画返回并在开始和结束值来回出现。为autoreversing动画设置repeatCount为整数值(例如,1.0)会造成动画停止到开始值。添加额外的一半(例如,repeatCount为1.5)会造成动画停止在结束值。

• 组动画(group animations)使用timeOffset属性来实现动画之间的延时启动。

暂停和恢复动画

为了暂停动画,你可以利用图层符合CAMediaTiming协议并设置图层动画的speed属性为0.0。设置speed为0会暂停动画直到你改变这个值为非0。

-(void)pauseLayer:(CALayer*)layer {
   CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
   layer.speed = 0.0;
   layer.timeOffset = pausedTime;
}
 
-(void)resumeLayer:(CALayer*)layer {
   CFTimeInterval pausedTime = [layer timeOffset];
   layer.speed = 1.0;
   layer.timeOffset = 0.0;
   layer.beginTime = 0.0;
   CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
   layer.beginTime = timeSincePause;
}
显示事务允许你改变动画参数

每次你对图层的改变都是事务的一部分。CATransaction类管理动画创建和分组以及执行时间。在大多数情况下,你不必创建事务。不管你添加隐式还是显示动画,Core Animation都会自动创建隐式事务。但是,你可以创建显式的事务来精确地管理这些动画。

你使用CATransaction类的方法来创建和管理事务。为了开始(和隐式创建的)新的事务,调用begin类方法;为了结束事务,调用commit类方法。在这2个调用之间是你所需要的事务改变。例如,为了改变图层的2个属性,你可以使用下面的代码:

[CATransaction begin];
theLayer.zPosition=200.0;
theLayer.opacity=0.0;
[CATransaction commit];

其中一个使用事务主要原因是能在显式事务的范围内进行持续时间、时间函数和其他参数的改变。你可以赋值完成代码块到完整的事务以便你能够获取一组动画完成的通知。改变动画参数需要通过使用setValue:forKey: 方法来适当修改事务字典的键值对。例如,为了改变默认的持续时间为10秒,你可以改变kCATransactionAnimationDuration键的值。

[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:10.0f]
                 forKey:kCATransactionAnimationDuration];
// Perform the animations
[CATransaction commit];

您可以在需要为不同的动画集提供不同的默认值的情况下嵌套事务。为了嵌套事务在另一个事务内,只是再次调用begin类方法。每次begin调用必须有匹配的commit方法调用。只有在你提交改变到最外图层的事务,Core Animation开始关联的动画。

[CATransaction begin]; // Outer transaction
 
// Change the animation duration to two seconds
[CATransaction setValue:[NSNumber numberWithFloat:2.0f]
                forKey:kCATransactionAnimationDuration];
// Move the layer to a new position
theLayer.position = CGPointMake(0.0,0.0);
 
[CATransaction begin]; // Inner transaction
// Change the animation duration to five seconds
[CATransaction setValue:[NSNumber numberWithFloat:5.0f]
                 forKey:kCATransactionAnimationDuration];
 
// Change the zPosition and opacity
theLayer.zPosition=200.0;
theLayer.opacity=0.0;
 
[CATransaction commit]; // Inner transaction
 
[CATransaction commit]; // Outer transaction
添加透视图到你的动画

APP能在3维空间操作图层,但是为了简化,Core Animation使用平行投影来展示图层,从本质上讲,将场景变平为二维平面。这种默认行为造成相同大小的图层会有不同的zPosition值,但看起来是相同大小,即使它们在z轴很远。你通常会在三维空间中看到这样一个场景的视角已经消失了。但是,你可以通过修改图层的转换矩阵幷包含视角信息来改变这种行为。

当修改场景的视角,你需要修改父图层的sublayerTransform矩阵,它包含被看见的图层。通过应用相同的视角信息到所有子图层来简化你的代码。这确保应用到兄弟子图层的视角信息是正确的,这些子图层在不同的平面上相互重叠。

下面展示为父图层创建简单的视角转换。在这个例子,自定义eyePosition变量指定从看的图层到z轴的相对距离。通常,你指定eyePosition为正值来保持图层的方向。更大的值会导致更平坦的场景,而较小的值则会导致图层之间更大的视觉差异。

CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0/eyePosition;
 
// Apply the transform to a parent layer.
myParentLayer.sublayerTransform = perspective;

父图层设置后,你可以改变zPosition属性在任何子图层并观察它们的大小是怎么基于从eye position的相对距离变化的。

改变图层的默认行为

Core Animation使用动作对象来实现图层的隐式动画行为。动作对象是实现CAAction协议并定义一些层的先关行为。所以CAAnimation对象都实现这个协议,当图层的属性被改变,这些对象通常被赋值并执行。

动画属性是其中一中动作,你可以自己定义动作对象并关联到图层对象。

自定义符合CAAction协议的动作对象

为了创建自己的动作对象,符合CAAction协议并实现runActionForKey:object:arguments: 方法。在这个方法,使用这些参数信息来执行你想要在图层对象的行为。你可以使用这个方法来添加动画到图层或者做其他任务。

当你定义动作对象,你必须定义动作对象要怎样被触发。动作的触发器定义一个注册图层键。动作对象可以被下面一些情况触发:

• 图层的属性被改变。这可以是图层的任何属性而且不只是动画那部分。(你可以为自定义属性关联动作)标识动作的键是属性的名称。

• 图层位于可视状态或者被添加到图层的层次结构。标识这个动作的键为kCAOnOrderIn。

• 图层从层次结构移除。标识这个动作的键为kCAOnOrderOut。

• 图层即将执行过渡动画。标识这个动作的键为kCATransition。

动作对象必须安装到图层才会有效

在动作能被执行前,图层需要发现对应的动作对象执行。图层先关的键是属性名或者是标识动作的特殊的字符串。当适当的时间发生到图层,图层会调用actionForKey: 方法来搜索关联键的动作对象。在搜索的时候插入动作对象并为这个键提供相关的动作对象。

Core Animation会以下面的顺序寻找动作对象:

1. 如果图层有代理对象并且代理实现actionForLayer:forKey: 方法,图层会调用这个方法。代理必须实现下面任务:

    • 为这个键返回动作对象。

    • 如果不能处理这个动作,返回nil,这回造成搜索继续。

    • 返回NSNull对象,这回造成搜索马上结束。

2. 图层在它的actions字典寻找对应键的动作对象。

3. 图层寻找style字典对应键的动作对象。(换句话说,style字典包含actions的键并且值也是一个字典。图层会在第二个字典寻找指定键的动作对象) 

4. 图层调用defaultActionForKey: 类方法。

5. 图层执行Core Animation的定义的隐式动作。

如果你在搜索的时候提供动作对象,图层会停止搜索并执行返回的动作对象。当发现动作对象,图层调用动作对象的runActionForKey:object:arguments: 方法来执行动作。如果你已经在CAAnimation定义的键再定义动作对象,你可以使用这个方法的默认实现来执行动作。如果你自定义CAAction协议的动作对象,你必须自己实现这个方法。

在哪里安装动作对象,依赖于需要怎样修改图层。

• 对于只在特殊的情况下才应用的动作对象或者对于已经使用代理对象的图层,提供代理对象并实现actionForLayer:forKey: 方法。

• 对于不使用代理对象的图层对象,直接添加动作到层的actions字典。

• 对于和图层对象的自定义属性相关的动作对象,包含这个动作到style字典。

• 对于图层的基本行为动作,子类这个图层对象并重写defaultActionForKey: 方法。

下面展示这个代理方法的实现。在这个例子中,代理寻找图层的contents属性变化并在替换新的内容时,使用一个过渡动画。

- (id<CAAction>)actionForLayer:(CALayer *)theLayer
                        forKey:(NSString *)theKey {
    CATransition *theAnimation=nil;
 
    if ([theKey isEqualToString:@"contents"]) {
 
        theAnimation = [[CATransition alloc] init];
        theAnimation.duration = 1.0;
        theAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
        theAnimation.type = kCATransitionPush;
        theAnimation.subtype = kCATransitionFromRight;
    }
    return theAnimation;
}
使用CATransaction类来临时失效动作
你可以使用CATransaction类来临时失效图层动作。当你改变图层属性,Core Animation通常创建隐式事务对象来动画改变。如果你不想动画改变,你可以创建显式事务并设置它的kCATransactionDisableActions属性为ture来失效隐式动画。
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue
                 forKey:kCATransactionDisableActions];
[aLayer removeFromSuperlayer];
[CATransaction commit];

提高动画性能

Core Animation是一个很好的方式来提高APP动画的帧率,但是它不保证能提高性能。特别在OS X,你必须选择最有效的方式来使用Core Animation行为。和所有与性能相关的问题,你应该使用Instruments来测量和跟踪APP的性能以便确保性能时提高的而不是下降的。

为你的OS X视图选择最后的重绘政策

NSView的默认重绘政策保存原始的绘画行为,即使视图是存储图层视图。如果你正在使用存储图层视图,你应该解释重绘政策选择和选择这个政策会怎样提高性能。在大多数情况下,默认政策并不是总能提高最佳性能。NSViewLayerContentsRedrawOnSetNeedsDisplay政策很可能会减少APP描绘任务并提高性能。其他政策也可能会提供更好哦性能在特定的视图。

常用的提示和技巧
尽可能使用不透明图层

设置图层的opaque属性为YES,让Core Animation知道不需要维持图层的透明度信号。没有透明度信号意味着编译器不需要把图层的内容和背景内容混合,这会节省渲染的时间。然而,这个属性对图层是相对重要的(一部分存储图层视图或者Core Animation创建的底层图层位图)。如果你直接赋值图形到图层的contents属性,不管opaque属性的值,图形的透明度信号都会被保存。

CAShapeLayer对象尽量使用简单路径

CAShapeLayer类提供渲染路径来创建内容并在编译时生成位图图形。优点是图层总是在尽可能最佳的分辨率下描绘路径,但是这个优点也会话费额外的渲染时间。如果你提供的路径是复杂,光栅化这个路径可能会很昂贵。如果图层的大小经常变化(必须经常重绘),花费的绘画时间会增加并成为性能瓶颈。

减少形状图层的绘画时间是划分复杂形状为简单形状。使用简单的路径并使用多个CAShapeLayer对象来组合布局;这会比描绘一个复杂大路径快得多。因为描绘操作发生在CPU然而组合任务发生在GPU。在这种简单化的方式下,潜在的性能获取依赖你的内容。在优化前必须测量你的代码性能以便有个比较。

显式地设置相同图层的内容

如果你在多个图层对象使用相同图形,加载图形并直接赋值到图层的contents属性。赋值图形到contents属性阻止图层分配内存存储。图层会使用这个图形来提供存储。当多个图层使用相同图形,这意味着所有这些图层共享相同内存而不是创建图形的副本。

总是设置层的大小为整数值

为了最好的结果,总是设置层对象的宽高为整数值。虽然图层边框宽高为浮点型,图层边框最终被用来创建位图图形。指定宽高为整数值可以简化Core Animation的工作。

图层尽可能使用异步渲染

drawLayer:inContext: 代理方法或者视图的drawRect: 方法通常在主线程同步进行的。在一些情况下,同步地描绘内容不会提供最好的性能。如果你意识到你的动画性能不好,你可以激活drawsAsynchronously属性并让这些描绘操作在后台进行。如果你这样做,确保你的描绘代码是线程安全。

当添加阴影到你的图层,指定阴影路径

让Core Animation确定阴影的形状可能会很昂贵并影响APP性能。使用CALayer的shadowPath属性来明确地指定阴影形状。当你为这个属性指定路径对象,Core Animation使用这个形状来描绘并缓存阴影效果。对于那些形状不会改变或者很少改变的图层,这会极大地提高性能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章