[译] Core Animation 编程指南 - 动画图层内容

696

本文首发地址

翻译说明
transaction - 事务

通过 Core Animation 提供的基础,我们可以轻松的为 app 的图层创建复杂的动画,并把动画扩展到拥有图层的视图上。可动画的例子包含:改变图层的尺寸、改变图层在屏幕上的位置、应用旋转形变或者改变它的透明度。通过 Core Animation,初始化一个动画通常和改变属性的值一样简单,但是你也可以创建一个动画,然后显式的设置动画参数。

关于创建高级动画的更多信息,可以参见高级动画技巧

图层属性的简单更改添加动画

你可以根据你的需求执行简单的隐式动画或者显式动画。隐式动画使用默认时间和动画属性去执行动画,而显式动画需要你去配置你使用的动画对象的参数。所以隐式动画更适用于你想执行一个没有大量代码的动画改变,而且默认的时间对你是适用的。

简单动画包含改变图层的属性以及让 Core Animation 随着时间使这些改变动画执行。图层定义了很多影响视觉外观的属性。改变这些属性是一种动画改变图层外观的方式。例如,将图层的透明度从 1.0 改为 0.0 会造成图层淡出透明。

重要:虽然有时你可以直接使用 Core Animation 的接口去执行图层支持视图的动画,但这通常需要额外的步骤。关于如何共同使用 Core Animation 和图层支持视图的更多信息,请参见如何动画图层支持视图

你只需要更新图层对象的属性即可触发隐式动画。当在图层树上修改图层对象时,这些对象会立即反应你的改变。然而,图层对象的视觉外观不会立即改变。Core Animation 会将你的修改当做一个触发器,去创建和安排一个或者多个隐式动画以供执行。因此,像 例3-1 的改变会造成 Core Animation 为你创建一个动画对象,然后在下次更新循环中开始运行这个动画。

例 3-1 隐式改变动画

theLayer.opacity = 0.0;

使用一个动画对象来制作显式改变,创建一个 CABasicAnimation 对象,用该对象配置动画参数。在将动画添加到图层之前,你可以设置动画的起止值,改变持续时间,或者改变动画的其他参数。例3-2 展示了如何通过使用动画对象淡出图层。当创建对象时,你指定你想动画的属性的关键路径,然后设置动画参数。为了执行该动画,你可以使用 addAnimation:forKey: 方法将它添加到你想执行动画的图层上。

例 3-2 显式改变动画

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"];
 
// 将图层的实际数据改为结束值
theLayer.opacity = 0.0;

提示:当创建显式动画时,推荐你给动画对象的 fromValue 属性赋值。如果你没给该属性赋值,Core Animation 会使用图层的当前值作为起始值。如果你已经更新了该属性的结束值,可能不会出现你想要的结果。

不像隐式动画,它会更新图层对象的数据值,显式动画不会修改图层树的数据。显式动画仅执行动画。在动画结束时,Core Animation 会从图层移除动画对象,然后使用图层的当前数据值重绘图层。如果你想显式动画的改变是永久的,你必须像前面的例子一样更新图层属性。

隐式动画和显式动画通常在当前 run loop 循环结束后开始执行,并且当前线程必须有 run loop 才能执行动画。如果修改多个属性,或者你对图层添加多个动画对象,所有的属性改变动画会同时执行。例如,你可以在同一时间配置两个动画来实现淡出图层的同时将它移动到屏幕外。然而,你也可以配置动画对象在特定的时间开始。关于修改动画时间的更多信息,可参见自定义动画时间

使用关键帧(Keyframe)动画改变图层属性

基于属性的动画将属性从起始值改变为终止值,CAKeyframeAnimation 对象允许你通过一组目标值来实现动画,它可以是线性的,也可以不是。一个关键帧是由一组目标值和目标值何时执行的时间组成。在最简单的配置中,你可以使用数组指定值和时间。例如改变图层的位置,你可以通过下面的路径改变。该动画对象使用你指定的关键帧,通过在给定时间段内从一个值插值到下一个值来构建动画。

图 3-1 展示了一个图层位置属性的 5秒动画。该位置动画跟随一个由 CGPathRef 数据类型指定的路径。该动画的代码如例3-3所示。

图 3-1 一个图层位置属性的 5秒动画
图片动画点此处

例 3-3 展示了实现图3-1 动画的代码。本例中的 path 对象用于定义动画每帧的图层位置。

例 3-3 创建弹跳关键帧动画

// 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 的数据类型(如图层的 anchorPointposition 属性),你也可以指定一个 CGPathRef 数据类型代替。

当指定数组值时,放在数组中元素的数据类型依赖于属性。你可以直接添加一些对象到数组中,但是,这些对象在添加前必须转换为id,所有标量类型或结构都必须由对象包装,例如:

  • 对于 CGRrect 类型的属性(如 boundsframe 属性),将其包成 NSValue 对象。
  • 对于图层的形变属性,将每个 CATransform3D 矩阵包成 NSValue 对象。动画该属性会造成关键帧动画将每一个形变矩阵应用到图层上。
  • 对于 borderColor 属性,在添加到数组之前,将每个 CGColorRef 数据类型转为 id 类型。
  • 对于 CGFloat 类型的属性,在添加到数组之前,将其包成 NSNumber 对象。
  • 当动画图层的 contents 属性时,指定一个 CGImageRef 数据类型的数组。

对于 CGPoint 类型的属性,你可以创建一个 points(包成 NSValue 的对象)的数组,或者你可以使用 CGPathRef 对象去指定一个路径去跟随。当你指定 points 数组时,关键帧动画对象在每个连续点之间画一条直线,并沿着该路径执行。当你制定一个 CGPathRef 对象时,动画在路径的起始点开始动画,跟随它的轮廓,包括任何曲线。你可以使用开放或者封闭路径。

指定关键帧动画的时间

关键帧动画的时间和节奏比基础动画更加复杂,这有几个你可以使用控制的属性:

  • calculationMode 属性定义了计算动画时间的算法。该属性的值影响其他时间相关的属性如何被使用。

    • 线性和立体动画,即动画的 calculationMode 属性值设置为 kCAAnimationLinearkCAAnimationCubic ,它们使用提供的时间信息去生成动画。这些模式给你动画时间最大的控制。
    • 节奏动画(paced animations),即动画的 calculationMode 属性值设置为 kCAAnimationPacedkCAAnimationCubicPaced ,它不依赖由 keyTimestimingFunctions 属性提供的外部时间值。时间值是被隐式计算用来提供动画的恒定速度。
    • 离散动画(Discrete animations),即动画的 calculationMode 属性值设置为 kCAAnimationDiscrete ,使动画属性在没有任何插值的情况下从一个关键帧值跳到下一个关键帧值。此计算模式使用 keyTimes 属性中的值,但忽略 timingFunctions 属性。
  • keyTimes 属性指定应用每个关键帧值的时间标记。该属性仅在计算模式设置为 kCAAnimationLinearkCAAnimationDiscretekCAAnimationDiscrete 的时候被使用。它不会被用于节奏动画。

  • timingFunctions 属性指定每个关键帧片段的时间曲线,该属性会覆盖继承的 timingFunction 属性。

如果你想自己处理动画时间,使用 kCAAnimationLinearkCAAnimationCubic 模式和 keyTimes timingFunctions 属性。keyTimes 属性定义了应用每个关键帧的时间点。所有中间值的定时由定时功能控制,允许你将 ease-in 或 ease-out 曲线应用于每个线段。如果不指定任何定时功能,则定时是线性的。

在执行过程中停止显式动画

动画通常会执行完成,但如果需要,你也可以使用下面的方法提前停止动画:

  • 为了移除图层的单个动画对象,你可以调用图层的 removeAnimationForKey: 方法去移除动画对象。该方法使用 addAnimation:forKey: 方法传递的 key 来识别动画。你指定的 key 不能为 nil。
  • 为了移除图层上所有的动画对象,你可以调用图层的 removeAllAnimations 方法。该方法会立即移除所有动画,并使用当前的状态信息重绘图层。

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

当你移除图层上的动画对象时,Core Animation 会使用图层的当前值来重绘图层。因为当前值通常是动画的结束值,这会造成图层的外观突然闪一下。如果你想图层的外观仍然保持在动画的最后一帧上,你可以使用表现树里的对象来获取最后的值,然后设置在图层树的对象上。

关于临时暂停动画的更多信息,请参见暂停和重启图层动画

同时执行多个动画

如果你想同时对图层应用多个动画,你可以使用 CAAnimationGroup 对象将它们打包在一起。使用组对象可以通过提供单独配置点来简化多个动画对象的管理。应用于组的时间和持续时间值会覆盖单个动画对象中的相同值。

例 3-4 展示了你使用组动画来用同样的持续时间同时执行两个线相关(border-related)的动画。

例 3-4 同时执行两个动画 
// 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: 方法在当前事务中添加一个完成块(completion block)。当事务中所有的动画结束时,事务会执行你的完成块。
  • 给你的 CAAnimation 对象赋值一个代理 ,并实现 animationDidStart:animationDidStop:finished: 代理方法。

如果你想将两个动画串在一起,以实现当一个动画结束时,另一个动画开始的需求。不要使用动画通知。使用动画对象的 beginTime 属性在所需的时间去开始每一个动画对象。为了将两个动画串在一起,你可以第一个动画的结束时间设置为第二个动画的开始时间。关于动画时间值的更多信息,请参见自定义动画时间

如何动画图层支持视图

如果图层属于图层支持视图,推荐的方法是使用基于视图,由UIKit 或 AppKit 提供的动画接口来创建动画。你可以直接使用 Core Animation 接口来制作图层动画,但如何创建动画依赖目标平台。

在 iOS 上修改图层的规则

因为 iOS 的视图底层都有图层,所以视图类直接从图层对象中获取大部分数据。因此,你在图层上的改变也会自动反映到视图对象上。该行为意味着你可以使用 Core Animation 或者视图接口 的任意一种来做你的改变。

如果你想使用 Core Animation 类去初始化动画,你必须从基于视图的动画块内部发出所有 Core Animation 调用。视图类默认禁止图层动画,但在动画块中可以重新启用。所以你在动画块外做的所有改变都是无动画的。例 3-5 展示了如何隐式改变透明度和显式的改变位置的例子。在该例中,myNewPosition 变量是在之前被计算好的,在块中被获取。两个动画在同一时间开始,但是透明度动画在默认的时间执行,而位置动画在动画对象指定的时间里执行。

例 3-5 动画 iOS 的视图

[UIView animateWithDuration:1.0 animations:^{
   // 隐式改变 opacity
   myView.layer.opacity = 0.0;
 
   // 显式改变 position
   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 上修改图层的规则

动画 OS X 上的图层支持视图,最好使用视图本身的接口。你应该极少或者绝不直接修改你的图层支持 NSView 对象的图层。AppKit 会创建配置图层对象,当你 app 运行时管理它们。修改图层可能会造成视图和图层之间的不同步,这会造成意想不到的结果。对于图层支持视图,你的代码一定不要修改图层对象的以下属性:

  • anchorPoint
  • bounds
  • compositingFilter
  • filters
  • frame
  • geometryFlipped
  • hidden
  • position
  • shadowColor
  • shadowOffset
  • shadowOpacity
  • shadowRadius
  • transform

重要:上述限制不适用于图层托管视图。如果你创建了图层对象并将其与视图手动关联,则你有责任修改该图层的属性并保持相应的视图对象同步。

AppKit 的图层支持视图默认禁止隐式动画。图层的动画代理对象自动为你生效隐式动画。如果你想直接动画图层的属性,你可以通过编码将当前 NSAnimationContext 对象的 allowsImplicitAnimation 值改为 YES。再一次说明,你只能对除上述列表以外的属性进行动画。

如果你使用基于约束的布局规则去管理你的视图的位置,记得更新视图约束是你动画的一部分。在配置动画时,必须删除可能干扰动画的任何约束,约束影响对视图的位置或大小所做的任何更改。如果你正在对其中任何一个项进行动画化更改,你可以删除约束,进行更改,然后应用任何需要的新约束。

有关约束以及如何使用它们来管理视图布局的更多信息,请参见Auto Layout 指南

最后