沃梦达 / IT编程 / 移动开发 / 正文

IOS实战之自定义转场动画详解

这篇文章主要介绍了IOS实战之自定义转场动画,CAAnimation的子类,用于做转场动画,能够为层提供移出屏幕和移入屏幕的动画效果,感兴趣的小伙伴们可以参考一下

转场动画这事,说简单也简单,可以通过presentViewController:animated:completion:和dismissViewControllerAnimated:completion:这一组函数以模态视图的方式展现、隐藏视图。如果用到了navigationController,还可以调用pushViewController:animated:和popViewController这一组函数将新的视图控制器压栈、弹栈。

下图中所有转场动画都是自定义的动画,这些效果如果不用自定义动画则很难甚至无法实现:

创建动画代理,在事情比较简单时,A自己就可以作为代理

  • 设置B的transitioningDelegate为步骤1中创建的代理对象
  • 调用presentViewController:animated:completion:并把参数animated设置为true
  • 系统会找到代理中提供的Animator,由Animator负责动画逻辑
  • 用具体的例子解释就是:

    
    // 这个类相当于A
    class CrossDissolveFirstViewController: UIViewController, UIViewControllerTransitioningDelegate {
      // 这个对象相当于B
      crossDissolveSecondViewController.transitioningDelegate = self 
    
      // 点击按钮触发的函数
      func animationButtonDidClicked() {
        self.presentViewController(crossDissolveSecondViewController, 
                 animated: true, completion: nil)
      }
    
      // 下面这两个函数定义在UIViewControllerTransitioningDelegate协议中
      // 用于为present和dismiss提供animator
      func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    //    也可以使用CrossDissolveAnimator,动画效果各有不同
    //    return CrossDissolveAnimator()
        return HalfWaySpringAnimator()
      }
    
      func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return CrossDissolveAnimator()
      }
    }
    
    

    动画的关键在于animator如何实现,它实现了UIViewControllerAnimatedTransitioning协议,至少需要实现两个方法,我建议您仔细阅读animateTransition方法中的注释,它是整个动画逻辑的核心:

    
    class HalfWaySpringAnimator: NSObject, UIViewControllerAnimatedTransitioning {
      /// 设置动画的持续时间
      func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
        return 2
      }
    
      /// 设置动画的进行方式,附有详细注释,demo中其他地方的这个方法不再解释
      func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
        let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
        let containerView = transitionContext.containerView()
    
        // 需要关注一下from/to和presented/presenting的关系
        // For a Presentation:
        //   fromView = The presenting view.
        //   toView  = The presented view.
        // For a Dismissal:
        //   fromView = The presented view.
        //   toView  = The presenting view.
    
        var fromView = fromViewController?.view
        var toView = toViewController?.view
    
        // iOS8引入了viewForKey方法,尽可能使用这个方法而不是直接访问controller的view属性
        // 比如在form sheet样式中,我们为presentedViewController的view添加阴影或其他decoration,animator会对整个decoration view
        // 添加动画效果,而此时presentedViewController的view只是decoration view的一个子视图
        if transitionContext.respondsToSelector(Selector("viewForKey:")) {
          fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)
          toView = transitionContext.viewForKey(UITransitionContextToViewKey)
        }
    
        // 我们让toview的origin.y在屏幕的一半处,这样它从屏幕的中间位置弹起而不是从屏幕底部弹起,弹起过程中逐渐变为不透明
        toView?.frame = CGRectMake(fromView!.frame.origin.x, fromView!.frame.maxY / 2, fromView!.frame.width, fromView!.frame.height)
        toView?.alpha = 0.0
    
        // 在present和,dismiss时,必须将toview添加到视图层次中
        containerView?.addSubview(toView!)
    
        let transitionDuration = self.transitionDuration(transitionContext)
        // 使用spring动画,有弹簧效果,动画结束后一定要调用completeTransition方法
        UIView.animateWithDuration(transitionDuration, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0, options: .CurveLinear, animations: { () -> Void in
          toView!.alpha = 1.0   // 逐渐变为不透明
          toView?.frame = transitionContext.finalFrameForViewController(toViewController!)  // 移动到指定位置
          }) { (finished: Bool) -> Void in
            let wasCancelled = transitionContext.transitionWasCancelled()
            transitionContext.completeTransition(!wasCancelled)
        }
      }
    }
    
    

    animateTransition方法的核心则是从转场动画上下文获取必要的信息以完成动画。上下文是一个实现了UIViewControllerContextTransitioning的对象,它的作用在于为animateTransition方法提供必备的信息。您不应该缓存任何关于动画的信息,而是应该总是从转场动画上下文中获取(比如fromView和toView),这样可以保证总是获取到最新的、正确的信息。

    present时,要把toView加入到container的视图层级。

  • dismiss时,要把fromView从container的视图层级中移除。
  • 如果您被前面这一大段代码和知识弄晕了,或者暂时用不到这些具体的知识,您至少需要记住自定义动画的基本原理和流程:

    • 设置将要跳转到的视图控制器(presentedViewController)的transitioningDelegate
    • 充当代理的对象可以是源视图控制器(presentingViewController),也可以是自己创建的对象,它需要为转场动画提供一个animator对象。
    • animator对象的animateTransition是整个动画的核心逻辑。
    • 交互式(Interactive)转场动画
      刚刚我们说到,设置了toViewController的transitioningDelegate属性并且present时,UIKit会从代理处获取animator,其实这里还有一个细节:UIKit还会调用代理的interactionControllerForPresentation:方法来获取交互式控制器,如果得到了nil则执行非交互式动画,这就回到了上一节的内容。

      如果获取到了不是nil的对象,那么UIKit不会调用animator的animateTransition方法,而是调用交互式控制器(还记得前面介绍动画代理的示意图么,交互式动画控制器和animator是平级关系)的startInteractiveTransition:方法。

      所谓的交互式动画,通常是基于手势驱动,产生一个动画完成的百分比来控制动画效果(文章开头的gif中第二个动画效果)。整个动画不再是一次性、连贯的完成,而是在任何时候都可以改变百分比甚至取消。这需要一个实现了UIPercentDrivenInteractiveTransition协议的交互式动画控制器和animator协同工作。这看上去是一个非常复杂的任务,但UIKit已经封装了足够多细节,我们只需要在交互式动画控制器和中定义一个时间处理函数(比如处理滑动手势),然后在接收到新的事件时,计算动画完成的百分比并且调用updateInteractiveTransition来更新动画进度即可。

      用下面这段代码简单表示一下整个流程(删除了部分细节和注释,请不要以此为正确参考),完整的代码请参考demo中的Interactivity文件夹:

      
      // 这个相当于fromViewController
      class InteractivityFirstViewController: UIViewController {
         // 这个相当于toViewController
        lazy var interactivitySecondViewController: InteractivitySecondViewController = InteractivitySecondViewController()
        // 定义了一个InteractivityTransitionDelegate类作为代理
        lazy var customTransitionDelegate: InteractivityTransitionDelegate = InteractivityTransitionDelegate()
      
        override func viewDidLoad() {
          super.viewDidLoad()
          setupView() // 主要是一些UI控件的布局,可以无视其实现细节
      
          /// 设置动画代理,这个代理比较复杂,所以我们新建了一个代理对象而不是让self作为代理
          interactivitySecondViewController.transitioningDelegate = customTransitionDelegate
        }
      
        // 触发手势时,也会调用animationButtonDidClicked方法
        func interactiveTransitionRecognizerAction(sender: UIScreenEdgePanGestureRecognizer) {
          if sender.state == .Began {
            self.animationButtonDidClicked(sender)
          }
        }
      
        func animationButtonDidClicked(sender: AnyObject) {
          self.presentViewController(interactivitySecondViewController, animated: true, completion: nil)
        }
      }
      
      

      非交互式的动画代理只需要为present和dismiss提供animator即可,但是在交互式的动画代理中,还需要为present和dismiss提供交互式动画控制器:

      
      class InteractivityTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate {
        func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
          return InteractivityTransitionAnimator(targetEdge: targetEdge)
        }
      
        func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
          return InteractivityTransitionAnimator(targetEdge: targetEdge)
        }
      
        /// 前两个函数和淡入淡出demo中的实现一致
        /// 后两个函数用于实现交互式动画
      
        func interactionControllerForPresentation(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
          return TransitionInteractionController(gestureRecognizer: gestureRecognizer, edgeForDragging: targetEdge)
        }
      
        func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
          return TransitionInteractionController(gestureRecognizer: gestureRecognizer, edgeForDragging: targetEdge)
        }
      }
      
      

      animator中的代码略去,它和非交互式动画中的animator类似。因为交互式的动画只是一种锦上添花,它必须支持非交互式的动画,比如这个例子中,点击按钮依然出发的是非交互式的动画,只是手势滑动才会触发交互式动画。

      
      class TransitionInteractionController: UIPercentDrivenInteractiveTransition {
        /// 当手势有滑动时触发这个函数
        func gestureRecognizeDidUpdate(gestureRecognizer: UIScreenEdgePanGestureRecognizer) {
          switch gestureRecognizer.state {
          case .Began: break
          case .Changed: self.updateInteractiveTransition(self.percentForGesture(gestureRecognizer)) //手势滑动,更新百分比
          case .Ended:  // 滑动结束,判断是否超过一半,如果是则完成剩下的动画,否则取消动画
            if self.percentForGesture(gestureRecognizer) >= 0.5 {
              self.finishInteractiveTransition()
            }
            else {
              self.cancelInteractiveTransition()
            }
          default: self.cancelInteractiveTransition()
          }
        }
        private func percentForGesture(gesture: UIScreenEdgePanGestureRecognizer) -> CGFloat {
          let percent = 根据gesture计算得出
          return percent
        }
      }
      

      交互式动画是在非交互式动画的基础上实现的,我们需要创建一个继承自UIPercentDrivenInteractiveTransition类型的子类,并且在动画代理中返回这个类型的实例对象。

      在这个类型中,监听手势(或者下载进度等等)的时间变化,然后调用percentForGesture方法更新动画进度即可。

      转场协调器与UIModalPresentationCustom
      在进行转场动画的同时,您还可以进行一些同步的,额外的动画,比如文章开头gif中的第三个例子。presentedView和presentingView可以更改自身的视图层级,添加额外的效果(阴影,圆角)。UIKit使用转成协调器来管理这些额外的动画。您可以通过需要产生动画效果的视图控制器的transitionCoordinator属性来获取转场协调器,转场协调器只在转场动画的执行过程中存在。

      设置presentedViewController的视图大小

    • 添加自定义视图来改变presentedView的外观
    • 为任何自定义的视图提供转场动画效果
    • 根据size class进行响应式布局

    您可以认为,. FullScreen以及其他present风格都是swift为我们实现提供好的,它们是.Custom的特例。而.Custom允许我们更加自由的定义转场动画效果。

    UIPresentationController提供了四个函数来定义present和dismiss动画开始前后的操作:

    • presentationTransitionWillBegin: present将要执行时
    • presentationTransitionDidEnd:present执行结束后
    • dismissalTransitionWillBegin:dismiss将要执行时
    • dismissalTransitionDidEnd:dismiss执行结束后

    下面的代码简要描述了gif中第三个动画效果的实现原理,您可以在demo的Custom Presentation文件夹下查看完成代码:

    
    // 这个相当于fromViewController
    class CustomPresentationFirstViewController: UIViewController {
      // 这个相当于toViewController
      lazy var customPresentationSecondViewController: CustomPresentationSecondViewController = CustomPresentationSecondViewController()
      // 创建PresentationController
      lazy var customPresentationController: CustomPresentationController = CustomPresentationController(presentedViewController: self.customPresentationSecondViewController, presentingViewController: self)
    
      override func viewDidLoad() {
        super.viewDidLoad()
        setupView() // 主要是一些UI控件的布局,可以无视其实现细节
    
        // 设置转场动画代理
        customPresentationSecondViewController.transitioningDelegate = customPresentationController
      }
    
      override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
      }
    
      func animationButtonDidClicked() {
        self.presentViewController(customPresentationSecondViewController, animated: true, completion: nil)
      }
    }
    
    

    重点在于如何实现CustomPresentationController这个类:

    
    class CustomPresentationController: UIPresentationController, UIViewControllerTransitioningDelegate {
      var presentationWrappingView: UIView? // 这个视图封装了原视图,添加了阴影和圆角效果
      var dimmingView: UIView? = nil // alpha为0.5的黑色蒙版
    
      // 告诉UIKit为哪个视图添加动画效果
      override func presentedView() -> UIView? {
        return self.presentationWrappingView
      }
    }
    
    // 四个方法自定义转场动画发生前后的操作
    extension CustomPresentationController {
      override func presentationTransitionWillBegin() {
        // 设置presentationWrappingView和dimmingView的UI效果
        let transitionCoordinator = self.presentingViewController.transitionCoordinator()
        self.dimmingView?.alpha = 0
        // 通过转场协调器执行同步的动画效果
        transitionCoordinator?.animateAlongsideTransition({ (context: UIViewControllerTransitionCoordinatorContext) -> Void in
          self.dimmingView?.alpha = 0.5
          }, completion: nil)
      }
    
      /// present结束时,把dimmingView和wrappingView都清空,这些临时视图用不到了
      override func presentationTransitionDidEnd(completed: Bool) {
        if !completed {
          self.presentationWrappingView = nil
          self.dimmingView = nil
        }
      }
    
      /// dismiss开始时,让dimmingView完全透明,这个动画和animator中的动画同时发生
      override func dismissalTransitionWillBegin() {
        let transitionCoordinator = self.presentingViewController.transitionCoordinator()
        transitionCoordinator?.animateAlongsideTransition({ (context: UIViewControllerTransitionCoordinatorContext) -> Void in
          self.dimmingView?.alpha = 0
          }, completion: nil)
      }
    
      /// dismiss结束时,把dimmingView和wrappingView都清空,这些临时视图用不到了
      override func dismissalTransitionDidEnd(completed: Bool) {
        if completed {
          self.presentationWrappingView = nil
          self.dimmingView = nil
        }
      }
    }
    
    extension CustomPresentationController {
    }
    
    

    除此以外,这个类还要处理子视图布局相关的逻辑。它作为动画代理,还需要为动画提供animator对象,详细代码请在demo的Custom Presentation文件夹下阅读。

    UINavigationController转场动画
    到目前为止,所有转场动画都是适用于present和dismiss的,其实UINavigationController也可以自定义转场动画。两者是平行关系,很多都可以类比过来:

    
    class FromViewController: UIViewController, UINavigationControllerDelegate {
      let toViewController: ToViewController = ToViewController()
    
      override func viewDidLoad() {
        super.viewDidLoad()
        setupView() // 主要是一些UI控件的布局,可以无视其实现细节
    
        self.navigationController.delegate = self
      }
    }
    
    

    与present/dismiss不同的时,现在视图控制器实现的是UINavigationControllerDelegate协议,让自己成为navigationController的代理。这个协议类似于此前的UIViewControllerTransitioningDelegate协议。

    FromViewController实现UINavigationControllerDelegate协议的具体操作如下:

    
    func navigationController(navigationController: UINavigationController, 
       animationControllerForOperation operation: UINavigationControllerOperation, 
               fromViewController fromVC: UIViewController, 
                 toViewController toVC: UIViewController) 
                -> UIViewControllerAnimatedTransitioning? {
        if operation == .Push {
          return PushAnimator()
        }
        if operation == .Pop {
          return PopAnimator()
        }
        return nil;
      }
    

    至于animator,就和此前没有任何区别了。可见,一个封装得很好的animator,不仅能在present/dismiss时使用,甚至还可以在push/pop时使用。

    UINavigationController也可以添加交互式转场动画,原理也和此前类似。

    总结
    对于非交互式动画,需要设置presentedViewController的transitioningDelegate属性,这个代理需要为present和dismiss提供animator。在animator中规定了动画的持续时间和表现逻辑。

    对于交互式动画,需要在此前的基础上,由transitioningDelegate属性提供交互式动画控制器。在控制器中进行事件处理,然后更新动画完成进度。

    对于自定义动画,可以通过UIPresentationController中的四个函数自定义动画执行前后的效果,可以修改presentedViewController的大小、外观并同步执行其他的动画。

    自定义动画的水还是比较深,本文仅适合做入门学习用,欢迎互相交流。

    本文标题为:IOS实战之自定义转场动画详解