是什么样的滑动能让人有‘哇哦’的效果呢? 滑动是最完美地展示了通过触摸屏直接操作的例子。滚动视图遵从于你的手指,当你的手指离开屏幕时,视图会自然地继续滑动直到该停止的时候停止。它用自然的方式减速,甚至在快到界限的时候也能表现出细腻的弹力效果。滑动在任何时候都保持相应,并且看上去非常真实。 动画的状态 在iOS中的大部分动画仍然没有按照最初iPhone指定的滑动标准实现。这里有很多动画一旦它们运行就不能交互(比如说解锁动画,主界面中打开文件夹和关闭文件夹的动画,和导航栏切换的动画,还有很多)。 然而现在有一些应用给我一种始终在控制动画的体验,我们可以直接操作那些我在用的动画。当我们将这些应用和其他的应用相比较之后,我们就能感觉到明显的区别。这些应用中最优秀的有最初的TwitteriPadapp,和现在的FacebookPaper。但目前,使用直接操作为主并且可以中断动画的应用仍然很少。这就给我们做出更好的应用提供了机会,让我们的应用有更不同的,更高质量的体验。 真实交互式动画的挑战 当我们用UIView或者CAAnimation来实现交互式动画时会有两个大问题:这些动画会将你在屏幕上的内容和layer上的实际的特定属性分离开来,并且他们直接操作这些特定属性。 模型(Model)和显示(Presentation)的分离 CoreAnimation是通过分离layer的模型属性和你在屏幕上看到的界面(显示层)的方式来设计的,这就导致我们很难去创建一个可以在任何时候能交互的动画,因为在动画时,模型和界面已经不能匹配了。这时,我们不得不通过手动的方式来同步这两个的状态,来达到改变动画的效果: view。layer。centerview。layer。presentationLayer。 〔view。layerremoveAnimationForKey:animation〕; 添加新动画 直接控制vs间接控制 CAAnimation动画的更大的问题是它们是直接在layer上对属性进行操作的。这意味着什么呢?比如我们想指定一个layer从坐标为(100,100)的位置运动到(300,300)的位置,但是在它运动到中间的时候,我们想它停下来并且让它回到它原来的位置,事情就变得非常复杂了。如果你只是简单地删除当前的动画然后再添加一个新的,那么这个layer的速率就会不连续。 然而,我们想要的是一个漂亮的,流畅地减速和加速的动画。 只有通过间接操作动画才能达到上面的效果,比如通过模拟力在界面上的表现。新的动画需要用layer的当前速度矢量作为参数传入来达到流畅的效果。 看一下UIView中关于弹簧动画的API animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion: 你会注意到速率是个CGFloat。所以当我们给一个移动view的动画在其运动的方向上加一个初始的速率时,你没法告知动画这个view现在的运动状态,比如我们不知道要添加的动画的方向是不是和原来的view的速度方向垂直。为了使其成为可能,这个速度需要用向量来表示。 解决方案 让我们看一下我们怎样来正确实现一个可交互并且可以中断的动画。我们来做一个类似于控制中心板的东西来实现这个效果(点击视频下面的开始按钮): 这个控制板有两个状态:打开和关闭。你可以通过点击来切换这两个状态,或者通过上下拖动来调调整它向上或向下。我要将这个控制面板的所有状态都做到可以交互,甚至是在动画的过程中也可以,这是一个很大的挑战。比如,当你在这个控制板还没有切换到打开状态的动画过程中,你点击了它,那么它应该从现在这个点的位置马上回到关闭状态的位置。在现在很多的应用中,大部分都是用默认的动画API,你必须要等一个动画结束之后你才能做自己想做的事情。或者,如果你不等待的话,就会看到一个不连续的速度曲线。我们要解决这个问题。 UIKit力学 随着iOS7的发布,苹果向我们展示了一个叫UIKit力学的动画框架(可以参见WWDC2013sessions206和221)。UIKit力学是一个基于模拟物理引擎的框架,只要你添加指定的行为到动画对象上来实现UIDynamicItem协议就能实现很多动画。这个框架非常强大,并且它能够在多个物体间启用像是附着和碰撞这样的复杂行为。请看一下UIKitDynamicsCatalog,确认一下什么是可用的。 因为UIKit力学中的的动画是被间接驱动的,就像我在上面提到的,这使我们实现真实的交互式动画成为可能,它能在任何时候被中断并且拥有连续的加速度。同时,UIKit力学在物理层的抽象上能完全胜任我们一般情况下在用户界面中的所需要的所有动画。其实在大部分情况下,我们只会用到其中的一小部分功能。 定义行为 为了实现我们的控制板的行为,我们将使用UIkit力学中的两个不同行为:UIAttachmentBehavior和UIDynamicItemBehavior。附着行为用来扮演弹簧的角色,它将界面向目标点拉动。另一方面,我们用动态itembehvaior定义了比如摩擦系数这样的界面的内置属性。 我创建了一个我们自己的行为子类,以将这两个行为封装到我们的控制板上: interfacePaneBehavior:UIDynamicBehavior property(nonatomic)CGPointtargetP property(nonatomic)CGP (instancetype)initWithItem:() end 我们通过一个dynamicitem来初始化这个行为,然后就可以设置它的目标点和我们想要的任何速度。在内部,我们创建了附着行为和dynamicitem行为,并且将这些行为添加到我们自定义的行为中: (void)setup { UIAttachmentBehaviorattachmentBehavior〔〔UIAttachmentBehavioralloc〕initWithItem:self。itemattachedToAnchor:CGPointZero〕; attachmentBehavior。frequency3。5; attachmentBehavior。damping。4; attachmentBehavior。length0; 〔selfaddChildBehavior:attachmentBehavior〕; self。attachmentBehaviorattachmentB UIDynamicItemBehavioritemBehavior〔〔UIDynamicItemBehavioralloc〕initWithItems:〔self。item〕〕; itemBehavior。density100; itemBehavior。resistance10; 〔selfaddChildBehavior:itemBehavior〕; self。itemBehavioritemB } 为了用targetPoint和velocity属性来影响item的behavior,我们需要重写它们的setter方法,并且分别修改在附着行为和itembehaviors中的对应的属性。我们对目标点的setter方法来说,这个改动很简单: (void)setTargetPoint:(CGPoint)targetPoint { targetPointtargetP self。attachmentBehavior。anchorPointtargetP } 对于velocity属性,我们需要多做一些工作,因为dynamicitembehavior只允许相对地改变速度。这就意味如果我们要将velocity设置为绝对值,首先我们就需要得到当前的速度,然后再加上速度差才能得到我们的目标速度。 (void)setVelocity:(CGPoint)velocity { CGPointcurrentVelocity〔self。itemBehaviorlinearVelocityForItem:self。item〕; CGPointvelocityDeltaCGPointMake(velocity。xcurrentVelocity。x,velocity。ycurrentVelocity。y); 〔self。itemBehavioraddLinearVelocity:velocityDeltaforItem:self。item〕; } 将Behavior投入使用 我们的控制板有三个不同状态:在开始或结束位置的静止状态,正在被用户拖动的状态,以及在没有用户控制时运动到结束位置的动画状态。 为了将从直接操作状态(用户拖动这个滑动板)过渡到动画状态这个过程做的流畅,我们还有很多其他的事要做。当用户停止拖动控制板时,它会发送一个消息到它的delegate。根据这个方法,我们可以知道这个板应该朝哪个方向运动,然后在我们自定义的PaneBehavior上设置结束点,以及初始速度(这非常重要),并将行为添加到动画器中去,以此确保从拖动操作到动画状态这个过程能够非常流畅。 (void)draggableView:(DraggableView)viewdraggingEndedWithVelocity:(CGPoint)velocity { PaneStatetargetStatevelocity。0?PaneStateClosed:PaneStateO 〔selfanimatePaneToState:targetStateinitialVelocity:velocity〕; } (void)animatePaneToState:(PaneState)targetStateinitialVelocity:(CGPoint)velocity { if(!self。paneBehavior){ PaneBehaviorbehavior〔〔PaneBehavioralloc〕initWithItem:self。pane〕; self。paneB } self。paneBehavior。targetPoint〔selftargetPointForState:targetState〕; if(!CGPointEqualToPoint(velocity,CGPointZero)){ self。paneBehavior。 } 〔self。animatoraddBehavior:self。paneBehavior〕; self。paneStatetargetS } 一旦用户用他的手指再次触动控制板时,我必须要将所有的dynamicbehavior从animator删除,这样才不会影响控制板对拖动手势的响应: (void)draggableViewBeganDragging:(DraggableView)view { 〔self。animatorremoveAllBehaviors〕; } 我们不仅仅允许控制板可以被拖动,还要允许它可以被点击,让它可以从一个位置跳转到另一个位置以达到开关的效果。一旦点击事件发生,我们就会立即调整这个滑动板的目标位置。因为我们不能直接控制动画,但是通过弹力和摩擦力,我们的动画可以非常流畅地执行这个动作: (void)didTap:(UITapGestureRecognizer)tapRecognizer { PaneStatetargetStateself。paneStatePaneStateOpen?PaneStateClosed:PaneStateO 〔selfanimatePaneToState:targetStateinitialVelocity:CGPointZero〕; } 这样就实现了我们的大部分功能了。你可以在GitHub上查看完整的例子。 重申一点:UIKit力学可以通过在界面上模拟力来间接地驱动动画(我们的例子中,使用的是弹力和摩擦力)。这间接地使我们在任何时候都能以连续的速度曲线来与界面进行交互。 现在我们已经通过UIKit力学实现了整个交互,让我们回顾一下这个场景。这个例子的动画中我们只用了UIKit力学中一小部分功能,并且它的实现方式也非常简单。对于我们来说这是一个去理解它其中的过程的很好的例子,但是如果我们使用的环境中没有UIKit力学(比如说在Mac上),或者你的使用场景中不能很好的适用UIKit力学呢。 自己操作动画 至于在你的应用中大部分时间会用的动画,比如简单的弹力动画,我们控制它真的不难。我们可以做一个练习,来看看如何抛弃UIKit力学这个巨大的黑盒子,看要如何手动来实现一个简单的交互。想法非常简单:我们只要每秒修改这个view的frame60次。每一帧我们都基于当前速度和作用在view上的力来调整view的frame就可以了。 物理原理 首先让我们看一下我们需要知道的基础物理知识,这样我们才能实现出刚才使用UIKit力学实现的那种弹簧动画效果。为了简化问题,虽然引入第二个维度也是很直接的,但我们在这里只关注一维的情况(在我们的例子中就是这样的情况)。 我们的目标是依据控制面板当前的位置和上一次动画后到现在为止经过的时间,来计算它的新位置。我们可以把它表示成这样: yy0y 位置的偏移量可以通过速度和时间的函数来表达: yvt 这个速度可以通过前一次的速度加上速度偏移量算出来,这个速度偏移量是由力在view上的作用引起的。 vv0v 速度的变化可以通过作用在这个view上的冲量计算出来: v(Ft)m 现在,让我们看一下作用在这个界面上的力。为了得到弹簧效果,我们必须要将摩擦力和弹力结合起来: FFspringFfriction 弹力的计算方法我们可以从任何一本教科书中得到(编者注:简单的胡克定律): Fspringkx k是弹簧的劲度系数,x是view到目标结束位置的距离(也就是弹簧的长度)。因此,我们可以把它写成这样: Fspringkabs(ytargety0) 摩擦力和view的速度成正比: Ffrictionv 是一个简单的摩擦系数。你可以通过别的方式来计算摩擦力,但是这个方法能很好地做出我们想要的动画效果。 将上面的表达式放在一起,我们就可以算出作用在界面上的力: Fkabs(ytargety0)v 为了实现起来更简单点些,我们将view的质量设为1,这样我们就能计算在位置上的变化: y(v0(kabs(ytargety0)v)t)t 实现动画 为了实现这个动画,我们首先需要创建我们自己的Animator类,它将扮演驱动动画的角色。这个类使用了CADisplayLink,CADisplayLink是专门用来将绘图与屏幕刷新频率相同步的定时器。换句话说,如果你的动画是流畅的,这个定时器就会每秒调用你的方法60次。接下来,我们需要实现Animation协议来和我们的Animator一起工作。这个协议只有一个方法,animationTick:finished:。屏幕每次被刷新时都会调用这个方法,并且在方法中会得到两个参数:第一个参数是前一个frame的持续时间,第二个参数是一个指向BOOL的指针。当我们设置这个指针的值为YES时,我们就可以与Animator取得通讯并汇报动画完成; protocolA (void)animationTick:(CFTimeInterval)dtfinished:(BOOL) end 我们会在下面实现这个方法。首先,根据时间间隔我们来计算由弹力和摩擦力的合力。然后根据这个力来更新速度,并调整view的中心位置。最后,当这个速度降低并且view到达结束位置时,我们就停止这个动画: (void)animationTick:(CFTimeInterval)dtfinished:(BOOL)finished { staticconstfloatfrictionConstant20; staticconstfloatspringConstant300; CGFloattime(CGFloat) 摩擦力速度摩擦系数 CGPointfrictionForceCGPointMultiply(self。velocity,frictionConstant); 弹力(目标位置当前位置)弹簧劲度系数 CGPointspringForceCGPointMultiply(CGPointSubtract(self。targetPoint,self。view。center),springConstant); 力弹力摩擦力 CGPointforceCGPointSubtract(springForce,frictionForce); 速度当前速度力时间质量 self。velocityCGPointAdd(self。velocity,CGPointMultiply(force,time)); 位置当前位置速度时间 self。view。centerCGPointAdd(self。view。center,CGPointMultiply(self。velocity,time)); CGFloatspeedCGPointLength(self。velocity); CGFloatdistanceToGoalCGPointLength(CGPointSubtract(self。targetPoint,self。view。center)); if(0。05distanceToG1){ self。view。centerself。targetP finishedYES; } } 这就是这个方法里的全部内容。我们把这个方法封装到一个SpringAnimation对象中。除了这个方法之外,这个对象中还有一个初始化方法,它指定了view中心的目标位置(在我们的例子中,就是打开状态时界面的中心位置,或者关闭状态时界面的中心位置)和初始的速度。 将动画添加到view上 我们的view类刚好和使用UIDynamic的例子一样:它有一个拖动手势,并且根据拖动手势来更新中心位置。它也有两个同样的delegate方法,这两个方法会实现动画的初始化。首先,一旦用户开始拖动控制板时,我们就取消所有动画: (void)draggableViewBeganDragging:(DraggableView)view { 〔selfcancelSpringAnimation〕; } 一旦停止拖动,我们就根据从拖动手势中得到的最后一个速率值来开始我们的动画。我们根据拖动状态paneState计算出动画的结束位置: (void)draggableView:(DraggableView)viewdraggingEndedWithVelocity:(CGPoint)velocity { PaneStatetargetStatevelocity。0?PaneStateClosed:PaneStateO self。paneStatetargetS 〔selfstartAnimatingView:viewinitialVelocity:velocity〕; } (void)startAnimatingView:(DraggableView)viewinitialVelocity:(CGPoint)velocity { 〔selfcancelSpringAnimation〕; self。springAnimation〔UINTSpringAnimationanimationWithView:viewtarget:self。targetPointvelocity:velocity〕; 〔view。animatoraddAnimation:self。springAnimation〕; } 剩下来要做的就是添加点击动画了,这很简单。一旦我们触发这个状态,就开始动画。如果这里正在进行弹簧动画,我们就用当时的速度作为开始。如果这个弹簧动画是nil,那么这个开始速度就是CGPointZero。想要知道为什么依然可以进行动画,可以看看animationTick:finished:里的代码。当这个起始速度为0的时候,弹力就会使速度缓慢地增长,直到面板到达目标位置: (void)didTap:(UITapGestureRecognizer)tapRecognizer { PaneStatetargetStateself。paneStatePaneStateOpen?PaneStateClosed:PaneStateO self。paneStatetargetS 〔selfstartAnimatingView:self。paneinitialVelocity:self。springAnimation。velocity〕; } 动画驱动 最后,我们需要一个Animator,也就是动画的驱动者。Animator封装了displaylink。因为每个displaylink都链接一个指定的UIScreen,所以我们根据这个指定的UIScreen来初始化我们的animator。我们初始化一个displaylink,并且将它加入到runloop中。因为现在还没有动画,所以我们是从暂停状态开始的: (instancetype)initWithScreen:(UIScreen)screen { self〔superinit〕; if(self){ self。displayLink〔screendisplayLinkWithTarget:selfselector:selector(animationTick:)〕; self。displayLink。pausedYES; 〔self。displayLinkaddToRunLoop:〔NSRunLoopmainRunLoop〕forMode:NSRunLoopCommonModes〕; self。animations〔NSMutableSetnew〕; } } 一旦我们添加了这个动画,我们要确保这个displaylink不再是停止状态: (void)addAnimation:() { 〔self。animationsaddObject:animation〕; if(self。animations。count1){ self。displayLink。pausedNO; } } 我们设置这个displaylink来调用animationTick:方法,在每个Tick中,我们都遍历它的动画数组,并且给这些动画数组中的每个动画发送一个消息。如果这个动画数组中已经没有动画了,我们就暂停这个displaylink。 (void)animationTick:(CADisplayLink)displayLink { CFTimeIntervaldtdisplayLink。 for(ain〔self。animationscopy〕){ BOOLfinishedNO; 〔aanimationTick:dtfinished:finished〕; if(finished){ 〔self。animationsremoveObject:a〕; } } if(self。animations。count0){ self。displayLink。pausedYES; } } 完整的项目在GitHub上。 权衡 我们必须记住,通过displaylink来驱动动画(就像我们刚才演示的例子,或者我们使用UIkit力学来做的例子,又或者是使用Facebook的Pop框架)是有代价需要进行权衡的。就像AndyMatuschar指出的那样,UIView和CAAnimation动画比其他任务更少受系统的影响,因为比起你的应用来说,渲染处于更高的优先级。 回到Mac 现在Mac中还没有UIKit力学。如果你想在Mac中创建一个真实的交互式动画,你必须自己去实现这些动画。我们已经向你展示了如何在iOS中实现这些动画,所以在OSX中实现相似的功能也是非常简单的。你可以查看在GitHub中的完整项目,如果你想要应用到OSX中,这里还有一些地方需要修改: 1。第一个要修改的就是Animator。在Mac中没有CADisplayLink,但是取而代之的有CVDisplayLink,它是以C语言为基础的API。创建它需要做更多的工作,但也是很直接。 2。iOS中的弹簧动画是基于调整view的中心位置来实现的。而OSX中的NSView类没有center这个属性,所以我们用为frame中的origin做动画来代替。 3。在Mac中是没有手势识别,所以我要在我们自定义的view子类中实现mouseDown:,mouseUp:和mouseDragged:方法。 上面就是我们需要在Mac中使用我们的动画效果在代码所需要做的修改。对于像这样的简单view,它能很好的胜任。但对于更复杂的动画,你可能就不会想通过为frame做动画来实现了,我们可以用transform来代替,浏览JonathanWilling写的关于OSX动画的博客,你会获益良多。 Facebook的POP框架 上个星期围绕着Facebook的POP框架的讨论络绎不绝。POP框架是Paper应用背后支持的动画引擎。它的操作非常像我们上面讲的驱动动画的例子,但是它以非常灵活的方式巧妙地封装到了一个程序包中。 让我们动手用POP来驱动我们的动画吧。因为我们自己的类中已经封装了弹簧动画,这些改变就非常简单了。我们所要做的就是初始化一个POP动画来代替我们刚才自己做的动画,并将下面这段代码加入到view中: (void)animatePaneWithInitialVelocity:(CGPoint)initialVelocity { 〔self。panepopremoveAllAnimations〕; POPSpringAnimationanimation〔POPSpringAnimationanimationWithPropertyNamed:kPOPViewCenter〕; animation。velocity〔NSValuevalueWithCGPoint:initialVelocity〕; animation。toValue〔NSValuevalueWithCGPoint:self。targetPoint〕; animation。springSpeed15; animation。springBounciness6; 〔self。panepopaddAnimation:animationforKey:animation〕; self。 } 你可以在GitHub中找到使用POP框架的完整例子。 让其工作非常简单,并且通过它我们可以实现很多更复杂的动画。但是它真正强大的地方在于它能够实现真正的可交互和可中断的动画,就像我们上面提到的那样,因为它直接支持以速度作为输入参数。如果你打算从一开始到被中断这过程中的任何时候都能交互,像POP这样的框架就能帮你实现这些动画,并且它能始终保证动画一直很平滑。