定制View Controller的切换特效
本文译自objc.io的文章:http://www.objc.io/issue-5/view-controller-transitions.html
定制动画效果
iOS7中最让我激动的特性之一就是提供了新的API来支持做定制ViewContrioller之间的转换特效。iOS7发布之前,我自己写过一些ViewController之间转换动画,这是一个比较头疼的过程,而且这种做法支持的并不完整,尤其是如果你想让这个转换动画有交互式的效果就更难了。
在继续阅读之前,我需要先声明一下:objc.io的文章通常是介绍各种iOS开发的最佳实践,但是,这个API是新近才发布的,目前还没有所谓的最佳实践,通常来说,开发者需要探索几个月才能知道关于新的API的最佳实践。因此请将本文看做对一个新API的探索,而非关于这个新API的最佳实践介绍。如果您有更好的关于这个API的实践,请不吝赐教,我们会把您的实践更新到这篇文章中。
在开始研究新的API之间,我们先来看看在iOS7中导航视图控制器之间的默认的行为发生了那些改变:在导航视图控制器的容器中,ViewController之间的转换动画有些小改动,这些改动让ViewController之间的切换有了交互的效果,比方说你想要弹出一个ViewController,可以把替换ViewController从屏幕左边平移过来,也可以把当前的ViewController拉到屏幕右边。
接下来,我们来看看新的API,有个发现个人觉得很有趣,这部分API使用了大量的protocols而非具体的对象,这初看起来有点诡异。但是细想起来,我个人更喜欢这样的API设计,因为这给了开发者更大的灵活性。下面,让我们来做件简单的事情:为把一个ViewController推入到导航视图控制器的容器中的操作,实现一个动画效果(本实例代码已托管在Github上:https://github.com/objcio/issue5-view-controller-transitions)。为了完成这个任务,需要实现UINavigationControllerDelegate类中的新方法:
- (id<UIViewControllerAnimatedTransitioning>) navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController*)fromVC toViewController:(UIViewController*)toVC { if (operation == UINavigationControllerOperationPush) { return self.animator; } return nil; }
从上面的代码可以看出,我们可以根据不同的操作(压入或弹出)返回不同的动画效果,如果想在多个操作之间共享动画效果,可以把动画效果设置为类的一个属性,然后再多个操作中共享。或者我们可以为每个操作都创建一个专属的动画效果对象,这儿存在很大的灵活性,就看开发者想怎么玩了。
为了执行动画效果,我们需要创建一个实现了UIViewControllerContextTransitioning协议的对象:
@interface Animator : NSObject <UIViewControllerAnimatedTransitioning> @end
这个协议要求我们实现2个方法,一个定义了动画的持续时间:
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext { return 0.25; }
另一个定义了整个动画的执行效果:
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext { UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; [[transitionContext containerView] addSubview:toViewController.view]; toViewController.view.alpha = 0; [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ fromViewController.view.transform = CGAffineTransformMakeScale(0.1, 0.1); toViewController.view.alpha = 1; } completion:^(BOOL finished) { fromViewController.view.transform = CGAffineTransformIdentity; [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; }]; }
从上面的例子中,你可以看到应该怎么使用这个协议:这个方法中通过接受一个类型为id<UIViewControllerContextTransitioning>的参数,来获取切换的上下文信息。值得注意的是,执行完动画效果之后,我们需要调用TransitionContext的completeTransition方法并更新ViewController的状态,剩下的代码和iOS7之前的一样了,我们从上下文信息中得到了需要做切换的2个ViewController,然后使用最简单的UIView动画效果,这就是实现一个切换ViewController的定制动画效果所需的全部代码,现在我们有了一个定制的切换动画效果了。
注意,这儿只是为Push操作实现了定制动画效果,对于Pop操作,还是会使用默认的滑动动画效果,另外,上面我们的实现中并没有交互效果,下面我们就来看看如何定制一个交互式的切换效果
交互式动画效果
制作一个交互式的动画效果非常简单,我们只需要覆盖另一个UINavigationControllerDelegate的方法即可:
- (id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController*)navigationController interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>)animationController { return self.interactionController; }
注意,在非交互式动画效果中,该方法返回nil。
这儿返回的interactionController属性是UIPercentDrivenInteractionTransition类的一个实例,开发者甚至都不需要进一步的配置或设置就可工作,我们创建了一个平移手势识别,下面就是处理该手势的代码:
if (panGestureRecognizer.state == UIGestureRecognizerStateBegan) { if (location.x > CGRectGetMidX(view.bounds)) { navigationControllerDelegate.interactionController = [[UIPercentDrivenInteractiveTransition alloc] init]; [self performSegueWithIdentifier:PushSegueIdentifier sender:self]; } }
只有用户的手势操作发生在屏幕右半部分的时候,我们把下一次动画效果设置为交互式的(通过设置interactionController属性),然后执行方法performSegueWithIdentifier做视图切换(如果你不是使用的storyboards,可以使用导航视图控制的pushViewController方法),为了驱动视图切换的动画效果,我们需要调用interactionController属性的方法:
else if (panGestureRecognizer.state == UIGestureRecognizerStateChanged) { CGFloat d = (translation.x / CGRectGetWidth(view.bounds)) * -1; [interactionController updateInteractiveTransition:d]; }
该方法会根据用户手势的平移距离计算一个百分比,表示动画效果已运行的百分比,最酷的是,interactionController会和动画视图一起协作,我们只使用了简单的UIView的动画效果,但是interactionController却控制了动画的执行进度,开发者都不需要把interactionController和动画Controller关联起来,所有的一切以一种离散的方式配合运行。
最后,我们需要根据用户手势的停止状态来判断该操作是结束了还是取消了?我们需要根据不同的结果去调用interactionController中对应的方法:
else if (panGestureRecognizer.state == UIGestureRecognizerStateEnded) { if ([panGestureRecognizer velocityInView:view].x < 0) { [interactionController finishInteractiveTransition]; } else { [interactionController cancelInteractiveTransition]; } navigationControllerDelegate.interactionController = nil; }
注意,当切换完成或者取消的时候,记得把interactionController设置为nil,这样可以防止在下一次交互式切换时受到上一次的interactionController属性状态的影响。
现在我们已经完成了一个功能完备的交互式ViewControlle切换动画效果,就只使用了简单的手势识别和UIKit提供的一个类,用几行代码就可以达到这样的效果。对于大部分的应用场景,你读到这儿就够用了,剩下的事就是思考如何使用上面提到的方法达到你想要的效果。但是,如果你想更深入的定制切换动画的效果,我可以接着往下看。
###使用GPUImage定制动画
下面我们就来看看如何真正的,彻底的定制动画效果,不使用UIView动画,和CoreAnimation,完全由自己完成所有的动画效果,Letterpress-style(作者的一个项目)中,刚开始,我将尝试使用CoreImage来做动画效果,但是在我的iPhone4上,动画效果的渲染最高只能达到9帧/秒,离我想要的60帧/秒差得很远。
iOS7引入GPUImage之后,开发者可以非常容易的实现一个效果非常好的动画,这儿我想构建一个2个视图控制器相互模糊,消融的动画效果。实现方法就是为2个视图分别照一个快照,然后在快照上使用GPUImage的过率器来达到动画的效果。
首先,我们先写一个类,这个类实现了UIViewControllerAnimatedTransitioning和UIViewControllerInteractiveTransitioning协议。
@interface GPUImageAnimator : NSObject <UIViewControllerAnimatedTransitioning, UIViewControllerInteractiveTransitioning> @property (nonatomic) BOOL interactive; @property (nonatomic) CGFloat progress; - (void)finishInteractiveTransition; - (void)cancelInteractiveTransition; @end
为了加速动画的运行,我们可以把图片一次加载到GPU中,然后所有的处理,画图都直接在GPU上执行,不需要再传送到CPU处理(这减少了数据传送时间),通过使用GPUImageView,我们就可以直接使用OpenGL画图(不需要深入到OpenGL底层去编码,我们只需要在高抽象层级编写代码)。
创建过滤器链也非常的直观,我们可以直接在样例代码的setup方法中看到如何构造它。比较难的是如何动态修改过滤器的配置,使用GPUImage,我们无法自动获得动画效果,因此我们需要每渲染一帧就更新一下过滤器。我们可以使用CADisplayLink类来完成这个工作:
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(frame:)]; [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
在frame方法中,我们可以根据当前的进度更新系统的状态,包括所有的过滤器状态:
- (void)frame:(CADisplayLink*)link { self.progress = MAX(0, MIN((link.timestamp - self.startTime) / duration, 1)); self.blend.mix = self.progress; self.sourcePixellateFilter.fractionalWidthOfAPixel = self.progress *0.1; self.targetPixellateFilter.fractionalWidthOfAPixel = (1- self.progress)*0.1; [self triggerRenderOfNextFrame]; }
好了,这基本上就是我们所需要知道的所有知识了,为了实现交互式的切换效果,我们需要保证根据手势的执行跟新进度,而非根据时间,剩下的处理代码基本上都一样。
这个功能非常强大,你可以在GPUImage中使用所有已有的过滤器,或者写一个自己的OpenGL着色器达到想要的效果。
结论
本文只探讨了在导航视图容器中的2个视图切换时的动画效果设计,但是这些做法在TabBar视图容器,或者你自定义的视图容器中都可以是通用。另外,UICollectionViewController经过扩展甚至支持切换布局时的动画效果了,背后使用的也是同样的机制。这真的太强大了。
在和Orta讨论这个API的时候,他提到他大量的使用了这些机制以创建更轻量的视图,你可以通过创建多个视图控制器,定制视图控制器间的切换效果,在视图控制器之间共享视图的方式来取代在一个视图控制器中维护各种状态。
延伸阅读:
WWDC:CustomTransitionsusingViewControllers(http://asciiwwdc.com/2013/sessions/218)
CustomUIViewControllertransitions(http://www.teehanlax.com/blog/custom-uiviewcontroller-transitions/)
iOS7:CustomTransitions(http://www.doubleencore.com/2013/09/ios-7-custom-transitions/)
CustomViewControllerTransitionswithOrientation(http://whoisryannystrom.com/2013/10/01/View-Controller-Transition-Orientation/)