自己定义转场动画--Swift3.0版本号

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

下图中全部转场动画都是自己定义的动画。这些效果假设不用自己定义动画则非常难甚至无法实现:


demo演示

因为录屏的原因,有些效果无法全然展现,比方它事实上还支持横屏。

自己定义转场动画的效果实现起来比較复杂。假设不过拷贝一份可以执行的代码却不懂当中原理,就有可能带来各种隐藏的bug。本文由浅入深介绍以下几个知识:

  1. 传统的基于闭包的实现方式及其缺点
  2. 自己定义present转场动画
  3. 交互式(Interactive)转场动画
  4. 转场协调器与UIModalPresentationCustom
  5. UINavigationController转场动画

我为这篇教程制作了一个demo,您能够去在我的github上clone下来:CustomTransition。假设认为有帮助还望给个star以示支持。

本文以Swift+纯代码实现,相应的OC+Storyboard版本号在demo中也能够找到,那是苹果的官方示范代码,正确性更有保证。demo中用到了CocoaPods,您或许须要运行pod install命令并打开.xcworkspace文件。

在開始正式的教程前,您首先须要下载demo,在代码面前文字是苍白的。demo中包括的凝视足以解释本文全部的知识点。

其次,您还得了解这几个背景知识。

From和To

在代码和文字中。常常会出现fromViewtoView

假设错误的理解它们的含义会导致动画逻辑全然错误。

fromView表示当前视图,toView表示要跳转到的视图。假设是从A视图控制器present到B。则A是from,B是to。从B视图控制器dismiss到A时,B变成了from,A是to。用一张图表示:


from和to

Presented和Presenting

这也是一组相对的概念,它easy与fromViewtoView混淆。简单来说,它不受present或dismiss的影响,假设是从A视图控制器present到B,那么A总是B的presentingViewController,B总是A的presentedViewController

modalPresentationStyle

这是一个枚举类型,表示present时动画的类型。当中能够自己定义动画效果的仅仅有两种:FullScreenCustom,两者的差别在于FullScreen会移除fromView,而Custom不会。比方文章开头的gif中。第三个动画效果就是Custom

基于block的动画

最简单的转场动画是使用transitionFromViewController方法:


传统的转场动画实现

这种方法尽管已经过时,可是对它的分析有助于后面知识的理解。

它一共同拥有6个參数。前两个表示从哪个VC開始,跳转到哪个VC,中间两个參数表示动画的时间和选项。最后两个參数表示动画的详细实现细节和回调闭包。

这六个參数事实上就是一次转场动画所必备的六个元素。它们能够分为两组,前两个參数为一组,表示页面的跳转关系,后面四个为一组,表示动画的运行逻辑。

这种方法的缺点之中的一个是可自己定义程度不高(在后面您会发现能自己定义的不不过动画方式),还有一个缺点则是重用性不好,也能够说是耦合度比較大。

在最后两个闭包參数中,能够预见的是fromViewControllertoViewController參数都会被用到,并且他们是动画的关键。

如果视图控制器A能够跳转到B、C、D、E、F,并且跳转动画基本相似。您会发现transitionFromViewController方法要被复制多次,每次仅仅会改动少量内容。

自己定义present转场动画

出于解耦和提高可自己定义程度的考虑,我们来学习转场动画的正确使用姿势。

首先要了解一个关键概念:转场动画代理,它是一个实现了UIViewControllerTransitioningDelegate协议的对象。我们须要自己实现这个对象,它的作用是为UIKit提供下面几个对象中的一个或多个:

  1. Animator:

    它是实现了UIViewControllerAnimatedTransitioning协议的对象,用于控制动画的持续时间和动画展示逻辑,代理能够为present和dismiss过程分别提供Animator,也能够提供同一个Animator。

  2. 交互式Animator:和Animator类似,只是它是交互式的。后面会有具体介绍

  3. Presentation控制器:

    它能够对present过程更加彻底的自己定义,比方改动被展示视图的大小,新增自己定义视图等,后面会有具体介绍。


转场动画代理

在这一小节中。我们首先介绍最简单的Animator。回想一下转场动画必备的6个元素,它们被分为两组,彼此之间没有关联。Animator的作用等同于第二组的四个元素,也就是说对于同一个Animator。能够适用于A跳转B,也能够适用于A跳转C。

它表示一种通用的页面跳转时的动画逻辑。不受限于详细的视图控制器。

假设您读懂了这段话,整个自己定义的转场动画逻辑就非常清楚了。以视图控制器A跳转到B为例:

  1. 创建动画代理,在事情比較简单时。A自己就能够作为代理
  2. 设置B的transitioningDelegate为步骤1中创建的代理对象
  3. 调用presentViewController:animated:completion:并把參数animated设置为true
  4. 系统会找到代理中提供的Animator。由Animator负责动画逻辑

用详细的样例解释就是:

// 这个类相当于A
class CrossDissolveFirstViewController: UIViewController, UIViewControllerTransitioningDelegate {
    // 这个对象相当于B
    crossDissolveSecondViewController.transitioningDelegate = self  

    // 点击button触发的函数
    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)。这样能够保证总是获取到最新的、正确的信息。


转场动画上下文

获取到足够信息后,我们调用UIView.animateWithDuration方法把动画交给Core Animation处理。千万不要忘记在动画调用结束后,运行completeTransition方法。

本节的知识在Demo的Cross Dissolve目录中有具体的代码。

当中有两个animator文件。这说明我们能够为present和dismiss提供同一个animator,或者分别提供各自相应的animator。假设两者动画效果类似,您能够共用同一个animator,惟一的差别在于:

  1. present时。要把toView增加到container的视图层级。
  2. dismiss时,要把fromView从container的视图层级中移除。

假设您被前面这一大段代码和知识弄晕了,或者临时用不到这些详细的知识,您至少须要记住自己定义动画的基本原理和流程:

  1. 设置将要跳转到的视图控制器(presentedViewController)的transitioningDelegate
  2. 充当代理的对象能够是源视图控制器(presentingViewController),也能够是自己创建的对象,它须要为转场动画提供一个animator对象。
  3. animator对象的animateTransition是整个动画的核心逻辑。

交互式(Interactive)转场动画

刚刚我们说到。设置了toViewControllertransitioningDelegate属性而且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类似。由于交互式的动画仅仅是一种锦上添花,它必须支持非交互式的动画,比方这个样例中。点击button依旧出发的是非交互式的动画,仅仅是手势滑动才会触发交互式动画。

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中的第三个样例。

presentedViewpresentingView能够更改自身的视图层级,加入额外的效果(阴影。圆角)。

UIKit使用转成协调器来管理这些额外的动画。您能够通过须要产生动画效果的视图控制器的transitionCoordinator属性来获取转场协调器,转场协调器仅仅在转场动画的运行过程中存在。


转场动画协调器

想要完毕gif中第三个样例的效果,我们还须要使用UIModalPresentationStyle.Custom来取代.FullScreen

由于后者会移除fromViewController,这显然不符合需求。

当present的方式为.Custom时。我们还能够使用UIPresentationController更加彻底的控制转场动画的效果。

一个 presentation controller具备下面几个功能:

  1. 设置presentedViewController的视图大小
  2. 加入自己定义视图来改变presentedView的外观
  3. 为不论什么自己定义的视图提供转场动画效果
  4. 依据size class进行响应式布局

您能够觉得,. FullScreen以及其它present风格都是swift为我们实现提供好的,它们是.Custom的特例。

.Custom同意我们更加自由的定义转场动画效果。

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

  1. presentationTransitionWillBegin: present将要运行时
  2. presentationTransitionDidEnd:present运行结束后
  3. dismissalTransitionWillBegin:dismiss将要运行时
  4. 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也能够加入交互式转场动画,原理也和此前类似。

总结

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

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

对于自己定义动画,能够通过UIPresentationController中的四个函数自己定义动画运行前后的效果。能够改动presentedViewController的大小、外观并同步运行其它的动画。

posted @ 2017-08-15 20:50  yangykaifa  阅读(235)  评论(0编辑  收藏  举报