SECTION 6

介绍Facebook的Pop

在2014年4月,Facebook的工程师Kimon Tsinteris发布了Pop,Facebook构建用来支撑他们app Paper的一个弹簧动画框架。这个框架的起源其实早于Facebook,Kimon构建了其中的大部分用来支撑他被Facebook于2011年收购的电子书公司Push Pop Press。你可能记得Push Pop Press,它获得了苹果的设计奖,作为iPad的电子书,为被称为“Our Choice”的AI Gore所构建。

SECTION 6 - 图1

点击观看AI Gore’s ‘Our Choice’ - an iPad app视频

“Our Choice”在2011年确实很有开创性。它是最早的有完整的基于手势来操作界面内容的iOS app之一。屏幕上的每个视觉元素都是弹簧动画的,有着很好的弹性和响应,这是从所未见的。

时间快进到2014年早期,来介绍一些Facebook的Paper。Paper同样包含基于手势的控制和弹簧动画,并且可以发现,它全部基于Al Gore三年前的电子书的工作。对我们开发者来说幸运的是,Facebook认为他们的动画框架Pop值得公开到社区中去,这样其他人也能构建酷的基于弹簧动画的app了。

Pop VS Core Animation

当我们在本书前面讨论Core Animation的时候,以及它是如何在一个基本的水平上工作的,我结识了model layer和presentation layer的不同。模型层表示已知的准确的CALayer预加到动画上的属性。如果你添加一个动画到layer上,然后在动画进行到一半时问模型层它的属性是什么,答案是不会反映任何动画当前的内容的。如果你想要知道动画中实时的、运动中的layer的值,你就得去看表现层。而一旦动画完成后,表现层就会消失,所以如果你不想你的layer回到开始的位置,你就需要设置模型层的属性来匹配动画的最终状态。

这就是Core Animation的工作。这是苹果为了构建一个iPhone上用的动画框架在很多年前做出的一个基本的实现选择。而因为JNWSpringAnimation简单地为我们开发了一个依然是Core Animation对象的CAKeyframeAnimation,我们还是需要设置动画模型层的最终值来在完成时保持住。

Pop是完全不同的!

Pop不使用Core Animation来执行任何它提供的动画功能。不同之处在于它设置了一个特殊的时间对象来每1/60秒执行一次。那个每秒执行60次的代码会直接基于下一个你在弹簧动作中定义的位置更新任何你想要的属性。没有什么特别的、额外的layer添加到你的元素中去,Pop直接在UIView或者CALayer上改变属性,或者,有趣地在任何你想要的对象类型上改变。这意味着在动画中的任何时候,你都可以直接接触改变的属性的当前值而不用跳到任何表现层。并且更好的是,你不需要单独设置最终值让动画在那逗留,因为动画始终在实际的真实值上工作。

这个Pop用来支撑整个框架的时间对象是CADisplayLink,它可以看做是NSTimer的一个更高级版本,NSTimer是Mac游戏开发者常年用来在他们的Mac和iOS游戏中一帧帧运行代码的。NSTimer可以在你想要的任何时候调用任何你想调用的代码,不断地重复或者只调用一次。如果你想每5秒钟调用一次代码就可以使用NSTimer来做。或者如果你想要每秒调用代码60次,也可以用NSTimer来做,但当这么快地调用代码的时候(比如每次运动一点点像素,一步步地动画一个界面元素),这个时间对象就会失去准确的同步刷新频率,你可能会丢失一些帧,从而导致一些奇怪的短暂跳跃。

这就是CADisplayLink施展之处。CADisplayLink就是设计来避免这个问题的,因为它不是设置时间间隔,它一遍遍地调用你的方法的速率完全取决于屏幕的刷新频率。它随着屏幕的刷新来启动你的代码,这样你就有了最好的机会来每秒更新你的界面60次(平滑感知动作的时间)。这就是Pop用来将动画一像素一像素、1/60秒一次推动的方法。

让我们看一些简单的Pop动画时如何工作的。

  1. // 添加我们的红球到界面上
  2. UIView *redBall = [[UIView alloc] initWithFrame:CGRectMake(300, 300, 75, 75)];
  3. redBall.backgroundColor = [UIColor redColor];
  4. redBall.layer.cornerRadius = 75/2; // Half the width
  5. [self.window addSubview:redBall];
  6. POPSpringAnimation *scale =
  7. [POPSpringAnimation animationWithPropertyNamed:kPOPViewScaleXY];
  8. scale.toValue = [NSValue valueWithCGPoint:CGPointMake(2, 2)];
  9. scale.springBounciness = 20.0f; // Between 0-20
  10. scale.springSpeed = 1.0f; // Between 0-20
  11. [redBall pop_addAnimation:scale forKey:@"scale"];

这就是这个代码产生的动画。


SECTION 6 - 图2


很有弹性!我们已经谈论了很多关于如何用JNWSpringAnimation和Core Animation创建弹簧动画的内容,现在来看看Pop的方法。

  1. POPSpringAnimation *scale =
  2. [POPSpringAnimation animationWithPropertyNamed:kPOPViewScaleXY];
  3. scale.toValue = [NSValue valueWithCGPoint:CGPointMake(2, 2)];
  4. scale.springBounciness = 20.0f; // Between 0-20
  5. scale.springSpeed = 1.0f; // Between 0-20
  6. [redBall pop_addAnimation:scale forKey:@"scaleAnimation"];

首先,我们创建了一个新的POPSpringAnimation对象。它被设计用 +animationWithPropertyNamed: 方法来初始化,获取参数来告诉Pop你想要动画什么属性。这非常像我们在JNWSpringAnimation中添加的关键路径值,但不是一个简单的字符串,Pop设计了很多值,这样你就不需要记住字符串。这里是一小部分Pop随时可以动画的属性。

  • kPOPViewAlpha——视图的透明度
  • kPOPViewFrame——视图的整体框架
  • kPOPViewScaleXY——视图的拉伸(X和Y轴)
  • kPOPViewBackgroundColor——视图的背景色
  • kPOPLayerCornerRadius——layer的角的度数
  • kPOPLayerRotation——layer的旋转度
  • kPOPLayerShadowRadius——layer下阴影的尺寸

所有的属性列表可以在Pop的GitHub找到。那是一个很长很长的清单,并且由于开发者一直在贡献,清单还在不断增长。

你可能注意到这些属性的命名有一些有趣的地方。我们有一个名为kPOPViewAlpha的属性,而另一个又名为kPOPLayerRotation。Pop酷的地方在于基于你传入的属性,你可以操作UIView的属性,也可以操作CALayer的属性。这完全取决于你,你只需要让Pop知道那一长串它支持的属性清单中你想要动画的是哪一个,无论它是一个view属性、layer属性或者任何类型的属性。Pop允许你更新任何你想要的类型的变量,甚至是与界面动画无关的。

我们设置了 toValue为 [NSValue valueWithCGPoint:CGPointMake(2, 2)] ,看起来可能会有点奇怪。为什么我们要将{2,2}这个点(我们的X和Y拉伸值)放到一个NSValue对象中去?好吧,这就是Pop工作的方式,它期待传到toValue参数中的是一个准确类型的值。而这个值得类型取决于我们添加的类型。它总是期待一个对象,在这个例子中,它想要一个CGPoint转化成的NSValue对象。不幸的是Pop的这个部分在文档中有点难懂,但随着开发者的贡献它也在变得更好。

至于要考虑的fromValue,我们在这个例子中没有设置它,因为Pop做了一些很酷的事情:如果你不设置它,它就会自动计算当前的开始值,并从这里开始。太赞了!

就如JNWSpringAnimation一样,你可以调整想要模仿的弹簧动作的属性。这里是上个例子中相关的部分。

  1. scale.springBounciness = 20.0f; // Between 0-20
  2. scale.springSpeed = 1.0f; // Between 0-20

Pop允许你调整弹簧的弹性和速度。每个值都可以从0到20.就如iOS 7中基于block的弹簧动画一样,这些值都是算入弹簧动作方程式的真实值的一个抽象。而不同于iOS 7的是,我认为Pop在抽象这些值时做的很棒,我还从没用Pop创建过一个看起来不自然或者违反物理法则的弹簧动作。

如果你想要调整动作方程使用的真实值,你也可以深入到一个更深的层次来操作它们。

  1. scale.dynamicsFriction = 20;
  2. scale.dynamicsMass = 1;
  3. scale.dynamicsTension = 300;

这些值类似于JNWSpringAnimation中使用的值,但不完全一样,所以如果你想要准确地将一个JNWSpringAnimation变成Pop,就需要进行一些调整。幸运的是,springBounciness和springSpeed值在控制弹簧的动作上已经做得很好了,所以我经常就直接使用它们。

让我们看看弹性值的调整会如何影响动画。


SECTION 6 - 图3


这三个球的速度都是10,。红球的弹性是5,篮球是12,绿球是20。

最终,我们将动画添加到我们想要动画的对象上去。

  1. [redBall pop_addAnimation:scale forKey:@"scaleAnimation"];

我们在视图上调用 -pop_addAnimation:forKey: 方法,并动画对象放入 pop_addAnimation:,然后将“scale”放入 forKey:。不同于JNWSpringAnimation和其他Core Animation的是,我们传入的键不需要匹配我们动画的属性。这个键只是视图上这个动画的一个唯一的名字,可以是你想要的任何值。如果你想要在运行的时候获取一个动画,你可以通过这个键来询问视图或者layer的Pop动画,这就是它的用处。

现在让我们来一次性添加一些不同的动画,每个都动画不同的属性。在我们展示代码之前,这里是它看起来的样子。


SECTION 6 - 图4


这个动画做了四件事情:视图的尺寸拉大,移动到了右边,旋转,并且还改变了背景色。这是我们添加的四个分开的动画,并且有四个分开的动画对象,每个表示一个不同的动画。

  1. POPSpringAnimation *scale =
  2. [POPSpringAnimation animationWithPropertyNamed:kPOPViewScaleXY];
  3. scale.toValue = [NSValue valueWithCGPoint:CGPointMake(1.5, 1.5)];
  4. scale.springBounciness = 15;
  5. scale.springSpeed = 5.0f;
  6. [orangeSquare pop_addAnimation:scale forKey:@"scale"];
  7. POPSpringAnimation *move =
  8. [POPSpringAnimation animationWithPropertyNamed:kPOPLayerPositionX];
  9. move.toValue = @(500);
  10. move.springBounciness = 15;
  11. move.springSpeed = 5.0f;
  12. [orangeSquare.layer pop_addAnimation:move forKey:@"position"];
  13. POPSpringAnimation *spin =
  14. [POPSpringAnimation animationWithPropertyNamed:kPOPLayerRotation];
  15. spin.toValue = @(M_PI*4);
  16. spin.springBounciness = 15;
  17. spin.springSpeed = 5.0f;
  18. [orangeSquare.layer pop_addAnimation:spin forKey:@"spin"];
  19. POPSpringAnimation *color =
  20. [POPSpringAnimation animationWithPropertyNamed:kPOPViewBackgroundColor];
  21. color.toValue = [UIColor greenColor];
  22. color.springBounciness = 15;
  23. color.springSpeed = 5.0f;
  24. [orangeSquare pop_addAnimation:color forKey:@"colorChange"];

我们使用了操作下面这些属性的动画:kPOPViewScaleXY、kPOPLayerPositionX、kPOPLayerRotation、kPOPViewBackgroundColor。两个动画时关于视图的,两个动画时关于layer的。

如果你观察一下我们设置为最终值的toValue变量,就可以看到一些不同的设置方法。如我之前所说,Pop一个有趣的(也有点烦人的?)方面在于Pop期望toValue改变的值取决于你要动画的属性。对于拉伸来说,我们已经说过了它想要一个NSValue对象。对于X位置动画,我们可以直接使用Objective-C的快捷方式@(500)来简单地给对象带来500.对于旋转,我们同样使用了特殊的@()语法。对于颜色我们设定了一个UIColor对象。所以你可以看到,因为Pop支撑了太多的动画属性,就有一些需要被理解的细微差别。我曾经混淆了NSValue包装的CGPoint`,并且盯着我的代码看了30秒才意识到它想要一些不同的值。

是时候用Pop来构建一些酷的东西了。

构建立即响应的按钮

你玩过Loren Brichter的游戏Letterpress吗?我很喜欢的Loren构建的一个关于界面的东西可能不是每个人都明显喜欢的:我喜欢每个按钮在用户按下时立即切换到一个不同的状态的样子。绝对不会延迟。这不是一个简单实现的行为,因为即使你可以将一个图片设为UIButtonUIControlStateHighlighted状态图,它也只会在点击发生后一小会启动,而且它不允许更进一步的代码来运行它。如果我想要在用户点击一个UIButton后立即运行一个动画,我就不得不自己写一个简单的自定义按钮类。但首先,先来看一看我们要构建的是什么。


SECTION 6 - 图5


如果我想要在用户点击后立即运行代码,我就不得不自己写一个好的UIButton子类,这样我就可以重写一些方法,即 -touchesBegan:withEvent: 和 -touchesEnded:withEvent:。iOS中的每个界面的控制都从UIResponder继承了这些方法,它是一个处理所有触摸控制事件的父类。有了子类,我就可以塞一些自己的代码来在这些方法启动的时候运行。来看看DTCTestButton的实现文件,这是我们的按钮子类,会为我们处理一些魔法。

  1. # import "DTCTestButton.h"
  2. # import "POP.h"
  3. @implementation DTCTestButton
  4. - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  5. // 自定义一些按钮第一次被点击时要运行的代码
  6. [super touchesBegan:touches withEvent:event];
  7. }
  8. - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
  9. // 自定义一些按钮不再被点击时要运行的代码
  10. [super touchesEnded:touches withEvent:event];
  11. }
  12. @end

我们这里只定义了两个方法,我们想要将我们的代码放到这些方法里面去。当子类化一个苹果提供的对象,比如UIButton时,做一个好的城市居民并确保调用super的关于这些方法的实现是很重要的,因为我们不知道苹果在这两个方法中需要运行什么代码,而且不想破坏按钮的默认行为。我们调用super后,就可以在这两个方法中添加任何我们想要的行为。

让我们添加一个Pop动画到 -touchesBegan:withEvent:中去。

  1. POPSpringAnimation *scale = [self pop_animationForKey:@"scale"];
  2. if (scale) {
  3. scale.toValue = [NSValue valueWithCGPoint:CGPointMake(0.8, 0.8)];
  4. } else {
  5. scale = [POPSpringAnimation animationWithPropertyNamed:kPOPViewScaleXY];
  6. scale.toValue = [NSValue valueWithCGPoint:CGPointMake(0.8, 0.8)];
  7. scale.springBounciness = 20;
  8. scale.springSpeed = 18.0f;
  9. [self pop_addAnimation:scale forKey:@"scale"];
  10. }

这和我们之前写的Pop代码有点不同。当使用Pop来构建好的响应动画去关联触摸动作时,一个聪明的做法是看看是否已经有一个Pop动画关联到这个视图或者layer了。如果有,只要更新已经存在的动画的toValue属性就可以了。Pop知道当前的值是什么并且已经设置好弹性和速度变量了,所以你不用做任何其他的事情。这避免了添加另一个错误的Pop动画来操作同样的值(在这个例子中,是kPOPViewScaleXY),这会造成愚蠢的结果。通过使用现存的动画,Pop可以优雅地从它的当前位置修改到你设置的新的toValue并进行一个漂亮、平滑的过度。这也是为什么Pop动画有一个名字:这样你就可以通过给出你之前设置的动画的名字来询问视图或者layer它们是否有已经添加进去的Pop动画并获取到动画对象。

如果动画不是已经存在,我们就和平常一样创建一个新的Pop动画对象,设置弹簧的动作属性,比如弹性,设置toValue,然后添加动画到视图或者layer上。在这个例子中,我们动画了视图的尺寸,所以我们将动画添加到视图上。

现在让我们在触摸事件结束时做同样的事情。这次代码放在 -touchesEnded:withEvent:中。

  1. POPSpringAnimation *scale = [self pop_animationForKey:@"scale"];
  2. if (scale) {
  3. scale.toValue = [NSValue valueWithCGPoint:CGPointMake(1.0, 1.0)];
  4. } else {
  5. scale = [POPSpringAnimation animationWithPropertyNamed:kPOPViewScaleXY];
  6. scale.toValue = [NSValue valueWithCGPoint:CGPointMake(1.0, 1.0)];
  7. scale.springBounciness = 20;
  8. scale.springSpeed = 18.0f;
  9. [self pop_addAnimation:scale forKey:@"scale"];
  10. }

如果你看看触摸事件开始时0.8的toValue以及触摸结束时的1.0的toValue,你就可以猜到整个动画会在用户点击按钮时稍微收缩按钮的尺寸,然后会在他们停止触摸时弹回完整的尺寸。完全正确!这里是它现在的样子。


SECTION 6 - 图6


很有意思!让我们再加一点点旋转动画来增色。它基本上和我们已经添加的代码一样,只是重复它,修改动画类型,然后改变toValue值。这里是完整的代码,以及一些注释。

  1. // 当用户开始点击时立即调用
  2. - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  3. // 看动画是否已经被添加到视图或者layer上
  4. POPSpringAnimation *scale = [self pop_animationForKey:@"scale"];
  5. POPSpringAnimation *rotate = [self.layer pop_animationForKey:@"rotate"];
  6. // 如果scale动画已经存在,就设置toValue
  7. if (scale) {
  8. scale.toValue = [NSValue valueWithCGPoint:CGPointMake(0.8, 0.8)];
  9. } else {
  10. // 如果不存在,就创建并添加它
  11. scale = [POPSpringAnimation animationWithPropertyNamed:kPOPViewScaleXY];
  12. scale.toValue = [NSValue valueWithCGPoint:CGPointMake(0.8, 0.8)];
  13. scale.springBounciness = 20;
  14. scale.springSpeed = 18.0f;
  15. [self pop_addAnimation:scale forKey:@"scale"];
  16. }
  17. // 如果旋转动画已经存在,就设置toValue
  18. if (rotate) {
  19. rotate.toValue = @(M_PI/6); // 旋转到1/6th π角度
  20. } else {
  21. // 旋转动画时layer上的,所以我们添加到layer上去
  22. rotate = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerRotation];
  23. rotate.toValue = @(M_PI/6);
  24. rotate.springBounciness = 20;
  25. rotate.springSpeed = 18.0f;
  26. // 添加到layer上,而不是view
  27. [self.layer pop_addAnimation:rotate forKey:@"rotate"];
  28. }
  29. [super touchesBegan:touches withEvent:event];
  30. }
  31. // 在用户离开手指时立即调用
  32. - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
  33. // 看动画是否存在(由于这是用户离开时,基本是已经存在的)
  34. POPSpringAnimation *scale = [self pop_animationForKey:@"scale"];
  35. POPSpringAnimation *rotate = [self pop_animationForKey:@"rotate"];
  36. if (scale) {
  37. // 拉伸回1.0的完整尺寸
  38. scale.toValue = [NSValue valueWithCGPoint:CGPointMake(1.0, 1.0)];
  39. } else {
  40. scale = [POPSpringAnimation animationWithPropertyNamed:kPOPViewScaleXY];
  41. scale.toValue = [NSValue valueWithCGPoint:CGPointMake(1.0, 1.0)];
  42. scale.springBounciness = 20;
  43. scale.springSpeed = 18.0f;
  44. [self pop_addAnimation:scale forKey:@"scale"];
  45. }
  46. if (rotate) {
  47. // 旋转回0角度的初始位置
  48. rotate.toValue = @(0);
  49. } else {
  50. rotate = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerRotation];
  51. rotate.toValue = @(0);
  52. rotate.springBounciness = 20;
  53. rotate.springSpeed = 18.0f;
  54. // 再次确保添加你的layer动画到layer上去。我曾经失误过很多次,这会导致一个有趣的bug :)
  55. [self.layer pop_addAnimation:rotate forKey:@"rotate"];
  56. }
  57. [super touchesEnded:touches withEvent:event];
  58. }

动画代码是重复的。简单,但是重复。它的一个缺点是需要很多行代码来完整构建你的动画,但优点是能让你练习写很多动画代码,所以我认为你可以学的更快。

再一次,这里是我们构建的最终动画。它是一个很有趣的效果,会在用户点击按钮时立即启动,它会让你的界面感觉响应很快。这里的弹性效果很显著,所以当添加动画到你的真实app界面时,去使用一会app的动画,并确保它们的速度和动作时合适且不分散注意力的。


SECTION 6 - 图7


现在让我们来用Pop做一些有趣的东西!

构建一个动画的汉堡按钮

汉堡按钮和滑出式菜单可能是整个产业中最两极分化的界面元素。苹果的狂热支持者反对汉堡按钮和相应的滑出式菜单,说设计师(以及工程师、产品经理和CEO们)喜欢在那堆积尽可能多的东西,因为你有了很多垂直地空间。

我不能说我不认同,因为用户测试表明用户其实不太使用滑出式菜单,但可能我是一个伪君子,因为我还是在我的iPhone app Interesting中使用了一个汉堡按钮,这样看来我也是一个问题!不论如何,如果你打算使用一个汉堡按钮,你也要让它有趣、讨喜来让人们点击。

所以一个汉堡按钮的基本元素是什么?典型的是有三个水平栏来描绘常规状态,然后如果你想要精致一点的话,你可以在菜单打开时将栏换成X形。当然了,Pop就是用来让用户界面开发师变得精致的,所以为什么不给这个过渡加上一些动画呢?

稍微看一下我们要构建的是什么。


SECTION 6 - 图8


开始时,我们有一个圆形的黑色按钮,里面中间有一个汉堡形的线。当按钮被点击时,它动画到一个稍微小一点的尺寸。但点击结束时,线会动画城红色的X。当点击X状态时,动画会回到原始的颜色和位置。这是一个明显简化的关于发生了什么的解释,让我们来看看代码。

  1. // 将汉堡按钮添加到屏幕上
  2. self.hamburgerButton = [DTCTestButton buttonWithType:UIButtonTypeCustom];
  3. [self.hamburgerButton addTarget:self action:@selector(didTapHamburgerButton:)
  4. forControlEvents:UIControlEventTouchUpInside];
  5. self.hamburgerButton.backgroundColor = [UIColor blackColor];
  6. [self.hamburgerButton setFrame:CGRectMake(200, 200, 150, 150)];
  7. self.hamburgerButton.layer.cornerRadius = 75;
  8. [self.window addSubview:self.hamburgerButton];

我们将汉堡按钮设为类的@property,这样我们就可以通过self.hamburgerButton来调用它。它使用了我们在之前的例子里创建的同样的按钮子类,这样我们就可以在用户点击时立即获取好的有弹性的感觉。我们还设置按钮在用户松开他们点击按钮的手指时的事件UIControlEventTouchUpInside下调用我们的方法 -didTapHamburgerButton: 。我们还将按钮设为黑色的并且有圆角。

这里是我们目前有的样子。


SECTION 6 - 图9


该把我们的汉堡线作为子视图添加到按钮上了。

  1. CGFloat sectionWidth = 80;
  2. CGFloat sectionHeight = 11;
  3. // 添加上、中、下汉堡线
  4. self.top = [[UIView alloc] initWithFrame:
  5. CGRectMake(self.hamburgerButton.bounds.size.width/2 - sectionWidth/2,
  6. 40, sectionWidth, sectionHeight)];
  7. self.top.backgroundColor = [UIColor whiteColor];
  8. self.top.userInteractionEnabled = NO;
  9. self.top.layer.cornerRadius = sectionHeight/2;
  10. [self.hamburgerButton addSubview:self.top];
  11. self.middle = [[UIView alloc] initWithFrame:
  12. CGRectMake(self.hamburgerButton.bounds.size.width/2 - sectionWidth/2,
  13. 69, sectionWidth, sectionHeight)];
  14. self.middle.backgroundColor = [UIColor whiteColor];
  15. self.middle.userInteractionEnabled = NO;
  16. self.middle.layer.cornerRadius = sectionHeight/2;
  17. [self.hamburgerButton addSubview:self.middle];
  18. self.bottom = [[UIView alloc] initWithFrame:
  19. CGRectMake(self.hamburgerButton.bounds.size.width/2 - sectionWidth/2,
  20. 99, sectionWidth, sectionHeight)];
  21. self.bottom.backgroundColor = [UIColor whiteColor];
  22. self.bottom.userInteractionEnabled = NO;
  23. self.bottom.layer.cornerRadius = sectionHeight/2;
  24. [self.hamburgerButton addSubview:self.bottom];

我设置了一些我们会在这个代码中重复用到的CGFloat的数字变量。我添加了三个UIView对象到主汉堡按钮上,每个都是白色背景的圆角矩形。它们都放置在大汉堡按钮的水平中心,并在垂直方向上分离。这段代码中最有趣的地方在于我设置这些每个视图的UserInteractionEnabled属性为NO。如果我们不对这些视图这样做,如果直接点击按钮,会吞没触摸事件并且不会传递到实际的完整汉堡按钮上。这里是现在看起来的样子。


SECTION 6 - 图10


现在不添加任何代码,因为这个按钮是我们在之前的例子中创建的UIButton子类DTCTestButton类型的,已经有了一些动画了。


SECTION 6 - 图11


我们现在真正想做的是让线动画交叉变成X。所以让我们进入我们的 didTapHamburgerButton: 方法,我们每次点击这个按钮都会调用它,来看一看我们要做什么。

  1. - (void)didTapHamburgerButton:(id)sender {
  2. if (self.hamburgerOpen) {
  3. self.hamburgerOpen = NO;
  4. // 添加把X变回三条线的动画
  5. } else {
  6. self.hamburgerOpen = YES;
  7. // 添加把三条线变成X的动画
  8. }
  9. }

我们需要一种方式来记录按钮是否被动画成X了(如果是一个完整的app,也就是滑出式菜单是否被推出了),所以我天界了一个@property(BOOL)hamburgerOpen到类上,这样我们就可以每次都设置它并且知道按钮当前的状态。这是我们在这个方法中做任何事情前都应该先检查的变量,因为它的值会指示我们需要执行何种类型的动画。

让我们从初始状态开始,也就是self.hamburgerOpen是false,并且代码会从上面的else开始执行。在进入实际的代码之前,让我们讨论一个计划来将三条水平线变成红色的X。

  1. 我们要将顶部的线向下旋转到45度角
  2. 我们要将底部的线向上旋转45度角
  3. 我们不需要中间的线所以就直接淡出它
  4. 旋转后的线可能不会很好地交叉,所以我们要动画它们到准确的位置
  5. 将两根交叉的线从白色动画到红色

如果你注意了,可能会意识到我们有很多动画要执行,你是对的。这不是一个不重要的例子,它由多个单独的动画组成,但如大多数动画代码一样,它会一步一步执行。我们一直一次只写一个动画block,除了这次有很多动画!让我们先从淡出中间行开始。

  1. // 淡出中间行
  2. [UIView animateWithDuration:0.2 animations:^{
  3. self.middle.alpha = 0.0f;
  4. }];

只是一个简单的基于block的UIView动画。这个淡出动画的目标是让中间行消失,所以我们不需要做任何其他的事情。嗷,我应该提一下,我将顶部、中间和底部的线都作为类的@property了,这就是为什么我们可以用self.前缀获取这个变量。

接下来,让我们把省下来的两根线从白色动画成红色。幸运的是,Pop让它变得很简单,你只需要设置toValue的颜色为你最终想要的颜色,它会自动插入中间的颜色。

  1. // 将顶部和顶部线的颜色变为红色
  2. POPSpringAnimation *topColor = [self.top pop_animationForKey:@"topColor"];
  3. if (topColor) {
  4. topColor.toValue = [UIColor redColor];
  5. } else {
  6. topColor =
  7. [POPSpringAnimation animationWithPropertyNamed:kPOPViewBackgroundColor];
  8. topColor.toValue = [UIColor redColor];
  9. topColor.springBounciness = 0;
  10. topColor.springSpeed = 18.0f;
  11. [self.top pop_addAnimation:topColor forKey:@"topColor"];
  12. }
  13. POPSpringAnimation *bottomColor = [self.bottom pop_animationForKey:@"bottomColor"];
  14. if (bottomColor) {
  15. bottomColor.toValue = [UIColor redColor];
  16. } else {
  17. bottomColor =
  18. [POPSpringAnimation animationWithPropertyNamed:kPOPViewBackgroundColor];
  19. bottomColor.toValue = [UIColor redColor];
  20. bottomColor.springBounciness = 0;
  21. bottomColor.springSpeed = 18.0f;
  22. [self.bottom pop_addAnimation:bottomColor forKey:@"bottomColor"];
  23. }

就如我们之前的按钮例子一样,当我们重复一个用户动作时,我们需要确保我们的动画时流动的,即使用户疯狂地快速点击按钮并打断我们的动画。从当前值开始动画非常重要,这样一切就是自然的。这就是为什么我在创建并添加新动画前做了一个topColor和bottomColor动画对象是否已经存在的检查。如果它们存在,我们就使用存在的动画并且只设置一个新的toValue,如果不存在,我们就构建一个新的动画对象。还有,我对这个颜色过渡没有使用任何弹性,因为我确实不想颜色动画过度迭代红色然后变成一些奇怪的颜色。

这时候当用户点击按钮时我们还没有X,但已经有了这个可爱的视觉了。


SECTION 6 - 图12


我们现在还剩两个动画,但它们比较大,需要一些思考。我们需要将顶部的线顺时针旋转45度(所以右边向下倾斜),然后我们需要底部的线逆时针旋转45度(所以右边向上倾斜)。逆时针旋转意味着我们需要旋转一个负值,所以是-45度。当然了,动画不会接受度数值,它们需要角度值,45度在角度上是π/4。来做一些旋转动画。

  1. // 旋转顶部的线来构成X
  2. POPSpringAnimation *topRotate =
  3. [self.top.layer pop_animationForKey:@"topRotate"];
  4. if (topRotate) {
  5. topRotate.toValue = @(-M_PI/4);
  6. } else {
  7. topRotate =
  8. [POPSpringAnimation animationWithPropertyNamed:kPOPLayerRotation];
  9. topRotate.toValue = @(-M_PI/4);
  10. topRotate.springBounciness = 11;
  11. topRotate.springSpeed = 18.0f;
  12. [self.top.layer pop_addAnimation:topRotate forKey:@"topRotate"];
  13. }
  14. // 旋转底部的线来构成X
  15. POPSpringAnimation *bottomRotate =
  16. [self.bottom.layer pop_animationForKey:@"bottomRotate"];
  17. if (bottomRotate) {
  18. bottomRotate.toValue = @(M_PI/4);
  19. } else {
  20. bottomRotate =
  21. [POPSpringAnimation animationWithPropertyNamed:kPOPLayerRotation];
  22. bottomRotate.toValue = @(M_PI/4);
  23. bottomRotate.springBounciness = 11;
  24. bottomRotate.springSpeed = 18.0f;
  25. [self.bottom.layer pop_addAnimation:bottomRotate forKey:@"bottomRotate"];
  26. }

Pop的旋转动画时在layer上操作的(看到kPOPLayerRotation了没),所以我们将动画添加到支撑这些视图的layer上。

我们向上旋转一根线、向下旋转一根线所以它们应该在中间交叉,对吗?让我们看看我们得到了什么。


SECTION 6 - 图13


额,直观地说,这可能并不是你期待的样子。旋转动画让线条变成这样的原因是没跟线条都是围绕着它们layer的中心旋转的。所以这些视图会像跷跷板一样旋转,而不是我们想要的在中间交叉的样子。我们可以改变layer旋转的锚点,但这有点麻烦,因为这样做会重定位layer并且我们需要调整框架,这纯粹是找麻烦。所以,更简单的做法是,我们可以就将顶部线下移一点,然后将底部的线上移一旦,然后重叠它们。

  1. // 重定位顶部的线到按钮的中间
  2. POPSpringAnimation *topPosition =
  3. [self.top.layer pop_animationForKey:@"topPosition"];
  4. if (topPosition) {
  5. topPosition.toValue = @(29);
  6. } else {
  7. topPosition =
  8. [POPSpringAnimation animationWithPropertyNamed:kPOPLayerTranslationY];
  9. topPosition.toValue = @(29);
  10. topPosition.springBounciness = 0;
  11. topPosition.springSpeed = 18.0f;
  12. [self.top.layer pop_addAnimation:topPosition forKey:@"topPosition"];
  13. }
  14. // 重定位底部的线到按钮的中间
  15. POPSpringAnimation *bottomPosition =
  16. [self.bottom.layer pop_animationForKey:@"bottomPosition"];
  17. if (bottomPosition) {
  18. bottomPosition.toValue = @(-29);
  19. } else {
  20. bottomPosition =
  21. [POPSpringAnimation animationWithPropertyNamed:kPOPLayerTranslationY];
  22. bottomPosition.toValue = @(-29);
  23. bottomPosition.springBounciness = 0;
  24. bottomPosition.springSpeed = 18.0f;
  25. [self.bottom.layer pop_addAnimation:bottomPosition forKey:@"bottomPosition"];
  26. }

经过一些测试和试错,我决定将顶部的线下移29像素,底部的线上移29像素,这样会让它们重合的最好。你也可以做一些三角几何计算来得出这个值。我们使用kPOPLayerTranslationY动画来让两根线旋转到按钮中间的X。


SECTION 6 - 图14


完成了!很好吧?现在,当你点击按钮,它会将三根线变成两根线,但当用户再次点击时会发生什么呢?这时候,不会发生任何事情,因为我们没有实现任何其他条件分支的逻辑来将X变回三根线。幸运的是,我们可以很简单地复制粘贴动画,但是要将toValue值改为初始值。比如说,我们需要将两根线都旋转回0度,记得要移动29像素,并将它们的颜色改回白色。还有要将中间的线淡入回100%不透明。这样就全部完成了,我们得到了一个漂亮的汉堡按钮。


SECTION 6 - 图15


所以你应该使用哪个动画框架呢?

我们讨论了iOS7的弹簧动画、JNWSpringAnimation和Facebook的Pop,很自然的你会想知道该使用哪个动画框架。有一个我不是仅仅选一个动画框架并单独专注于它的原因,我想要提供一个大的关于你可以构建动画和iOS原生界面的不同方式的概览。

在我的app中,我倾向于根据不同的目的对三种框架都进行使用。如果我只是非常简单地淡出一个视图,我们直接使用基于block的UIView动画,因为它们的代码很少。如果我在做一个指数延迟类型的动画(缓慢、平稳地移动到它的最终值而没有弹性)我喜欢JNWSpringAnimation设置不同属性时得到的动作感觉。对于完全的弹簧动画,我通常会分离JNWSpringAnimation和Pop,但最近学习了更多的Pop,因为我很喜欢不重新设置它的最终值就可以直接操作一个视图或者layer的属性。

当你构建你自己的动画时,你可以选择任何对你来说工作得最好的。我希望我提供了一个很好的选择范围。

这是开始,不是结束

在这个指南中你学习了关于思考、设计和构建动画的基础。我记得当我第一次进入动画开发并让我的第一个对象在屏幕上移动的时候,它完全使我震惊了。它真的改变了我,和我的工作。我不再是仅仅将静止的app模型放到Photoshop中,或者在Keynote或其他工具中做一些可点击的模型,我真的构建了可以运行在我的手机的上界面!那是2008年,在真正酷的弹簧动画框架出现之前,所以当时只是使用了简单的淡入淡出。现在,iOS 7和iOS 8完全不一样了,还有像JNWSpringAnimation和Facebook Pop的非常棒的第三方动画框架。

当你在设计和构建你自己的动画时最后的一个建议:思考动画的目的。是为了集中用户的注意力吗?在不同的界面状态之间过渡?还是仅仅用有趣的动作来取悦用户?这些应该是引导你要在你的app中实现的动画的设计以及动画类型的根本问题。

最后,非常感谢你阅读本指南。如果你有任何问题或评价,或者想要我看看你做的一些很酷的东西并给出反馈,请随便发Twitter给我或者直接给我发邮件。

祝好运!
-Mike


终于又翻译完毕了一本书,看了一下第一篇文章的时间,竟然已经四个月了,期间断断续续,有长有短,到了最后竟有点不舍。这本书没有纸质的,是纯网页版,想来也是,毕竟内容中有很多动图,也许这样才更适合用来做动画教学吧。整本书一开始就介绍了很多炫酷的动画效果,之前自己只是学习了简单的UIView动画,对于动画也有了一点自己的心得,其实如作者的好习惯,每次观察一个动画时都先去进行拆解,看看都做了些什么,然后再一个一个去实现,这样想来,大部分动画都有了实现的勇气了,也不是看上去那么难的。我很喜欢做界面动画,喜欢好看的界面,平常也会去感受一些设计独到的app,发现好的动画,自己也会思考一下怎么实现的,这非常有意思,我想,这才是我选择做移动端开发的潜在目的吧,喜欢好看的界面,也希望自己以后能做出更多有意思的app~
最后提一句,如果这本书的翻译对大家有一些帮助的话,不吝给github点个星呗~~

更多内容参见我的博客
回到目录