• Core Animation 文档翻译 (第四篇)—让Layer的content动画起来


    前言


    核心动画的基础接口以及为拥有Layer的View做的动画扩展接口,使得为Layer制作复杂动画变得简单化。例如改变Layer的frame的size、改变Layer在屏幕上的position、应用旋转transform、或者改变它的opacity。通过使用核心动画,创建一个动画效果将会变得简单的就像修改属性一样,但是我们也能显式的创建和设置动画参数。
    关于创建更多高级动画可以参见Advanced Animation Tricks(后续会有译文)。


    Layer属性改变的动画


    根据需要,我们可以选择显式或者隐式的执行简单的动画。隐式的动画使用默认的速度调节和动画属性来执行动画;相反,隐式动画需要我们使用核心动画对象配置相应的属性。因此当我们想要使用少量代码和默认定时速度做动画过度改变的时候,隐式动画是个不错的选择。

    简单的动画意味着改变Layer的属性,和让核心动画是这些改变随着时间发生变化。Layers定义了许多能够影响可视化外观的属性,改变这些属性之一就会引起相应的外观动画性的变化;例如,改变Layer的opacity由1.0到0.0将会引起Layer渐渐隐藏(淡出)并变透明。

    重要提示:我们有时可以直接使用核心动画的接口为Layer-backed View(iOS 内的view都可称为此种view,前面章节有说明)做动画,这么做常常需要一些额外的步骤,更多关于如何使用与Layer-backed views结合的动画可以参见如何为Layer-Backed View做动画

    为了触发隐式动画,我们只需要去更新Layer对象的属性。当更改图层树上的Layer对象的时候,Layer的属性将会立刻变更为我们调整的值,然而Layer对象的外观不会立刻改变为我们设置的值;相反,核心动画使用我们调整的值作为触发器来创建和规划一个或多个隐式动画,然后核心动画并执行这些动画。因此,像代码3-1中调整属性值将会引起核心动画创建动画对象,并规划那个动画到下个更新循环开始执行。

    Listing 3-1 Animating a change implicitly

    theLayer.opacity = 0.0;
    

    为了制作和隐式动画一样的动画,我们也可通过显示的使用动画对象,创建一个CABasicAnimation 对象和使用那个对象配置动画参数。在将动画对象添加到Layer上之前,我们可以为动画对象设置开始和结束值、改变动画周期,或者任何其他的动画参数。代码3-2展示了如何通过动画对象将Layer渐隐。当创建动画对象后,我们指定要做动画的属性的Key path,然后设置动画参数。为了执行动画,我们使用addAnimation:forKey:方法将动画对象添加到Layer上。

    Listing 3-2 Animating a change explicitly

    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;
    

    提示:当创建显示动画的时候,建议为动画对象设置fromValue属性值,如果我们不指定这个值,核心动画将会使用Layer的当前值作为动画的开始值,如果我们将这个属性值设定的和动画的最终值一致,那么将不会产生我们想要的动画效果。

    和隐式动画不一样的是,隐式动画将会更改Layer对象的值,显示动画不会修改图层树上的数据。显示动画仅仅产生动画效果。在动画的结束后,核心动画将会从Layer上移除动画对象并使用Layer的当前值重新绘制。如果我们想要显示动画的改变发生到Layer上,那么我们必须也更新Layer的属性,就像3-2中指出的一样。

    隐式和显示动画通常在当前runloop循环结束后就开始执行,为了动画对象的执行,当前线程必须有一个runloop。如果我们改变多个属性,如果为Layer添加多个动画对象,所有这些属性将会同时发生动画。例如我们通过同时设置两个动画,就可以使得隐藏一个Layer和将Layer移动到屏幕外面的同时发生。我们一可以设置动画对象在某个特殊的时间点开始。关于更多的修改动画的时间函数参见 Customizing the Timing of an Animation(后续会有译文)。

    使用关键帧动画改编Layer的属性


    属性动画通过改编属性从开始值到结束值发生动画, CAKeyframeAnimation 对象将会通过设定线性或者非线性目标值点集合的方式制作动画。一个关键帧动画由目标集合点,和与单个目标点对应的时间点的集合组成。最简单的配置就是,我们仅仅通过使用数组指定这两种值。对于改变Layer的position来说,我们也可以指定它沿着path发生变化。动画对象取我们指定的关键帧并通过一个值到下一个值在给定的时间片刻上进行插值建立动画。

    图3-1显示了Layer的position属性5s的动画。Position是沿着一条path做的动画,就是使用CGPathRef数据类型制定的path。对应的代码见代码3-3。

    Figure 3-1 5-second keyframe animation of a layer’s position property

    代码3-3展示了如何实现图3-1中动画的代码。在这个例子中,path对象是被用来定义每帧动画中Layer的position。

    Listing 3-3 Creating a bounce keyframe animation

    // 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数据类型的时候(例如Layer的anchorPoint和position属性),我们也可以指定CGPathRef数据类型代替。

    当指定数组值的时候,数组中应该放的数据类型取决于要做动画的属性。我们可以将某些对象直接添加到数组中;但是有些类型必须先转换到id对象类型在添加之前,所有的标量类型和结构体必须先包装成对象,例如:

    • 对于对应CGRect的属性(例如bounds和frame属性),需要一NSValue对象包装每个值。
    • 对于Layer的transform属性,则需要将CATransform3D矩阵包装为NSValue对象。动画这个属性引起关键帧动画按顺序将每个transform矩阵变换应用到Layer上。
    • 对于borderCorlor属性,在加入数组前,需要将每个CGColorRef数据类型映射为id类型。
    • 对于对应CGFloat类型的属性,再加入数组前,需要将每个值包装为NSNumber对象。
    • 当要为Layer的contents属性做动画的时候,数组中需要添加CGImageRef类型的数据。

    对于对应CGPoint数据类型的属性,我们可以创建point的数组(先包装成NSValue对象)或者使用CGPathRef对象指定动画的路线。当我们指定point的数组的时候,关键帧动画对象创建的动画将会走直线在相邻的点。当我们指定CGPathRef对象,动画从路径的起点开始,并沿着他的轮廓走,如果是曲线,它也能够沿着曲线的路径走。我们也可以使用非闭合或者闭合的路径。

    指定关键帧动画的时间函数


    关键帧的时间函数和空间位置是比基本动画(basic animations)更复杂的,这里有几个供控制它们的属性:

    • calculationMode属性用来指定计算动画函数的算法。它的值将影响其他和时间相关的属性如何变化。
      • 线性和多阶动画——当动画calculationMode属性被设置为kCAAnimationLinear 或 kCAAnimationCubic时,将启用被提供的时间(timeing)的信息生成动画,这种模式将会为我们提供对于动画时间函数最灵活的控制。
      • 平缓的动画——当动画calculationMode属性被设置为kCAAnimationPaced或kCAAnimationCubicPaced时,动画将不依赖keyTimes或timingFunctions属性额外提供的时间函数值。相反的时间函数的插值将会以一个常量速度隐式的计算得出。
      • 离散动画——当动画calculationMode属性被设置为kCAAnimationDiscrete时,此时动画将会在没有任何插值的情况下,从一个关键帧跳到下一个关键帧。这个计算模式使用keyTimes属性里面的值,但是忽略timingFunctions属性。
    • keyTimes 属性指定时间标记,对应于这些时间标记值会应用到每一帧的值。仅仅当calculationMode属性被设置为 kCAAnimationLinear, kCAAnimationDiscrete, or kCAAnimationCubic,这个属性才会被才用。对于平缓动画它是无用的。
    • timingFunctions属性将使用时间函数曲线制作每一帧。(该属性将会取代父类的timingFunction属性。)

    如果我们想要自己控制时间函数,需要采用 kCAAnimationLinear或kCAAnimationCubic 模式和keyTimes以及timingFunctions属性。keytimes属性指定应用于对应帧值的时间点。timeingFunctions用于控制所有时间的插值,它将允许在每个片段我们应用缓入或者缓出。如果我们不指定timingFunctions时间函数将会是线性的。

    移除显示的动画(当显示动画在执行过程中)


    正常情况下动画会运行到动画完成,但是如果有需要,我们能够通过以下方式提前移除它们:

    • 从Layer上移除单个动画对象,调用Layer的removeAnimationForKey: 方法以便移除动画对象。这个方法需要使用之前被提供到 addAnimation:forKey:方法的key,因为再添加动画的时候key就在这个Layer上对应到该动画对象。key不能为nil。
    • 从Layer上移除所有的动画对象,只需要调用Layer的removeAllAnimations方法。这个方法将会立即移除所有正在进行的动画并使用当前状态信息重绘Layer。

    注意:我们不能从Layer直接移除隐式动画。

    当我们从Layer上面移除动画的时候,核心动画将会响应——使用Layer当前的值重绘Layer。因为通常是动画的最终值(end values),这可能引起Layer的样貌发生跳跃性的变化。如果我们想要Layer的样貌保持在动画的最后一帧,我们可以使用呈现树上的Layer对象获取最终值(如果是移除操作也可以称为,动画当前值)并设置他们这些值到图层树。

    更多关于临时暂停动画可参见代码5-4(后续会有译文)。

    将多个调整绑定在一起做动画


    如果我们想要将多个动画同时应用到Layer对象,我们可以将他们绑定在一起通过使用CAAnimationGroup对象,group对象可以以简单的配置简化多个动画对象的管理。设置到group对象的时间函数和duration值将会覆盖单个动画对象对应的值。

    代码3-4展示了如何使用一个group对象以相同的时间函数和相同时间周期的实现两个边框相关的动画。

    Listing 3-4 Animating two animations together

    // 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"];
    

    Transaction对象能够提供将动画组合在一起的更高级方式。Transcation通过允许我们创建嵌套的动画集和为每个动画关联不同动画参数,也因此Transcation更灵活,更多关于如何使用Transaction对象,可参见 Explicit Transactions Let You Change Animation Parameters(后续会有译文)。

    获悉动画的开始和结束时机


    核心动画提供获取动画开始和结束事件的支持。这些通知对于动画辅助任务来说是很好的时间点。例如我们可以借助获取到开始通知时去设置一些相关联的状态信息并使用对应的结束通知撤销这些状态。

    有一下两种不同的获取方式:

    • 添加完成的block到当前的transaction通过使用setCompletionBlock:方法。当transaction中所有的动画结束的时候,transaction就会执行我们的完成block。
    • 为CAAnimation对象关联代理并实现 animationDidStart:和animationDidStop:finished:代理方法。

    如果我们想要去将两个动画衔接在一起,实现当一个结束另外一个就开启,不要使用动画通知,相反的应该使用动画对象的beginTime属性去开启每一个动画在期望的时间点。为了将两个动画前后衔接在一起,设置第二个动画的start为第一个动画的end时间。更多关于动画和时间值的信息参见Customizing the Timing of an Animation(后续会有译文)。

    如何为Layer-Backed View做动画


    如果Layer属于Layer-backed view,动画创建的推荐方式是使用通过UIKit或AppKit提供的基于View的动画接口。通过使用核心动画接口,有许多方式可以为Layer做动画,但是如何创建这些动画取决于所在的目标平台(iOS 、OSX)。


    在iOS上修改Layer的规则


    因为iOS中的view一直都有一个内在的Layer,UiView类直接从layer对象直接获取许多数据,这就导致我们为Layer做的调整也会在View对象上自动提现出来,这就意味着我们可以使用核心动画或者UIView的接口来实现这些调整。

    基于Layer-backed View,如果我们想要使用核心动画创建动画,我们必须在基于view的动画block内部发起所有的核心动画的调用。UIView类默认禁用了Layer动画,但是在动画bolcks中可以激活,因此任何在bolcks外的Layer调整将不会发生动画。代码3-5展示了基于Layer-backed View内如何为Layer制作opacity隐式动画,和position显示动画。在这个例子中,myNewPosition变量是被提前计算并被block捕获到的。两个动画在同一时间开始,但是opacity动画一默认的时间函数运行,position动画以他动画对象所指定的时间函数运行。

    Listing 3-5 Animating a layer attached to an iOS view

    [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"];
    }];
    

    作为动画的一部分,记着更新View的约束


    如果使用基于约束的布局规则管理views的position,那么作为配置动画的一部分,我们必须移除任何影响动画的约束。约束影响任何我们为view的position或size做的调整,他们常常影响view之间以及view和他们子view的关系;如果我们为这些带有约束的view做动画,我们需要移除这些约束,并应用我们需要的新的约束。

    更多关于约束和如何使用他们管理views的layout的信息参见Auto Layout Guide

  • 相关阅读:
    unknown: Are you trying to mount a directory onto a file (or vice-versa)? Check if the specified host path exists and is the expected type.
    Go的学习笔记之Channel发送和接收
    聊聊 PC 端自动化最佳方案
    一文彻底搞懂快速幂(原理实现、矩阵快速幂)
    一次core.<number> 文件占满磁盘空间的处理过程
    博文目录
    备忘录:C#获取微信小程序的云数据库中数据
    T-SQL——关于跨库连接查询
    .NET异步程序设计——给线程传递数据
    自研 Pulsar Starter:winfun-pulsar-spring-boot-starter
  • 原文地址:https://www.cnblogs.com/zhouyubo/p/8410871.html
Copyright © 2020-2023  润新知