SECTION 5

创建Jeff Broderick的地图动画

在本指南的前面,我提到了一些Jeff Broderick设计并发布到Dribbble的很棒的动画。


SECTION 5 - 图1


如我所说,这里有一些不懂得动画。首先,当地图的图标被点击时,应用的主界面(包括导航栏)同时有不透明度和比例的动画来让其淡出到黑色的背景中并且有一点点缩小。同时,地图伴随着不透明度和比例的动画显著地显现到界面的前面来。地图还会向屏幕上方移动一点,就像过度动画一样。地图图标会保持在原位。

在我们编码重现Jeff的动画前,先看一眼我们创建的最终的动画效果。


SECTION 5 - 图2


我们通过一些简单的UIImageViewUIButton来重新开发这个动画,因为它们可以准确地得到动画的感觉,但在真实的地图中这会是一个真实的可伸缩的地图视图。

首先,让我们添加代表app主界面的图片。

  1. // 添加app的主背景图片
  2. self.appBackground = [[UIImageView alloc] initWithFrame:CGRectMake(0, 20,
  3. self.window.bounds.size.width, 548)];
  4. self.appBackground.image = [UIImage imageNamed:@"app-bg"];
  5. [self.window addSubview:self.appBackground];

我们添加了一个图片属性为“app-bg@2x.png”的简单的UIImageView。app的运行时很聪明,你只用写“app-bg”它就会在app包的图片资源中找到“app-bg@2x.png”。这个视图被添加为类的@property了,这样我们就可以在之后的代码中引用它。这里显示了如何声明一个@porperty

  1. @property (assign) UIImageView *appBackground;

这个@property既可以定义在类的.h文件的@interface中,也可以定义在.m实现文件的@interface块中来让其私有。在苹果的开发者网站的Objective-C指南中可以阅读更多关于程序的属性的内容。

最后,我们将UIImageView作为主屏幕的一个子视图添加进去。这是一个快速的模型,否则我会创建另一个UIViewController的子类来装载我们的UI代码。

如果我构建并运行,这就是app目前看起来的样子。


SECTION 5 - 图3


非常棒!现在让我们添加地图,它会是透明的,并且会伴随着变化开始。我们会在主应用图片后立即添加它,因为我们想要最后添加图标按钮,这样它就会使z轴上最高的,也就是在其他视图的顶部。

  1. // 添加地图视图
  2. self.mapView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 62,
  3. self.window.bounds.size.width, 458)];
  4. self.mapView.image = [UIImage imageNamed:@"map-arrow"];
  5. self.mapView.alpha = 0.0f;
  6. self.mapView.transform = CGAffineTransformMakeTranslation(0, 30);
  7. self.mapView.transform = CGAffineTransformScale(self.mapView.transform, 1.1, 1.1);
  8. [self.window addSubview:self.mapView];

想在Swift下开发这些例子么?这里就是Swift下的上面Objective-C的代码。

  1. self.mapView =
  2. UIImageView(frame: CGRectMake(0, 62, self.window!.bounds.size.width, 458))
  3. self.mapView!.image = UIImage(named: "map-arrow")
  4. self.mapView!.alpha = 0.0
  5. self.mapView!.transform = CGAffineTransformMakeTranslation(0, 30)
  6. self.mapView!.transform = CGAffineTransformScale(self.mapView!.transform, 1.1, 1.1)
  7. self.window!.addSubview(self.mapView!)

地图视图的frame开始会在左上角,但会距离顶部62像素,这样就会正好位于我们要添加的地图按钮的下方一点点。图片属性被设为“map-arrow”,这只是一个地图图片,我将其和一个箭头放在一起,来模仿Jeff在他的动画中所涉及的样子。

一开始,这个视图会是完全透明的,所以alpha属性被设为0。有两个变换添加到视图中:第一个将视图往下移动30像素,第二个将其从正常尺寸拉伸到1.1倍。

这里是它现在看起来的样子,我注视了alpha那一行,这样我们就可以看到地图在哪。


SECTION 5 - 图4


这看起来是动画开始的准确位置了。

现在让我们添加我们的图标按钮。

  1. // 添加图标
  2. UIButton *icon = [UIButton buttonWithType:UIButtonTypeCustom];
  3. [icon setImage:[UIImage imageNamed:@"map-icon"] forState:UIControlStateNormal];
  4. [icon addTarget:self action:@selector(didTapMapIcon:)
  5. forControlEvents:UIControlEventTouchUpInside];
  6. [icon setFrame:CGRectMake(self.window.bounds.size.width - 49, 19, 49, 44)];
  7. [self.window addSubview:icon];

这是一个非常典型的添加图标按钮的方式。UIButton类有一个便利的方式来构建一个按钮:+buttonWithType:类方法。我将按钮类型设为UIButtonTypeCustom,意味着没有默认的风格会被设置,完全取决于我。这是一种实用的简单图标按钮,没有边界和其他怪异的风格需要移除。有点类似于CSS中对按钮进行重置。

接下来我设置按钮的图片为我app包中的“map-icon”图片。参数UIControlStateNormal意味着这个图标会在常规、默认状态下为显示按钮的图片。你可以用多种图片多次设置这个值,只要你想要改变图标,比如UIControlStateHighlighted状态。默认情况下,当一个UIButton被点击时,iOS会自动暗化图片。

最后,我让按钮可被点击并且会调用我定义的一个方法。self参数值意味着我想要这个按钮调用其被点击时所在的类,而@selector(didTapMapIcon:)是我想要调用的Objective-C方法。接下来我通过设置frame将按钮放置在准确的位置。

让我们看看现在app的样子,地图的alpha值被设为了0,所以它是不可见的。


SECTION 5 - 图5


好,现在我们将动画的所有主要部件都添加到界面上了,是时候在地图图标被点击时添加一些动画了。

首先,我们需要实现按钮被点击时被调用的方法。这里是不含任何内容的方法看起来的样子。

  1. - (void)didTapMapIcon:(id)sender {
  2. // 暂时没有任何内容!
  3. }

它会在用户点击地图按钮时被调用,因为我们之前通过 -addTarget:action:forControlEvents:方法进行了设置。

所以,按照逻辑,当你点击按钮时,下面两种事件之一会发生:将地图动画到界面上,或者如果地图已经可见了,则将地图动画出界面。我们可以检查我们的界面元素并查看它们的位置来决定我们应该做什么,但那太麻烦了,所以让我们通过一个简单的作为类@property的 BOOL 变量来跟踪状态。在这个文件的顶部我添加了一个名为mapShowing的BOOL变量来管理我们是需要打开还是关闭地图视图。这个属性会放置在我们按钮方法的下面,而我们添加的其他属性是我们界面的主视图。

  1. @interface DTCAppDelegate ()
  2. - (void)didTapMapIcon:(id)sender;
  3. @property (assign) BOOL mapShowing;
  4. @property (strong) UIImageView *appBackground;
  5. @property (strong) UIImageView *mapView;
  6. @end

现在,回到我们的按钮点击方法,我们需要在这里添加一些逻辑,来检查地图是显示还是不显示,然后将变量设为相反的。

  1. - (void)didTapMapIcon:(id)sender {
  2. if (self.mapShowing) {
  3. self.mapShowing = NO;
  4. // 当地图已经可见时要运行的代码
  5. } else {
  6. self.mapShowing = YES;
  7. // 当地图不可见时要运行的代码
  8. }

让我们从else的情况开始,此时地图未显示,我们需要进行不透明度的动画。我们需要做的是淡出主app背景一点点然后淡入地图。主app背景的淡出速度会比地图的淡入速度慢一点点,这样地图会更显眼。

  1. [UIView animateWithDuration:.5 delay:0
  2. options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionBeginFromCurrentState
  3. animations:^{
  4. self.appBackground.alpha = 0.3f;
  5. } completion:NULL];
  6. [UIView animateWithDuration:.15 delay:0
  7. options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionBeginFromCurrentState
  8. animations:^{
  9. self.mapView.alpha = 1.0f;
  10. } completion:NULL];

你可能注意到了放置在这个基于block的UIView动画方法总的options依据里的巨大的参数。这实际上是两个选项通过二进制 | 操作组合在一起的:UIViewAnimationOptionCurveEaseInOut用来定义动画的淡入淡出,UIViewAnimationOptionBeginFromCurrentState会从其alpha的当前值开始动画,这样即使动画被打断了,它也不会跳回开始动画前的初始值。这对像这样被用户动作管理的动画非常重要,因为你不知道用户会不会在动画发生后不停点击按钮,而且你肯定不想在动画完成后都没做任何事。

当然,调整主app界面和地图的不透明度并没有准确地完成我们的动画,因为我们还需要动画地图的比例和位置,这样它才能够到达它最终的位置和尺寸。对于主app界面,我们只会稍微动画其比例。

即使这些动画可以通过一个淡出动画曲线来完成,我也想使用含有相同damping和stiffness值得弹簧动画,这样我就可以减缓速度。这里不会有弹性,只是非常平滑的过渡。

  1. CGFloat dampingStiffness = 16.0f;
  2. // 主app背景的比例动画
  3. JNWSpringAnimation *scale =
  4. [JNWSpringAnimation animationWithKeyPath:@"transform.scale"];
  5. scale.damping = dampingStiffness;
  6. scale.stiffness = dampingStiffness;
  7. scale.mass = 1;
  8. scale.fromValue = @([[self.appBackground.layer.presentationLayer
  9. valueForKeyPath:scale.keyPath] floatValue]);
  10. scale.toValue = @(0.9);
  11. [self.appBackground.layer addAnimation:scale forKey:scale.keyPath];
  12. self.appBackground.transform =
  13. CGAffineTransformScale(CGAffineTransformIdentity, .9, .9);

我将damping和stiffness值设为一个CGFloat变量,这样我就可以更简单地调整它们而不用一次更新两个值。

这个block代码中的一个主要的与其他例子不同的改变是比例动画的fromValue没有被设为一个常量,而是设为[[self.appBackground.layer.presentationLayer valueForKeyPath:scale.keyPath] floatValue]。这是什么意思呢?如果你一块块拆开,这些事要发生的事:

  • 我会使用self.appBackground来访问这个类的appBackground属性
  • 我会获取到这个视图的CALayer对象
  • 我在layer上获取presentationLayer属性,通过它来获取特殊的presentation model layer,让我们看到动画改变时的值
  • 当我有了presentationLayer后,我会调用 -valueForKeyPath: 来取得变换的比例部分的当前值。(scale.keyPath = @”transform.scale”)
  • 当我最后有了当前的比例值后,它不是JNWSpringAnimation需要的数据格式,所以我使用了floatValue。

记得之前我提到过在动画中layer上的很多属性值都不会改变么?以及presentation model layer是Core Animation用来存储动画发生过程中精确的变更值的?我们需要获取比例变换的当前值,这样就可以在当前任何点开始动画(记住如果用户很开心地不停点击,我们不想要动画重新开始!)。我们需要获取特殊的显示层来查看值。然后我们使用它作为我们动画的fromValue,这样他就能始终正常工作,无论fromValue是我们为视图设置的正常的、未触摸的比例值,还是动画中的某个值。如果我们不通过presentationLayer获取它,这个值在动画中就始终不会正确,直到动画结束。

我们不仅仅需要动画主app背景,还需要动画地图,将比例降回1.0,,并且通过过渡移动到屏幕上。让我们现在做。

  1. // 地图有两个分开的动画,一个用于位置,一个用于比例
  2. JNWSpringAnimation *mapScale =
  3. [JNWSpringAnimation animationWithKeyPath:@"transform.scale"];
  4. mapScale.damping = dampingStiffness;
  5. mapScale.stiffness = dampingStiffness;
  6. mapScale.mass = 1;
  7. mapScale.fromValue =
  8. @([[self.mapView.layer.presentationLayer valueForKeyPath:mapScale.keyPath] floatValue]);
  9. mapScale.toValue = @(1.0);
  10. [self.mapView.layer addAnimation:mapScale forKey:mapScale.keyPath];
  11. self.mapView.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1, 1);
  12. JNWSpringAnimation *mapTranslate =
  13. [JNWSpringAnimation animationWithKeyPath:@"transform.translation.y"];
  14. mapTranslate.damping = dampingStiffness;
  15. mapTranslate.stiffness = dampingStiffness;
  16. mapTranslate.mass = 1;
  17. mapTranslate.fromValue =
  18. @([[self.mapView.layer.presentationLayer valueForKeyPath:mapTranslate.keyPath] floatValue]);
  19. mapTranslate.toValue = @(0);
  20. [self.mapView.layer addAnimation:mapTranslate forKey:mapTranslate.keyPath];
  21. self.mapView.transform = CGAffineTransformTranslate(self.mapView.transform, 0, 0);

这里没有什么很复杂的,除了获取当前变化的值来从其开始,如前面的动画一样。我在这也使用了damping和stiffness变量,这样所有的动画都感觉是同一个类型的动作。

锁着这是一块正统的代码,好在其非常简单,而且现在你应该习惯了JNWSpringAnimation代码块的样子。这是目前动画看起来的样子。


SECTION 5 - 图6


现在是时候添加这个界面的其他动画了,即当用户点击地图图标且地图可见时,我们想要将其淡出并且将主app背景放回到前面。因为它和我们刚才展示的动画除了开始和结束值外完全一样,这里就直接放一个大块来解释发生了什么。

  1. if (self.mapShowing) {
  2. self.mapShowing = NO;
  3. // 再次使用这些动画相同的damping和stiffness
  4. // 这样我们就可以获取CGFloat形式的值。注意这个值会高一点
  5. // 意味着动画会花费更少的时间(在匹配此damping和stiffness的弹簧动画下)。
  6. // 少时间是好的,因为我们要回到界面的默认状态,而此时用户只想让地图赶紧消失。
  7. CGFloat dampingStiffnessOut = 24.0f;
  8. // 再说一次,从当前状态开始很重要,这样用户点击按钮时就不会抽动
  9. [UIView animateWithDuration:.5 delay:0
  10. options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionBeginFromCurrentState
  11. animations:^{
  12. self.appBackground.alpha = 1.0f;
  13. } completion:NULL];
  14. [UIView animateWithDuration:.3 delay:0
  15. options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionBeginFromCurrentState
  16. animations:^{
  17. self.mapView.alpha = 0.0f;
  18. } completion:NULL];
  19. // 地图有两个分开的动画,一个是位置一个是比例。
  20. // 我们通过presentationLayer获取@“transform.scale”的变化的值,如之前的例子一样
  21. JNWSpringAnimation *mapScale =
  22. [JNWSpringAnimationanimationWithKeyPath:@"transform.scale"];
  23. mapScale.damping = dampingStiffnessOut;
  24. mapScale.stiffness = dampingStiffnessOut;
  25. mapScale.mass = 1;
  26. mapScale.fromValue =
  27. @([[self.mapView.layer.presentationLayer
  28. valueForKeyPath:mapScale.keyPath] floatValue]);
  29. mapScale.toValue = @(1.1);
  30. [self.mapView.layer addAnimation:mapScale forKey:mapScale.keyPath];
  31. self.mapView.transform =
  32. CGAffineTransformScale(CGAffineTransformIdentity, 1.1, 1.1);
  33. JNWSpringAnimation *mapTranslate =
  34. [JNWSpringAnimation animationWithKeyPath:@"transform.translation.y"];
  35. mapTranslate.damping = dampingStiffnessOut;
  36. mapTranslate.stiffness = dampingStiffnessOut;
  37. mapTranslate.mass = 1;
  38. mapTranslate.fromValue =
  39. @([[self.mapView.layer.presentationLayer
  40. valueForKeyPath:mapTranslate.keyPath] floatValue]);
  41. mapTranslate.toValue = @(30);
  42. [self.mapView.layer addAnimation:mapTranslate forKey:mapTranslate.keyPath];
  43. self.mapView.transform = CGAffineTransformTranslate(self.mapView.transform, 0, 30);
  44. // 主app背景的比例动画。我们将其动画回1.0倍
  45. JNWSpringAnimation *scale =
  46. [JNWSpringAnimation animationWithKeyPath:@"transform.scale"];
  47. scale.damping = dampingStiffnessOut;
  48. scale.stiffness = dampingStiffnessOut;
  49. scale.mass = 1;
  50. scale.fromValue =
  51. @([[self.appBackground.layer.presentationLayer
  52. valueForKeyPath:@"transform.scale.x"] floatValue]);
  53. scale.toValue = @(1.0);
  54. [self.appBackground.layer addAnimation:scale forKey:scale.keyPath];
  55. self.appBackground.transform =
  56. CGAffineTransformScale(CGAffineTransformIdentity, 1.0, 1.0);
  57. }

这里是完整的、最终的动画的样子。如果你想一个疯子一样点击,会发现它确实是从当前值开始动画的,而且不会抽动。


SECTION 5 - 图7


这很有意思!现在让我们去转眼有些断断续续的动画。

构建Jakub Antalik的音乐播放器

Jakub是斯洛伐克的一名出色的设计师,已经设计了一些经常发布到Dtibbble去的非常有创造力的界面。其中一个作品非常打动我,那是一个很有趣的例子,证明了界面上每次操作一个元素的断断续续的动画是如何抓住用户的眼球的。在本指南的早期我展示了一个他设计的音乐播放器,含有一些很酷的内置动画,这里我们再看一下。


SECTION 5 - 图8


所以这里他明显使用到的技术是什么?他操纵了动画的开始时间。通过让每个元素比另一个元素慢一点动画到屏幕上的位置,并按照行的顺序操作屏幕上的每一个元素,就形成了一个非常整齐的波浪效果,感觉就像每个元素都被前一个元素用橡皮筋带动的一样。

让我们重建他音乐播放器概念的第二个屏幕:歌曲列表。

首先,我们需要重建设计来切片元素并且尽可能整齐地分开动画它们。我拉出我选择的设计工具:Photoshop,然后开始工作。musicplayer.psd文件是放置该设计文件的地方,如果你喜欢的话可以打开来检出它。我不会详细说明如何用Photoshop创建这个设计,但文件和设计都足够简单和直接。

这里是我重建的第二个屏幕的歌曲列表。


SECTION 5 - 图9


如果你仔细观察原始的动画,会发现有8个分开动画的不同元素。

  1. 黑色箭头和“Dance Club”文本
  2. “Ministry of Fun”文本
  3. “Add a Song”按钮
  4. 五首歌对应的五行

这8个元素(或元素组,因为箭头和“Dance Club”文本是一起动画的)是通过不同的开始时间递进进入视图的,这就是我们要在动画中获取的非常酷的波浪感效果。

首先我们整理一下计划。我需要做的是分开添加这些元素到界面上,这样我就可以分开动画它们了。如果这是一个真实的app,有着真实流入的数据,这个界面最可能是一个UITableView或者UICollectionView来获取一个好的、结构化的展示行的方式。从高层面来概括这两个视图类型的话,就是你实现你需要定义的它们的接口方法,来返回一些数据到界面上,比如返回行高的方法,或者返回一个只有一行的视图的方法。因为我们没有数据,而且我的主要目的是演示如何构建动画,我就仅仅是保存一些Photoshop里设计的图片并手动将这些图片添加到界面上去,从顶部的箭头和“Dance Club”文本开始。

  1. // 定义一个变量来获取屏幕的宽度,我们会经常用到这个值。
  2. CGFloat windowWidth = self.window.bounds.size.width;
  3. // 将背景添加到界面上
  4. UIImageView *backgroundView = [[UIImageView alloc] initWithFrame:self.window.bounds];
  5. backgroundView.image = [UIImage imageNamed:@"background"];
  6. [self.window addSubview:backgroundView];
  7. // 添加箭头和文本
  8. UIImageView *arrowView =
  9. [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, windowWidth, 45)];
  10. arrowView.image = [UIImage imageNamed:@"arrow"];
  11. [self.window addSubview:arrowView];

这里没什么特别的,只是简单地添加一些视图到我们原型的主屏幕上。名为@“background”的图片是大的渐变的图片,作为其他视图的背景。@“arrow”图片是用Photoshop做出来的包含箭头和“Dance Club”文本的图片,因为我会同时动画它们,所以将它们简单地放在一个图片里。

这里是目前界面看起来的样子。


SECTION 5 - 图10


现在让我们添加更多的视图!

  1. // “Ministry of Fun”图片
  2. UIImageView *ministryView =
  3. [[UIImageView alloc] initWithFrame:CGRectMake(0, 57, windowWidth, 28)];
  4. ministryView.image = [UIImage imageNamed:@"ministry"];
  5. [self.window addSubview:ministryView];
  6. // 添加一个歌曲按钮
  7. UIButton *addButton = [UIButton buttonWithType:UIButtonTypeCustom];
  8. [addButton setImage:[UIImage imageNamed:@"add-button"] forState:UIControlStateNormal];
  9. [addButton setImage:[UIImage imageNamed:@"add-button-pressed"]
  10. forState:UIControlStateHighlighted];
  11. [addButton setFrame:CGRectMake(0, 102, windowWidth, 45)];
  12. [self.window addSubview:addButton];

我添加“Ministry of Fun”图片视图(使用我用Photoshop分割出来的PNG图片)到界面上然后为“Add a Song”按钮创建一个UIButton。我本可以懒一点,不将按钮做成一个真的UIButton,而是使用一个UIImageView,但我想演示如何为一个自定义的UIButton设置点击的和普通的图片。只需要调用同样的一个 -setImage:forState:方法,但给它传输不同的属性。你可以随便调用它来设置不同的状态属性,来覆盖用户对按钮的每一个可能的操作。接着我设置按钮的位置并将它添加到界面上。

这里是目前状态的界面,以及点击按钮时不同状态的演示。


SECTION 5 - 图11


我们UIControlStateHighlighted状态的图片只是将白色边框换成了白色的填充。

现在让我们添加我们的行。它们也都是UIImageView,所以也只用直接在背景图片上放置就可以了。

  1. // Katy Perry 行
  2. UIImageView *firstRow =
  3. [[UIImageView alloc] initWithFrame:CGRectMake(0, 170, windowWidth, 80)];
  4. firstRow.image = [UIImage imageNamed:@"1st-row"];
  5. [self.window addSubview:firstRow];
  6. // Shakira 行
  7. UIImageView *secondRow =
  8. [[UIImageView alloc] initWithFrame:CGRectMake(0, 170+80, windowWidth, 80)];
  9. secondRow.image = [UIImage imageNamed:@"2nd-row"];
  10. [self.window addSubview:secondRow];
  11. // Pitbull 行
  12. UIImageView *thirdRow =
  13. [[UIImageView alloc] initWithFrame:CGRectMake(0, 170+160, windowWidth, 80)];
  14. thirdRow.image = [UIImage imageNamed:@"3rd-row"];
  15. [self.window addSubview:thirdRow];
  16. // Lana del Rey 行
  17. UIImageView *fourthRow =
  18. [[UIImageView alloc] initWithFrame:CGRectMake(0, 170+240, windowWidth, 80)];
  19. fourthRow.image = [UIImage imageNamed:@"4th-row"];
  20. [self.window addSubview:fourthRow];
  21. // HEX 行
  22. UIImageView *fifthRow =
  23. [[UIImageView alloc] initWithFrame:CGRectMake(0, 170+320, windowWidth, 80)];
  24. fifthRow.image = [UIImage imageNamed:@"5th-row"];
  25. [self.window addSubview:fifthRow];

你可能注意到每一行frame的Y坐标(垂直位置)都有一个小方程式。每一行都是80px高,所以放置它们每一行的时候我都在Y坐标上加了80。我也可以使用Auto Layout来做,但对这个例子来说就有点过于复杂了。

这里是在添加动画前的样子。


SECTION 5 - 图12


但等一下,我们并不想要在第一次进入的看到这样的界面。这次练习的目的在于让每个元素都动画到它们的位置上,也就是说它们不应该立即出现在它们的最终位置。我要做的是从屏幕的右边开始每一个元素,然后我会让每个元素的左边动画到屏幕的左边,来到最终的位置。

让我们回到我们的视图设置代码并修改每个元素的frame,这样它们的X轴坐标就不再是0了,而是屏幕的宽度。这样就会让每个元素的左边界并齐屏幕的右边界,用户就看不到了。

  1. // 添加箭头和顶部的文字
  2. UIImageView *arrowView =
  3. [[UIImageView alloc] initWithFrame:CGRectMake(windowWidth, 0, windowWidth, 45)];
  4. arrowView.image = [UIImage imageNamed:@"arrow"];
  5. [self.window addSubview:arrowView];
  6. // Ministry of Fun 文字
  7. UIImageView *ministryView =
  8. [[UIImageView alloc] initWithFrame:CGRectMake(windowWidth, 57, windowWidth, 56/2)];
  9. ministryView.image = [UIImage imageNamed:@"ministry"];
  10. [self.window addSubview:ministryView];
  11. // Add a Song 按钮
  12. UIButton *addButton = [UIButton buttonWithType:UIButtonTypeCustom];
  13. [addButton setImage:[UIImage imageNamed:@"add-button"]
  14. forState:UIControlStateNormal];
  15. [addButton setImage:[UIImage imageNamed:@"add-button-pressed"]
  16. forState:UIControlStateHighlighted];
  17. [addButton setFrame:CGRectMake(windowWidth, 102, windowWidth, 45)];
  18. [self.window addSubview:addButton];
  19. // Katy Perry 行
  20. UIImageView *firstRow =
  21. [[UIImageView alloc] initWithFrame:CGRectMake(windowWidth, 170, windowWidth, 80)];
  22. firstRow.image = [UIImage imageNamed:@"1st-row"];
  23. [self.window addSubview:firstRow];
  24. // Shakira 行
  25. UIImageView *secondRow =
  26. [[UIImageView alloc] initWithFrame:CGRectMake(windowWidth, 170+80, windowWidth, 80)];
  27. secondRow.image = [UIImage imageNamed:@"2nd-row"];
  28. [self.window addSubview:secondRow];
  29. // Pitbull 行
  30. UIImageView *thirdRow =
  31. [[UIImageView alloc] initWithFrame:CGRectMake(windowWidth, 170+160, windowWidth, 80)];
  32. thirdRow.image = [UIImage imageNamed:@"3rd-row"];
  33. [self.window addSubview:thirdRow];
  34. // Lana del Rey 行
  35. UIImageView *fourthRow =
  36. [[UIImageView alloc] initWithFrame:CGRectMake(windowWidth, 170+240, windowWidth, 80)];
  37. fourthRow.image = [UIImage imageNamed:@"4th-row"];
  38. [self.window addSubview:fourthRow];
  39. // HEX 行
  40. UIImageView *fifthRow =
  41. [[UIImageView alloc] initWithFrame:CGRectMake(windowWidth, 170+320, windowWidth, 80)];
  42. fifthRow.image = [UIImage imageNamed:@"5th-row"];
  43. [self.window addSubview:fifthRow];

你可以想象一下现在所有元素都移动到屏幕的右边去的界面样式,现在只显示了背景图片。

现在所有内容都在屏幕外并且准备好动画了,策略是让每个元素都动画到左边,一次一个,每个都有所延迟,这样就会产生一种波浪的感觉。为了好玩,我们试试使用基于block的UIView动画方法来让我们的元素动画到屏幕上。

这里是第一个动画block,我们会将箭头和“Dance Club”图片滑动到左边。

  1. [UIView animateWithDuration:1.1 delay:0 usingSpringWithDamping:0.3
  2. initialSpringVelocity:0 options:0 animations:^{
  3. [arrowView setFrame:CGRectMake(0, 0, windowWidth, 45)];
  4. } completion:NULL];

这个基于block的动画有1.1秒的持续时间和0.3的弹簧阻尼。持续时间是动画完成需要的时间,而阻尼是iOS 7在UIView动画方法中提供的一个弹簧属性,用来控制弹簧的弹力。JNWSpringAnimation提供了三个属性来控制弹簧的物理性质,但Apple值提供了一个,即damping属性。damping需要时一个0到1之间的值,越接近0,弹簧动作就越有弹性,越接近1,就越没有弹性,直到完全没有弹性,变成一个平滑的淡入。

让我们看看这个duration和damping值产生的动作。


SECTION 5 - 图13


恩,有点不太对。动画太快也太跳跃了。这种类型的弹性动画带来了一些焦虑。这是一个关于仅仅使用一个弹簧动画并不能提升你的app整体用户体验的很好的例子。每种类型的动画都给你的用户带来了一些感受,而这个带来了错误地感受。

让我们将持续时间提升到2.1秒并看看感觉。


SECTION 5 - 图14


比起Jakub的原始动画,这个又太弹了,我们的damping值也需要调整。让我们将damping从0.3提升到0.6,如我之前所说,它更靠近1这个不弹的值。我们还是需要一点弹性,现在让我们来看看它怎么样了。


SECTION 5 - 图15


好了,不是太坏。你可以发现当你使用iOS 7提供的弹簧动画方法时,它直接提供了一些值来获取你想要的感觉。NSWSpringAnimation给出的弹簧属性更容易理解,至少对我来说是这样,因为它们都操作了弹簧动作方程的不同属性。iOS 7的基于block的动画中的damping值实际上是一个解释值,这意味着苹果无论获取到你输入的什么值,都会做一些复杂的计算来操作这个值并将其放入弹簧动作方程式中。你可以说苹果操作了这个值,因为它在0和1之间改变弹性。而在实际的弹簧动作方程中,动作的时间(它到达平衡点或者最终位置的时间)是由弹簧的其他属性决定的,它不是你去设置然后强制弹簧遵循的。苹果的动画方法有一个你需要设置的持续时间,所以你在以一种并非完全遵循物理法则管理下的弹簧动作。这就是为什么我倾向于用JSWSpringAniamtion(或者Facebook Pop,我会马上提及),因为它们有着更加自然、逼真的弹簧动画。

现在,让我们从上到下动画屏幕上的其他元素。每个都需要比前一个开始得稍微慢一点。同时我想要控制app启动后动画开始的时间,来看看我如何管理。

  1. CGFloat initialDelay = 1.0f;
  2. CGFloat stutter = 0.3f;
  3. // 动画箭头图片
  4. [UIView animateWithDuration:2.1 delay:initialDelay
  5. usingSpringWithDamping:0.6 initialSpringVelocity:0 options:0 animations:^{
  6. [arrowView setFrame:CGRectMake(0, 0, windowWidth, 45)];
  7. } completion:NULL];
  8. // 动画Ministry of Fun文字
  9. [UIView animateWithDuration:2.1 delay:initialDelay + (1 * stutter)
  10. usingSpringWithDamping:0.6 initialSpringVelocity:0 options:0 animations:^{
  11. [ministryView setFrame:CGRectMake(0, 57, windowWidth, 28)];
  12. } completion:NULL];

我设置了两个CGFloat变量,一个initialDelay值来存储延迟时间,一个stutter来存储每个动画之间细微的延迟。这个数字对我们动画效果整体的感觉和流动感都非常重要。动画之间太长的延时会让他们觉得不连贯,太短就不足以形成我们想要构建的波浪效果。

回到代码:第一个动画的delay属性就是initialDelay变量的值,因为这是来到屏幕上的第一个动画。第二个动画block的delay值为initialDelay+(1*stutter)。这表示它会等待开始的延迟时间,然后会等待stutter值乘以1的时间。接下来的所有动画都会遵循这个公式作为延时,并且每次都会加1倍stutter。这可以确保他们的动画之间都是同样的延时。

这里是现在看起来的样子。


SECTION 5 - 图16


我觉得这个看起来不错。老实说,只动画两个元素很难看出波浪效果是不是好的,因为你无法获取一个整体的真实感受,除非动画一系列的元素。所以让我们动画屏幕上的其他元素。

  1. [UIView animateWithDuration:2.1 delay:initialDelay + (2 * stutter)
  2. usingSpringWithDamping:0.6 initialSpringVelocity:0 options:0 animations:^{
  3. [addButton setFrame:CGRectMake(0, 102, windowWidth, 45)];
  4. } completion:NULL];
  5. [UIView animateWithDuration:2.1 delay:initialDelay + (3 * stutter)
  6. usingSpringWithDamping:0.6 initialSpringVelocity:0 options:0 animations:^{
  7. [firstRow setFrame:CGRectMake(0, 170, windowWidth, 80)];
  8. } completion:NULL];
  9. [UIView animateWithDuration:2.1 delay:initialDelay + (4 * stutter)
  10. usingSpringWithDamping:0.6 initialSpringVelocity:0 options:0 animations:^{
  11. [secondRow setFrame:CGRectMake(0, 170+80, windowWidth, 80)];
  12. } completion:NULL];
  13. [UIView animateWithDuration:2.1 delay:initialDelay + (5 * stutter)
  14. usingSpringWithDamping:0.6 initialSpringVelocity:0 options:0 animations:^{
  15. [thirdRow setFrame:CGRectMake(0, 170+160, windowWidth, 80)];
  16. } completion:NULL];
  17. [UIView animateWithDuration:2.1 delay:initialDelay + (6 * stutter)
  18. usingSpringWithDamping:0.6 initialSpringVelocity:0 options:0 animations:^{
  19. [fourthRow setFrame:CGRectMake(0, 170+240, windowWidth, 80)];
  20. } completion:NULL];
  21. [UIView animateWithDuration:2.1 delay:initialDelay + (7 * stutter)
  22. usingSpringWithDamping:0.6 initialSpringVelocity:0 options:0 animations:^{
  23. [fifthRow setFrame:CGRectMake(0, 170+320, windowWidth, 80)];
  24. } completion:NULL];

现在我们动画了所有的元素到位置上了,让我们看看效果。


SECTION 5 - 图17


对我来说感觉还不太对。动画的延时还是有点太长了,破坏了想要的波浪感。看起来一点也没有流动感。让我们降低延时,把stutter变量的值从0.3降为0.15来看看效果。


SECTION 5 - 图18


很接近了,但我认为我们可以再缩小一点点延迟时间来让它更有天然的流动感,就像每个元素都牵引了下一个。让我们将stutter变量降为0.6。


SECTION 5 - 图19


现在我们有些成果了。我认为它看起来很棒并且有非常好的波浪动作。让我们和Jakub原始的动作做一些比较。


SECTION 5 - 图20


看起来我们匹配得很接近!所以从这个例子中学到了什么呢?

  • 基于block的UIView动画方法中的弹簧的damping值是一个抽象值,对获取一个好的感觉并没有什么用。这就是为什么我喜欢用真实的弹簧动作(不需要设置持续时间的),比如JSWSpringAnimation提供的那种。
  • 当实现一个像这个一样内置的动画时,调整动画之间的延时是得到一个好的波浪形动作的关键点。

我在我自己的iPhone app Interesting中也使用了波浪形的动画。来看看我的app的动画并构建它。

动画Interesting的Stories Into Position

当我的新闻app Interesting首次打开时,我会发起一个网络请求来拉取最近的文章。当请求返回时,我需要用UITableView来放置文章数据,每行一篇文章。一些app选择在数据返回时淡入列表,一些会将行一行行地滑动到位置上,而其他的则立即显示行,没有任何动画。我选择使用一个内置的类似我们刚刚构建的音乐播放器的效果,但不是水平地动画它们,我从底部垂直地动画它们。这就是我的加载动画的样子。


SECTION 5 - 图21


要完成它,先来一步步地分解我做了什么。

  1. 如果数据返回了并且我调用了[self.tableView reloadData],它会立即出现并且对用户可见。所以我首先让列表的透明度变为0,这样我就可以操作它,不让用户看到任何东西,直到我想让他们看见。
  2. 然后我会调用[self.tableView reloadData]将数据加载到列表行中去,这时候所有的行都在它们正常的位置上,但因为整个列表透明度为0并且是隐藏的,屏幕上什么都看不见。
  3. 我遍历现在屏幕上可见的行并且移动UITableView将行都放到屏幕底部。我通过改变列表的位置,将其移动到整个列表高度的下方来达到目的,这样每行都会藏在屏幕的底部了。
  4. 现在所有的行都在屏幕的底部了,我将alpha改回1.0来让列表变得可见。现在列表是可见的了,但素有的行都在屏幕底部所以看不到任何文章。
  5. 最后,我再次遍历所有的行将其推离屏幕底部,通过移除我初始设置的变换将其动画到原本的位置上。

这个看上去相当简单的效果有这么多的步骤!这里是完成这些步骤的代码。

  1. // 将列表变为不可见,重载数据
  2. self.tableView.alpha = 0.0f;
  3. [self.tableView reloadData];
  4. // 存储一个时间变量,这样我就可以调整每行动画之间的延迟时间
  5. CGFloat diff = .05;
  6. CGFloat tableHeight = self.tableView.bounds.size.height;
  7. NSArray *cells = [self.tableView visibleCells];
  8. // 遍历行并将它们移动到屏幕底部
  9. for (NSUInteger a = 0; a < [cells count]; a++) {
  10. UITableViewCell *cell = [cells objectAtIndex:a];
  11. if ([cell isKindOfClass:[UITableViewCell class]]) {
  12. // 通过变换cell的Y坐标来讲其移动到屏幕底部
  13. cell.transform = CGAffineTransformMakeTranslation(0, tableHeight);
  14. }
  15. }
  16. // 现在所有的行都在屏幕底部了,将列表设为可见
  17. self.tableView.alpha = 1.0f;
  18. // 将每行动画回位置
  19. for (NSUInteger b = 0; b < [cells count]; b++) {
  20. UITableViewCell *cell = [cells objectAtIndex:b];
  21. [UIView animateWithDuration:1.6 delay:diff*b usingSpringWithDamping:0.77
  22. initialSpringVelocity:0 options:0 animations:^{
  23. cell.transform = CGAffineTransformMakeTranslation(0, 0);
  24. } completion:NULL];
  25. }

如果你注意第二个循环,在动画的block中,我的延迟值设为了diff*b。因为我在一个循环中,我可以同步地使用循环次数变量b来保持动画的时间,只需要操作每行的动画时间间隔即可。这可以确保每一行的动画之间都是同样的时间,来达到一个好的波浪形动作。这就是全部了!


是时候换挡了。

至此,我们使用了Core Animation来创建我们的动画界面。无论我们是使用iOS 7的基于block的动画方法及其弹簧属性,还是使用很棒的为我们创建了CAKeyframeAnimation的JNWSpringAnimation框架,我们都还在Core Animation的范围内,苹果有众多的框架管理了iOS繁多的界面表现。

但有很多种方法可以解决问题,也就是说,还有其他的不使用苹果的Core Animation框架的方式可以在iOS app的屏幕上创建动作。

其中一个创建动画的方法最近获取了很多的关注。它实在是iOS动画框架界的一股清流,而且已经在世界上一些最常用的app中被用来构建了非常棒的动画。

我说的当然就是Facebook创建的杰出的Pop框架。

你准备好学习一些新东西了吗?开始吧!