IOS 转场动画 UIViewControllerAnimatedTransitioning

自定义UIViewController的转场动画

现在让我们来看一个好东西。苹果公司不仅为开发者引入了新的动画API,而且还扩大了其应用范围。在使用UIViewController管理视图的推入推出时,可以很容易地自定义以下转场动画:

• UIViewController

• presentViewController

• UITabBarController

• setSelectedViewController

• setSelectedIndex

• UINavigationController

• pushViewController

• popViewController

• setViewControllers

 

在示例应用程序中,我创建了一系列转场动画,在动画中使用了之前讲解过的新引入的弹簧动画和关键帧block方法,现在让我们来看看如何使用新API来自定义上述的转场动画。

 

核心概念:动画控制器

那么,如何在使用自定义动画的同时不影响视图的其他属性?对此苹果公司提供了一个新的协议:UIViewControllerAnimatedTransitioning,我们可以在协议方法中编写自定义的动画代码。苹果开发者文档中称实现了此协议的对象为动画控制器。

 

由于我们使用了协议这一语法特性,自定义动画的代码可以灵活的放在自己想要的位置。你可以创建一个专门用于管理动画的类, 也可以让UIViewController实现UIViewControllerAnimatedTransitioning接口。由于需要实现一系列不同的动画,因此选择为每个动画创建一个类。接下来创建这些动画类的通用父类——BaseAnimation,它定义了一些通用的属性和助手方法。

 

让我们来看第一个动画,使用UINavigationController推入推出视图时,会有一个简单的缩放效果。

 


  1. -(void)animateTransition: 

  2. (id)transitionContext { 

  3. //获取容器视图引用 

  4. UIView *containerView = [transitionContext 

  5. containerView]; 

  6. UIViewController *fromViewController = [transitionContext 

  7. viewControllerForKey:UITransitionContextFromViewControllerKey 

  8. ]; 

  9. UIViewController *toViewController = [transitionContext 

  10. viewControllerForKey:UITransitionContextToViewControllerKey]; 

  11. if (self.type == AnimationTypePresent) { 

  12. //插入“to”视图,初始缩放值为0.0 

  13. toViewController.view.transform = 

  14. CGAffineTransformMakeScale(0.0, 0.0); 

  15. [containerView insertSubview:toViewController.view 

  16. aboveSubview:fromViewController.view]; 

  17. //缩放“to”视图为想要的效果 

  18. [UIView animateWithDuration:[self 

  19. transitionDuration:transitionContext] animations:^{ 

  20. toViewController.view.transform = 

  21. CGAffineTransformMakeScale(1.0, 1.0); 

  22. } completion:^(BOOL finished) { 

  23. [transitionContext completeTransition:YES]; 

  24. }]; 

  25. else if (self.type == AnimationTypeDismiss) { 

  26. //插入“to”视图 

  27. [containerView insertSubview:toViewController.view 

  28. belowSubview:fromViewController.view]; 

  29. //缩小“from”视图,直到其消失 

  30. [UIView animateWithDuration:[self 

  31. transitionDuration:transitionContext] animations:^{ 

  32. fromViewController.view.transform = 

  33. CGAffineTransformMakeScale(0.0, 0.0); 

  34. } completion:^(BOOL finished) { 

  35. [transitionContext completeTransition:YES]; 

  36. }]; 

  37. -(NSTimeInterval)transitionDuration: 

  38. (id)transitionContext { 

  39. return 0.4; 

符合UIViewControllerAnimatedTransitioning协议的任何对象都需要实现animateTransition:和transitionDuration:两个方法。你也可以选择实现@optional方法animationEnded:,它在动画完成后由系统自动调用,相当于completion block,非常方便。

 

在animateTransition:中你需要处理以下过程:

1. 将“to”视图插入容器视图

2. 将“to”和“from”视图分别移动到自己想要的位置

3. 最后,在动画完成时千万别忘了调用completeTransition: 方法

UIViewControllerAnimatedTransitioning协议中的方法都带有一个参数:transitionContext,这是一个系统级的对象,它符合 UIView-ControllerContextTransitioning协议,我们可以从该对象中获取用于控制转场动画的必要信息,主要包括以下内容:

 

IOS 转场动画 UIViewControllerAnimatedTransitioning_第1张图片

 

显然,苹果公司帮助开发者完成了大部分让人讨厌的细节工作,仅仅需要我们自己完成的工作就是定义动画的初始状态和终止状态,并调整到自己满意的效果。最后我再啰嗦两句有关transitionContext的重要注意事项:

 

1.获取frame的方法可能会返回CGRectZero——如果系统无法确定该frame的值具体是什么。例如,如果你使用自定义的模态视图控制器

推出动画,在结束时系统无法确定其finalFrame。

2.如果视图控制器已经从屏幕上移除了,那么获取frame的方法也会返回CGRectZero。例如在导航控制器的转场动画结束后,试图获取“from”视图的finalFrame。

你不用手动去移除“from”视图,transitionContext将自动帮你完成。

3.如果你在应用的其他地方需要使用transitionContext,你可以放心地使用动画控制器保留一个transitionContext的引用。

将动画控制器应用到转场动画中。

 

现在,我们已经开发好了动画控制器,那么最后需要做的就是,将它们应用到转场动画中:我们需要对管理转场动画的UIViewController做一些操作。

 

一般来说,我们只需要让UIViewController符合UIViewController-TransitioningDelegate 协议, 编写animationController-ForPresentedController和animationControllerForDismissedController方法。在我的示例应用程序中,我设置了一个属性,用来让动画控制器知道目前正在推入还是推出视图:

 


  1. -(id) 

  2. animationControllerForPresentedController:(UIViewController 

  3. *)presented presentingController:(UIViewController 

  4. *)presenting sourceController:(UIViewController *)source { 

  5. modalAnimationController.type = AnimationTypePresent; 

  6. return modalAnimationController; 

  7. -(id) 

  8. animationControllerForDismissedController:(UIViewController 

  9. *)dismissed { 

  10. modalAnimationController.type = AnimationTypeDismiss; 

  11. return modalAnimationController; 

 

然后,在推入模态视图控制器时,我们设置modalPresentationStyle为UIModalPresentationFullScreen或UIModalPresentationCustom。我们还必须将一个符合UIViewControllerTransitioningDelegate协议的对象设置为它的transitioningDelegate,一般来说都是推入该模态视图控制器的UIViewController。

 


  1. OptionsViewController *modal = [[OptionsViewController alloc] 

  2. initWithNibName:@"OptionsViewController" bundle:[NSBundle 

  3. mainBundle]]; 

  4. modal.transitioningDelegate = self; 

  5. modal.modalPresentationStyle = UIModalPresentationCustom; 

  6. [self presentViewController:modal animated:YES 

  7. completion:nil]; 

如果需要将动画控制器应用到UINavigationController的转场动画中,我们需要使用UINavigationControllerDelegate协议中的一个新方法:animationControllerForOperation。对于任何自定义的导航转场动画,导航栏都会有一个淡入淡出的动画过程。同样,对于UITabBarController,使用UITabBarControllerDelegate协议的新方法——animationController-ForTransitionFromViewController。

 

为转场动画定义交互方式

在iOS7中,苹果到处都在使用交互式弹出手势,同时,苹果也给开发者们提供了一系列工具,只需简单几步就能将交互手势应用在视图切换过程中。我们可以通过相应的委托方法返回一个交互控制器:

• UINavigationController

• interactionControllerForAnimationController

• UITabBarController

• interactionControllerForAnimationController

• UIViewController

• interactionControllerForPresentation

• interactionControllerForDismissal

 

这里唯一需要注意的是,如果没有自定义转场动画,这些方法就不会起作用。例如,你必须从animationControllerForOperation得到一个有效的动画控制器,UINavigationController才会调用interactionController-

ForAnimationController——即使你在转场交互中没有使用动画控制器。

 

其次,交互控制器非常灵活,有很强的可扩展性。虽然在示例应用程序中我使用手势检测来控制交互,但是你也可以用手势以外的其他方式来实现。你可以设计任意你想要的效果用以转场交互。

 

交互控制器:最简单的实现方式有两种方式可以创建交互控制器。第一个也是最简单的一个,就是使用UIPercentDrivenInteractiveTransition。

 


  1. @interface UIPercentDrivenInteractiveTransition : NSObject 

  2.  

  3. @property (readonly) CGFloat duration; 

  4. @property (readonly) CGFloat percentComplete; 

  5. @property (nonatomic,assign) CGFloat completionSpeed; 

  6. @property (nonatomic,assign) UIViewAnimationCurve 

  7. completionCurve; 

  8. - (void)updateInteractiveTransition:(CGFloat)percentComplete; 

  9. - (void)cancelInteractiveTransition; 

  10. - (void)finishInteractiveTransition; 

这个类具体实现了UIViewControllerInteractiveTransitioning协议,我们可以使用它轻松为动画控制器添加自定义的交互方式。只要为目标视图加入手势(或者其他交互方式)并调用updateInteractiveTransition:,传入动画时间占整个过程的百分比即可。同时, 记住在交互完成后调用finishInteractiveTransition: , 交互被取消时调用cancel-InteractiveTransition:。下面的例子展示了如何将捏合手势应用到转场动画中:

 


  1. -(void)handlePinch:(UIPinchGestureRecognizer *)pinch { 

  2. CGFloat scale = pinch.scale; 

  3. switch (pinch.state) { 

  4. case UIGestureRecognizerStateBegan: { 

  5. _startScale = scale; 

  6. self.interactive = YES; 

  7. [self.navigationController 

  8. popViewControllerAnimated:YES]; 

  9. break

  10. case UIGestureRecognizerStateChanged: { 

  11. CGFloat percent = (1.0 - scale/_startScale); 

  12. [self updateInteractiveTransition:(percent < 0.0) ? 

  13. 0.0 : percent]; 

  14. break

  15. case UIGestureRecognizerStateEnded: { 

  16. CGFloat percent = (1.0 - scale/_startScale); 

  17. BOOL cancelled = ([pinch velocity] < 5.0 && percent 

  18. <= 0.3); 

  19. if (cancelled) [self cancelInteractiveTransition]; 

  20. else [self finishInteractiveTransition]; 

  21. break

  22. case UIGestureRecognizerStateCancelled: { 

  23. CGFloat percent = (1.0 - scale/_startScale); 

  24. BOOL cancelled = ([pinch velocity] < 5.0 && percent 

  25. <= 0.3); 

  26. if (cancelled) [self cancelInteractiveTransition]; 

  27. else [self finishInteractiveTransition]; 

  28. break

当你继承了UIPercentDrivenInteractiveTransition类,交互过程中系统会自动调用动画控制器的animateTransition:方法,按照你传递的percentComplete参数实时地展现动画效果。在交互完成后,它还自动调用animateTransition:方法恢复到正常状态,一旦交互完成,我们就可以改变completionSpeed和completionCurve属性来修改其他的一些样式。

 

交互控制器:通过自定义的方式

如果你需要深入控制UIPercentDrivenInteractiveTransition处理转场动画的细节,那么就不用去继承该类,而是使用UIViewController-InteractiveTransitioning协议。此协议与UIViewController-AnimatedTransitioning类似,我们可以通过该协议控制所有关于转场动画的细节。在该协议中我们需要完成以下步骤:

 

1. 实现startInteractiveTransition:方法,用于初始化专场动画。

2. 获取transitionContext 对象的引用(如果继承了UIPercentDrivenInteractiveTransition,可以看到它自动帮我们完成了这一步骤,因此这里我们必须手动获取该对象)。

3. 和之前一样,在适当的情况下调用updateInteractiveTransition:,cancelInteractiveTransition和finishInteractiveTransition(对于导航控制器来说,完成方法中还需要显示或隐藏导航栏)。

4. 完成后仍然请记住调用transitionCompleted:。

 

下面是我通过自定义的交互控制器来实现与之前相同的动画,仍然是使用捏合手势控制转场动画。

 


  1. -(void)startInteractiveTransition: 

  2. (id)transitionContext { 

  3. //获取transitionContext对象的引用 

  4. _context = transitionContext; 

  5. //获取容器视图引用 

  6. UIView *containerView = [transitionContext 

  7. containerView]; 

  8. UIViewController *fromViewController = [transitionContext 

  9. viewControllerForKey:UITransitionContextFromViewControllerKey 

  10. ]; 

  11. UIViewController *toViewController = [transitionContext 

  12. viewControllerForKey:UITransitionContextToViewControllerKey]; 

  13. //插入“to”视图 

  14. toViewController.view.frame = [transitionContext 

  15. finalFrameForViewController:toViewController]; 

  16. [containerView insertSubview:toViewController.view 

  17. belowSubview:fromViewController.view]; 

  18. //保留需要缩?小的视图的引用 

  19. _transitioningView = fromViewController.view; 

  20. -(void)updateWithPercent:(CGFloat)percent { 

  21. CGFloat scale = fabsf(percent-1.0); 

  22. _transitioningView.transform = 

  23. CGAffineTransformMakeScale(scale, scale); 

  24. [_context updateInteractiveTransition:percent]; 

  25. -(void)end:(BOOL)cancelled { 

  26. if (cancelled) { 

  27. [UIView animateWithDuration:_completionSpeed 

  28. animations:^{ 

  29. _transitioningView.transform = 

  30. CGAffineTransformMakeScale(1.0, 1.0); 

  31. } completion:^(BOOL finished) { 

  32. [_context cancelInteractiveTransition]; 

  33. [_context completeTransition:NO]; 

  34. }]; 

  35. else { 

  36. [UIView animateWithDuration:_completionSpeed 

  37. animations:^{ 

  38. _transitioningView.transform = 

  39. CGAffineTransformMakeScale(0.0, 0.0); 

  40. } completion:^(BOOL finished) { 

  41. [_context finishInteractiveTransition]; 

  42. [_context completeTransition:YES]; 

  43. }]; 

你可以让动画控制器同时实现UIViewControllerInteractive-Transitioning和 UIViewControllerAnimatedTransitioning(像示例程序中那样),从而把所有代码都放在一个类中。你也可以将交互控制器和动画控制器分成两个类——协议这一语法特性的妙处在于,你可以轻松实现符合需求的最佳解决方案。

 

更多小技巧

在block中选择是否进行动画

开发者或许会遇到这样一种情况:在一串精美的动画效果中,我们需要让某些视图不进行动画,从而营造一种动静相宜的效果。在动画block方法推出之前,我们可以在[UIView beginAnimations]和[UIView commitAnimations]之间使用setAnimationsEnabled方法来设置哪些动画不需要执行。而在iOS7SDK中,苹果公司为开发者提供了新方法,只要把不需要执行的动画写在block中即可:

 


  1. [UIView performWithoutAnimation:^{ 

  2. //确保不执行动画 

  3. }]; 

你可以随时执行这段代码来控制不需要执行的动画。

 

集合视图的导航转场动画

你可能对UICollectionView的setLayout:animated:方法非常熟悉了。在iOS7中,当导航控制器推入推出集合视图控制器时,如果开启了 useLayout-ToLayoutNavigationTransitions属性,系统将自动调用setLayout:animated:方法。因此,在你推入集合视图控制器时,只需要设置该属性,导航控制器就可以自动执行动画,和你手动对集合视图调用setLayout:animated方法的效果一样。

 


  1. CollectionViewController *VC = [[CollectionViewController 

  2. alloc] initWithCollectionViewLayout:flowLayout]; 

  3. VC.title = @"Mini Apples"

  4. VC.useLayoutToLayoutNavigationTransitions = YES; 

  5. [self.navigationController pushViewController:VC 

  6. animated:YES]; 

转场动画调度器

还有一个非常有用的API, 它可以帮助视图控制器管理转场动画:UIViewControllerTransitionCoordinator协议。在iOS7中,每一个视图控制器(当然也包括UINavigationController和UITabBarController)都有一个transitionCoordinator属性,该属性提供了一系列用于转场动画的强大工具,首先我们来看看animateAlongsideTransition:方法。

 


  1. [self.transitionCoordinator 

  2. animateAlongsideTransition:^(id

  3. rdinatorContext> context) { 

  4. //要执行的动画 

  5. completion:^(id 

  6. context) { 

  7. //动画结束后执行的代码块 

  8. }]; 

我们可以通过这个方法在进行转场动画时并行执行一些其他动画,context参数和之前提到的符合UIViewControllerContextTransitioning协议的transitionContext参数相类似,从该参数中我们可以获取有关转场过程的一些重要信息,包括container view和转场效果。苹果公司甚至允许开发者不传入context参数,只传入完成后执行的block。所以请大胆尝试使用它吧。

 

对于交互转场来说, 视图在转场过程中状态可能发生改变, 于是notifyWhenInteractionEndsUsingBlock:方法特别有用——它可以用来管理视图状态。在交互转场中,viewWillAppear:方法或许会在某个视图控制器推入时被调用,但是按照常理随后应该会被调用的viewDidAppear:则不一定,因为用户随时可能取消该交互(例如在之前的例子中,捏到一半又恢复原状)。

 

由此,如果我们不希望在这种情况下修改视图状态,我们可以使用该方法,恢复对视图的更改(使用UIViewControllerTransitionCoordinatorContext的isCancelled属性)。

 


  1. [self.transitionCoordinator 

  2. notifyWhenInteractionEndsUsingBlock:^(id

  3. sitionCoordinatorContext> context) { 

  4. //动画结束后执?行的代码块 

  5. }]; 

 

屏幕快照

在iOS7 以前, 获取一个UIView的快照有以下步骤: 首先创建一个UIGraphics的图像上下文,然后将视图的layer渲染到该上下文中,从而取得一个图像,最后关闭图像上下文,并将图像显示在UIImageView中。现在我们只需要一行代码就可以完成上述步骤了:

 


  1. [view snapshotViewAfterScreenUpdates:NO]; 

这个方法制作了一个UIView的副本,如果我们希望视图在执行动画之前保存现在的外观,以备之后使用(动画中视图可能会被子视图遮盖或者发生其他一些变化),该方法就特别方便。

 

afterUpdates参数表示是否在所有效果应用在视图上了以后再获取快照。例如,如果该参数为NO,则立马获取该视图现在状态的快照,反之,以下代码只能得到一个空白快照:

 


  1. [view snapshotViewAfterScreenUpdates:YES]; 

  2. [view setAlpha:0.0]; 

由于我们设置afterUpdates参数为YES,而视图的透明度值被设置成了0,所以方法将在该设置应用在视图上了之后才进行快照,于是乎屏幕空空如也。另外就是……你可以对快照再进行快照……继续快照……

 

结论

苹果公司在iOS7中为开发者添加了新的用于创建和自定义动画的API。iOS7 SDK不仅引入了强大的动画block和许多易于使用的方法,而且彻底改变了为视图自定义动画的方式。最后,强烈建议你将示例应用程序的代码下载下来,看看我在本文中介绍的API的使用方法!

 


你可能感兴趣的:(动画,转场,Transitioning)