简单动画

优质
小牛编辑
137浏览
2023-12-01

是时候写一些代码了。让我们先添加一个简单的UIView对象到屏幕上并设置它的圆角。我们要把它添加到我们的主窗口上时因为它是一个快速的例子,但在真实的app界面中你需要添加到管理当前界面的视图控制器中。

UIView *redBall = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 100, 100)];
redBall.backgroundColor = [UIColor redColor];
redBall.layer.cornerRadius = 50;
[self.window addSubview:redBall];

我们创建了一个新的UIView对象并设置了它的框架来定义它在屏幕上的的X和Y坐标,以及它的宽和高,然后将其添加到屏幕中。我们还将它的背景颜色属性设为了红色。如我前面所说,要让一个视图的角变为圆角,你需要获取它的layer,所以我们设置它的layer.cornerRadius值为50,这是宽度的一半。如果你在你的app的delegate类的-application:didFinishLaunchingWithOptions方法中添加这个代码,就可以在运行后的屏幕上看到它。

这里是和上面一样的功能,但是是Swift而不是Objective-C写的。你可以打开Balls In Swift Xcode工程导出这个例子的Swift版本。

var redBall = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 100))
redBall.backgroundColor = UIColor.redColor()
redBall.layer.cornerRadius = 50
view.addSubview(redBall)

我们在屏幕上有了一个红色的球!很激动,我知道。现在我们让它动起来。

iOS提供了一些内置的技术来创建动画:创建并添加一个CAAnimation到我们之后要讨论的layer中,或者使用简单的基于block的动画方法来动画化UIView的值。让我们创建一个基于block的动画来将圆从1.0扩大到2.0倍,这会让它变成原来的两倍大。

UIView *redBall = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 100, 100)];
redBall.backgroundColor = [UIColor redColor];
redBall.layer.cornerRadius = 50;
[self.window addSubview:redBall];

[UIView animateWithDuration:.5 delay:0
    options:UIViewAnimationOptionCurveEaseInOut animations:^{
    redBall.transform = CGAffineTransformMakeScale(2.0, 2.0);
} completion:NULL];

这个UIView上称为 +animateWithDuration:delay:options:animations:completion: 的类方法时UIView提供的多种动画方法之一。第一个安排,持续时间(duration),被设为半秒,第二个安排,延迟(delay),被设为0。

选项(options)参数让我们设置想要使用的动画类型(它还允许你设置一大串其他选项例如在动画完成后自动反转),所以这个简单的测试中我们选择UIViewAnimationOptionCurveEaseInOut来将时间设为简单的淡入淡出时间曲线。其他的时间曲线选项还有线性、淡入和淡出。

接下来,动画(animations)安排使用了一个block代码作为值,在block中你可以设置你要动画的视图的最终状态。Core Animation会自动在球的当前尺寸值和你的最终值之间更改来产生一个平滑的动画。这一次,我希望动画能最终让球变成两倍大,所以我设置了球的transform属性为一个新值。transform是一个表述了视图中每个像素根据一些线性代码应该改变的值的矩阵。有很多方式来操作一个视图的transform(尺寸、旋转、位置),所以苹果提供了很多函数来改变你感兴趣的值,在我们的例子中,是尺寸。将transform属性设为`CGAffineTransformMakeScale(2.0, 2.0)意味着我们想要其他所有的值都保持不变,除了尺寸,我们想让尺寸变为原来的两倍。

最后,我们不需要在动画完成后运行任何代码,所以我么你设置完成(completion)的安排为NULL。这里是你再次运行代码后会看到的样子。GIF会回到原始的样子但实际上球并不会。

这里是Swift下同样的代码:

UIView.animateWithDuration(0.5, delay: 0,
    options: UIViewAnimationOptions.CurveEaseInOut, animations: {
    redBall.transform = CGAffineTransformMakeScale(2.0, 2.0)
}, completion: nil)

在block代码块中我们可以改变很多视图相关的属性,它们会在同一个持续时间内一起动画。现在让我们再添加一些值的改变到动画block中来丰富你使用基于block的动画可以操作的内容。

UIView *redBall = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 100, 100)];
redBall.backgroundColor = [UIColor redColor];
redBall.layer.cornerRadius = 50;
[self.window addSubview:redBall];

[UIView animateWithDuration:.5 delay:0
    options:UIViewAnimationOptionCurveEaseInOut animations:^{
    redBall.backgroundColor = [UIColor greenColor];
    redBall.transform = CGAffineTransformConcat(
        CGAffineTransformMakeScale(2.0, 2.0),
        CGAffineTransformMakeTranslation(75, 0));
} completion:NULL];

在我们现在的动画block中,我们做了很多事情。首先,我们将视图的背景色从原始的红色改成了绿色。Core Animation会帮我们修改它并处理中间的颜色。接下来,我们改变了两个关于视图的transform的内容:它的尺寸和平移。平移的更改会将视图上、下、左、右移动。在我们的例子中,我们会将它右移75个像素。我们使用了CGAffineTransformConcat()函数来将两个更改操作合成了一个,这样就可以分配一个简单矩阵转化给视图。你可以手动构建转变矩阵来包含尺寸和平移更改到一个数据结构中,但我发现让iOS来帮我们结合多个单独的转变到一个最终转变会容易一些。

到目前为止有意义吗?围绕转变矩阵的数学有一点复杂和困难,但是苹果让它变得亲近,即使你没有线性代数的背景。动画一个视图的转变矩阵是发动动画最有效的方式之一。

##从iOS 7中的弹簧动画开始 从iOS 7开始,苹果在他现有的一套动画方法中添加了类弹簧的动画能力。实际上,他们还添加了很多东西;他们的UIKit Dynamics 框架是一个整合到了UIKit中的完整的物理引擎,允许你添加地心引力、弹簧附着、动力等到你的界面元素中。

让我们看看一个iOS 7中介绍的更改了的基于block的动画方法,它现在增加了一些额外的参数来实现类弹簧动画。这个是我们动画代码的更改。

UIView *redBall = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 100, 100)];
redBall.backgroundColor = [UIColor redColor];
redBall.layer.cornerRadius = 50;
[self.window addSubview:redBall];

[UIView animateWithDuration:3 delay:0 usingSpringWithDamping:.3
    initialSpringVelocity:0 options:0 animations:^{
    redBall.transform = CGAffineTransformMakeTranslation(300, 0);
} completion:NULL];

哇,这是一个长方法!如你所见,有一些我们之前的例子里没有的额外的参数在方法中调用了。参数包括弹簧阻尼和初始弹簧速度。弹簧阻尼是一个0到1之间的值,1模仿一个没有震荡的过阻尼弹簧系统,0表示很有弹力的欠阻尼系统。速度参数用来定义物体开始的快慢,当你使用手势用于用户在屏幕上滑动手指持续一个物体的移动的时候会非常有用。

在我们的例子中我们设置阻尼为0.3(有点弹性),因为我们是用物体静止开始的所以速度为0。因为弹动会使时间变长所以我们也增加了持续时间。

就我个人来说,我不认为iOS 7中使用了新block方法的弹簧动画如我所愿地平滑移动,当你想要完善动作时他们也没有提供足够的弹簧属性来操作。还有,如果你在创建一个地图app并想要使用这些UIKit Dynamics中的弹簧动作将是不幸的。如果你的app还需要支持iOS 7之前的版本怎么办?你也是不幸的,因为UIKit Dynamics直到iOS 7才出现并且不能用于之前的版本中。

所以还有什么别的方式可以创建自然的动作、类弹簧的动画呢?其他的可选项是什么?幸运的是,我认为有两个非常好的UIKit Dynamics的替代方式可以解决我上面列出的关于调整属性和不修改太多就能在iOS 6以及Mac OS X上工作的所有问题。我是下面给两个框架的铁杆粉丝,并且在我已发布和未来开发的app上广泛地使用它们。

这两个框架是JNWSpringAnimationPop by Facebook

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

[redBall.layer addAnimation:scale forKey:scale.keyPath];

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

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

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

// This is within the JNWSwift-Bridging-Header.h file
// that was automatically created for me

#import "JNWSpringAnimation.h"
#import "NSValue+JNWAdditions.h"

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

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

let scale = JNWSpringAnimation(keyPath: "transform.scale")
scale.damping = 9.0
scale.stiffness = 100
scale.mass = 2
scale.fromValue = NSNumber(float: 1.0)
scale.toValue = NSNumber(float: 2.0)

redBall.layer.addAnimation(scale, forKey: scale.keyPath)

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

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

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

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

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

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

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

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

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

[redBall.layer addAnimation:scale forKey:scale.keyPath];
redBall.transform = CGAffineTransformMakeScale(2.0, 2.0);

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

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

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

JNWSpringAnimation *scale =
    [JNWSpringAnimation animationWithKeyPath:@"transform.scale"];
scale.damping = 13;
scale.stiffness = 540;
scale.mass = 11;

scale.fromValue = @(1.0);
scale.toValue = @(2);

[redBall.layer addAnimation:scale forKey:scale.keyPath];
redBall.transform = CGAffineTransformMakeScale(2.0, 2.0);

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

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

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

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

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

JNWSpringAnimation *scale =
    [JNWSpringAnimation animationWithKeyPath:@"transform.rotation"];
scale.damping = 10;
scale.stiffness = 100;
scale.mass = 3;

scale.fromValue = @(0);
scale.toValue = @(M_PI_2);

[redBall.layer addAnimation:scale forKey:scale.keyPath];
redBall.transform = CGAffineTransformMakeRotation(M_PI_2);

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

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

JNWSpringAnimation *scale = [JNWSpringAnimation
    animationWithKeyPath:@"transform.translation.x"];
scale.damping = 7;
scale.stiffness = 7;
scale.mass = 1;

scale.fromValue = @(0);
scale.toValue = @(400);

[redBall.layer addAnimation:scale forKey:scale.keyPath];
redBall.transform = CGAffineTransformMakeTranslation(400, 0);

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

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

JNWSpringAnimation *scale = [JNWSpringAnimation
    animationWithKeyPath:@"transform.scale"];
scale.damping = 9;
scale.stiffness = 9;
scale.mass = 1;
scale.fromValue = @(1);
scale.toValue = @(4.0);

[redBall.layer addAnimation:scale forKey:scale.keyPath];
redBall.transform = CGAffineTransformScale(redBall.transform, 4.0, 4.0);

JNWSpringAnimation *rotate = [JNWSpringAnimation
    animationWithKeyPath:@"transform.rotation"];
rotate.damping = 9;
rotate.stiffness = 9;
rotate.mass = 1;
rotate.fromValue = @(0);
rotate.toValue = @(M_PI);

[redBall.layer addAnimation:rotate forKey:rotate.keyPath];
redBall.transform = CGAffineTransformRotate(redBall.transform, M_PI);

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

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

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

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

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

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