JNWSpringAnimation

JNWSpringAnimation是Jonathan Willing,一个Mac和iOS开发者,写的一个很棒的动画框架。要理解它为什么棒,让我们先回过头再一次谈谈Core Animation。

如我之前所说,Core Animation的时间曲线是由三维贝塞尔曲线定义的。你可以告诉一个动画去使用线性、淡入、淡入淡出或者淡出时间曲线,或者你可以手动设置曲线的控制点,就如你可以在CSS动画中使用三维贝塞尔动画时间函数。

然而,你不能用这种方式定义弹簧动作动画曲线,因为他们的形状太高级了。所以你可以怎么做呢?我们可以创建类似这个的其他什么动作吗?


JNWSpringAnimation - 图1

这种类型的弹簧动画曲线无法通过简单的三维贝塞尔曲线来创建。


苹果还给开发者提供了一种称为CAKeyframeAnimation的特殊的动画类别,用来代替无忧的像我们之前讨论的动画(你定义开始和结束值并让Core Animation为你计算中间值)

关键帧动画是指你给系统提供一系列的值(用来改变物体的位置、旋转、比例等等。)然后它会根据你定义的时间间隔一步步地改变你列出来的值。你可以使用关键帧动画来创建多重部分的动画,其中一些物体在开始的几秒移动到一个位置,然后移动到另一个方向。你还可以改变每段的时间曲线。

JNWSpringAnimation工作的方式就是定义你的弹簧的关键属性,例如阻尼、刚度和质量,然后告诉它你要动画的属性是什么,JNWSpringAnimation就会为你创建一个包含你的动画的大量值的CAKeyframeAnimation,在到达最终值前弹簧动作曲线中的每1/60秒都有值。接着,你要做的只是将这个关键帧动画添加到你想要动画的CALayer中去,(可以是它自己的layer,或者是一个UIView的layer属性),Core Animation会一步步地执行每个关键帧,每秒60次,直到它到达最终位置动画就结束了。系统不需要知道你是如何生产关键帧列表中的所有值的,也不需要知道它会产生什么类型的动作,它只是盲目地在每一步按照你想要的方式改变动画属性。

详细地说的话,JNWSpringAnimation获取你给它的用来描述你想要在动作中模仿的弹簧的值,并用代码绘制真实的弹簧曲线。然后生成所有的动画关键帧值,它本质上在曲线上每次只走非常小的一步来定义曲线上每1/60秒的值。那就是为物体移动过程中每个位置的值。完成这个过程会非常快,因为要在动画开始前就全部准备好。

让我们看一些使用JNWSpringAnimation来使用不同类型的弹簧动作并有不同属性的动画的例子。在我们的第一个例子中,我们还是要动画之前同样的红色的球,使用我们定义的弹簧管理的弹簧效果将它的尺寸从1提升到2.0倍。

  1. JNWSpringAnimation *scale =
  2. [JNWSpringAnimation animationWithKeyPath:@"transform.scale"];

首先,我们定义我们的JNWSpringAnimation对象,一个动画的新实例,命名为scale。我们使用定义的初始化器并将关键路径置为“transform.scale”,不过这表示什么呢?这个关键路径就是指我们想要动画的属性或值。它是视图下的CALayer对象的一个属性,也就是我们实际打算使用关键帧动画的动画。还记得CALayer是Core Animation中真正的主力么?这是因为当使用类似关键帧动画的动画时,你会将其放置到你想要动画的layer上,而且一般这个layer是UIView对象的组成部分。想要动画一个展示照片的UIImageView?动画它的layer。想要动画一个UIButton?动画它的layer。

基于此我们有一个知道它要作用的属性是什么的JNWSpringAnimation对象。是时候通过调整一些弹簧的属性来调整这个动画的动作了。

  1. JNWSpringAnimation *scale =
  2. [JNWSpringAnimation animationWithKeyPath:@"transform.scale"];
  3. scale.damping = 9;
  4. scale.stiffness = 100;
  5. scale.mass = 2;

阻尼、刚度和质量是我们要调整获得我们的球动画的完美的动作的三个重要的弹簧属性。我如何触碰这些值呢?很简单!JNWSpringAnimation也包含了一个Mac app让你交互式地处理这三个值并直接看到结果。

还有一个要注意的重点是你没有在JNWSpringAnimation中像之前在基于block的UIView动画中一样设置持续时间。阻尼、刚度和质量三个属性或产生一个一旦系统的力学到达最终值就会在最终值安定下来的弹簧动作。如果你想要缩短你动画的持续时间,就需要调整弹簧的属性才能快一点到达最终值,一般来说会增加弹簧的阻尼属性。通过非人工地操作弹簧动作的整体持续时间,就可以让你在动画的物体想在自然世界中伴随真实弹簧管理其整个动作和持续时间一样移动。这就是JNWSpringAnimation创建的动画看起来非常自然和有趣的原因。

我们刚才将一个红色的球作为动画示例,弹簧的动作并不是关键的,我们何时开始用下一节中定义的动作动画实际的界面元素,以及我们想要实现什么才是关键。这就是为什么一个类似JNWSpringAnimation提供的交互式的弹簧定义的app很重要,当你创建你的动画时它节省了大量的时间。


JNWSpringAnimation - 图2


一旦你完成你动画的完美动作,你只需要插入阻尼、刚度和质量值到你的动画代码中,然后无论你动画什么都会和你之前正确的值的动作一样。

我们也需要让JNWSpringAnimation对象知道我们想要动画属性的开始和结束值是什么。这是用来绘制弹簧和关键帧值的。

  1. JNWSpringAnimation *scale =
  2. [JNWSpringAnimation animationWithKeyPath:@"transform.scale"];
  3. scale.damping = 9;
  4. scale.stiffness = 100;
  5. scale.mass = 2;
  6. scale.fromValue = @(1.0);
  7. scale.toValue = @(2.0);

现在我们的JNWSpringAnimation对象知道了它的开始值和结束值,以及我们想要模仿的弹簧的准确属性,我们现在可以把它添加到我们想要移动的CALayer上去了。在我们的例子中,我们要将它添加到redBall上去。

  1. JNWSpringAnimation *scale =
  2. [JNWSpringAnimation animationWithKeyPath:@"transform.scale"];
  3. scale.damping = 9;
  4. scale.stiffness = 100;
  5. scale.mass = 2;
  6. scale.fromValue = @(1.0);
  7. scale.toValue = @(2.0);
  8. [redBall.layer addAnimation:scale forKey:scale.keyPath];

这个名为scale的动画现在根据给定的关键路径(又称为我们想要在layer上改变的值)被添加到redBall.layer中了。我们可以将“transform.scale”传入到forkey:的参数中,但我们也可以只传入准确的我们创建的动画关键路径,这样我们就不会混淆JNWSpringAnimation的关键路径和我们要协调动画时使用的关键路径。在这个例子中,我们创建动画使用的关键路径“transform.scale”可以直接写成scale.keyPath.

如果我们创建并运行我们的代码,这就是产生的动画。


JNWSpringAnimation - 图3


现在如果你想要在Swift工程中使用JNWSpringAnimation,由于你是使用一个Objective-C框架,你需要使用一些称为“桥街头”的东西让Xcode知道你想要在你的Swift代码中使用非Swift的框架。所以首先,我拖动称为JNWSwift的我需要使用JNWSpringAnimation的.h和.m文件到Xcode中的我的Swift工程中(包含到Xcode工程文件中)。Xcode就会询问是否要创建一个桥街头,我选择要,这就是哪个特殊文件的内容。

  1. // This is within the JNWSwift-Bridging-Header.h file
  2. // that was automatically created for me
  3. # import "JNWSpringAnimation.h"
  4. # import "NSValue+JNWAdditions.h"

任何添加到这个特殊的桥街头文件中的Objective-C头文件都会被设为Swift可见,这样你就可以使用Swift来交互它们的Objective-C函数。酷的地方在于当你想要在你的Swift代码中使用它们时,你不需要有任何import说明,Xcode会处理它。

当设置好桥街头之后,你就可以进入你的Swift代码中并开始处理你想要操作的对象,在这个例子中,就是JNWSpringAnimation。这里是我用Swift写的创建与上面的例子一样的动画的代码,依然是使用JNWSpringAnimation

  1. let scale = JNWSpringAnimation(keyPath: "transform.scale")
  2. scale.damping = 9.0
  3. scale.stiffness = 100
  4. scale.mass = 2
  5. scale.fromValue = NSNumber(float: 1.0)
  6. scale.toValue = NSNumber(float: 2.0)
  7. redBall.layer.addAnimation(scale, forKey: scale.keyPath)

它看起来和上面的Objective-C代码非常接近,但是当然没有包含调用方法的方括号,并且如果你写过JavaScript的话,它看起来与其非常相似。

这就是Swift代码和Objective-C代码会创建的一样的动画。


JNWSpringAnimation - 图4


球的动画是从其原始尺寸增加到两倍大然后立即跳回其原始尺寸。这确实是我们上面所写代码的准确行为,但球在动画完成后跳回到起原始尺寸的原因却是需要重点理解的。

Core Animation在任何给出的时间内会维持三个你的层的集合或者树。每个层树都会在你的界面显示过程中扮演一个重要的角色。

  1. 模型层树。模型层树反映了一个layer静止不动画时的所有属性。比如说,当我们设置redBall.layer.cornerRadius到50来让它变成球时,我们就是在模型层上设置属性。模型层上的值是你的app交互的最多的。任何时候你改变一个layer的值时,都在更新它的模型层。模型层上的值不会在动画过程中改变,并会持续反应你添加动画前的值。
  2. 表现层树。表现层树反映了动画时layer上的属性,并包含了运行动画时的变化值。你不应该在这个层树上设置任何值,通常都是在想要准确了解一个layer在哪或是其在动画过程中的行为时通过查看当前的动画值来与表现层树交互。
  3. 渲染树。渲染树时苹果的私有值集合,用来执行渲染到屏幕上的实际绘制。你不需要与其交互或知道这些值。

当我们添加一个动画到layer的时候,动画会在layer 的表现树上操作这些值,当动画完成的时候,动画会自动从layer移除,并且表现树的值会变回模型树的值,因为这些值反映了真实、静止的layer属性。

如果我们想要layer 的属性更新为动画的最终值,我们需要明确地说明。对,我知道折痕奇怪,但因为Core Animation允许开发者构建非常多类型的动画,它们需要支持有些时候你确实想要你的动画被移除然后layer回到其原始位置的使用案例。

这里是在末尾添加了决定性的一行后的代码示例。

  1. JNWSpringAnimation *scale =
  2. [JNWSpringAnimation animationWithKeyPath:@"transform.scale"];
  3. scale.damping = 9;
  4. scale.stiffness = 100;
  5. scale.mass = 2;
  6. scale.fromValue = @(1.0);
  7. scale.toValue = @(2.0);
  8. [redBall.layer addAnimation:scale forKey:scale.keyPath];
  9. redBall.transform = CGAffineTransformMakeScale(2.0, 2.0);

通过手动地设置redBall的transform属性为两倍比例,并匹配动画的最终值,动画会在移除的时候将实际的layer上的transform值无缝更新为匹配动画的最终值。现在球就会维持在2倍大小了。GIF依然会回到起始位置,不过在代码中球不会。


JNWSpringAnimation - 图5


你可能会想,我们使用基于block的UIView动画时并不需要处理这些,完全正确。UIView上基于block的动画方法是一个创建简单动画的更方便的方式,因为它们会自动保留最终值而无需去设置。当然了,你会被默认的过渡动作或者iOS 7提供的简单的弹簧动作所限制。如果你想要完整控制你的动画并想要细致地调整你的弹簧属性,你就需要奔向真实的CAAnimation对象,JNWSpringAnimation就是其中之一。

使用类似JNWSpringAnimation弹簧动画框架的真实诱惑是你可以获得对你弹簧力学的精确控制,所以让我们看看更多使用不同弹簧动作的红球的例子。

  1. JNWSpringAnimation *scale =
  2. [JNWSpringAnimation animationWithKeyPath:@"transform.scale"];
  3. scale.damping = 13;
  4. scale.stiffness = 540;
  5. scale.mass = 11;
  6. scale.fromValue = @(1.0);
  7. scale.toValue = @(2);
  8. [redBall.layer addAnimation:scale forKey:scale.keyPath];
  9. redBall.transform = CGAffineTransformMakeScale(2.0, 2.0);

这些弹簧属性产生了一个更慢、更深的移动。


JNWSpringAnimation - 图6


下一个例子没有任何弹性,但有一个指数衰减动作来慢慢地到达最终值。这是模仿过阻尼的弹簧系统。这个动作类似于简单的淡出动作,但到达最终值时会更加的轻缓。我们也可以通过操作阻尼和刚度属性来调整其到达最终值的速度。

  1. // 所有其他部分的代码都是一样的
  2. scale.damping = 6;
  3. scale.stiffness = 6;
  4. scale.mass = 1;

JNWSpringAnimation - 图7


这里是三个并排的球,第一个的阻尼为6、刚度为6、质量为1。第二个阻尼为15、刚度为15、质量为1。第三个阻尼为30、刚度为30、质量为1。他们都是指数衰减型的动作,但他们到达最终值的速度不同。


JNWSpringAnimation - 图8


我大部分展示的比例变更动画,但这不意味着你不能动画layer的更多属性!这里就是使用JNWSpringAnimation来使用弹簧动作旋转一个layer的示例。

  1. JNWSpringAnimation *scale =
  2. [JNWSpringAnimation animationWithKeyPath:@"transform.rotation"];
  3. scale.damping = 10;
  4. scale.stiffness = 100;
  5. scale.mass = 3;
  6. scale.fromValue = @(0);
  7. scale.toValue = @(M_PI_2);
  8. [redBall.layer addAnimation:scale forKey:scale.keyPath];
  9. redBall.transform = CGAffineTransformMakeRotation(M_PI_2);

由于这是一个旋转动画,开始和结束值是由弧度定义的角度。我们使用便利的函数CGAffineTransformMakeRotation()来设置模型层的最终值为2π。


JNWSpringAnimation - 图9


接下来我们要设置弹簧的阻尼和刚度为如之前展示的3个层示例一般会导致指数衰减类型动作的类似值。我们会动画其位置,而不是layer的比例。

  1. JNWSpringAnimation *scale = [JNWSpringAnimation
  2. animationWithKeyPath:@"transform.translation.x"];
  3. scale.damping = 7;
  4. scale.stiffness = 7;
  5. scale.mass = 1;
  6. scale.fromValue = @(0);
  7. scale.toValue = @(400);
  8. [redBall.layer addAnimation:scale forKey:scale.keyPath];
  9. redBall.transform = CGAffineTransformMakeTranslation(400, 0);

我们要动画的位置关键路径为“transform.translation.x”,是从左到右的位置——x坐标。我们会将其向右移动400个像素,所以toValue是400,要设置最终值并保持球在我们动画的地方,我们需要设置球的transform到CGAffineTransformMakeTranslation(400, 0)。这个函数是一个改变视图的变化矩阵的平移组件的简单方式,它接收两个参数,x和y的变化。


JNWSpringAnimation - 图10


当然,我们可以一次性动画很多属性。这里是一个同时动画比例和旋转的代码。看你能不能发现与单个属性动画的区别。

  1. JNWSpringAnimation *scale = [JNWSpringAnimation
  2. animationWithKeyPath:@"transform.scale"];
  3. scale.damping = 9;
  4. scale.stiffness = 9;
  5. scale.mass = 1;
  6. scale.fromValue = @(1);
  7. scale.toValue = @(4.0);
  8. [redBall.layer addAnimation:scale forKey:scale.keyPath];
  9. redBall.transform = CGAffineTransformScale(redBall.transform, 4.0, 4.0);
  10. JNWSpringAnimation *rotate = [JNWSpringAnimation
  11. animationWithKeyPath:@"transform.rotation"];
  12. rotate.damping = 9;
  13. rotate.stiffness = 9;
  14. rotate.mass = 1;
  15. rotate.fromValue = @(0);
  16. rotate.toValue = @(M_PI);
  17. [redBall.layer addAnimation:rotate forKey:rotate.keyPath];
  18. redBall.transform = CGAffineTransformRotate(redBall.transform, M_PI);

第一个动画是一个比例变化,从1.0到4.0变成四倍大小。与之前的例子的代码相比第一个不同是当我们在添加动画后设置模型层的实际变化值时(所以它才能保持最终值。)不是使用CGAffineTransformMakeScale()函数来进入新的比例,而是使用了名称相似容易混淆的CGAffineTransformScale()函数并接收了三个参数。CGAffineTransformMakeScale()(包含make在其中)假设你想改变到的变化矩阵是常规、默认、未触摸的恒等变换的变化矩阵,其刚刚创建了此时的视图。

另一方面接收三个参数的CGAffineTransformScale(),第一个参数是你想要改变的起始的变化矩阵。这可以是恒等变化或者一个已经有了一些操作的变形,比如已经被旋转了、伸缩了、平移了等等。我们使用这个函数并且将视图当前的变形作为第一个参数的原因是我们正在添加两个动画到其中并且它们都会操作layer的变形矩阵。如果我们使用CGAffineTransformMakeScale(),就会影响所有的第二个动画的变形调整,使用开始的恒等变换,而不是最近更新的第二次动画设置的layer变形。通过引入当前的变形值,我们可以确保对我们的操作使用最近的值,而这就会包含第二个动画的最终值。

第二个动画会旋转我们的对象π的角度。让我们看看包含比例和旋转变形的动画看起来什么样。


JNWSpringAnimation - 图11


很酷对吧,我们不需要对每个动画设置同样的时间曲线;因为这是两个单独的动画对象,我们可以单独地控制每个弹簧的属性。这里是一个比例和旋转动画的例子,其比例弹簧使用了一个指数衰减类型的弹簧动作(没有弹性),而旋转动画动作非常有弹性。


JNWSpringAnimation - 图12


这里是另一个同时添加两个动画的例子。这次它组合了一个位置(平移)动画和一个比例变形。


JNWSpringAnimation - 图13


我不知道你如何,但我对于仅仅动画这些色块已经有点无聊了。我认为是时候进入一些使用JNWSpringAnimation来实现弹簧动作动画的真实世界、真实app的例子了。