• iOS7.0后苹果提供了自定义转场动画的API,利用这些API我们可以改变 push和pop(navigation非模态),present和dismiss(模态),标签切换(tabbar)的默认转场动画
  • 转场动画这事,说简单也简单,可以通过presentViewController:animated:completion:和dismissViewControllerAnimated:completion:这一组函数以模态视图的方式展现、隐藏视图。如果用到了navigationController,还...

    转场动画这事,说简单也简单,可以通过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

    这也是一组相对的概念,它容易与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  
    
        // 点击按钮触发的函数
        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类似。因为交互式的动画只是一种锦上添花,它必须支持非交互式的动画,比如这个例子中,点击按钮依然出发的是非交互式的动画,只是手势滑动才会触发交互式动画。

    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的大小、外观并同步执行其他的动画。

    展开全文
  • 之前用OC代码写过PUSH和POP转场动画,闲来无事,将其转换成Swift语言,希望对大家有帮助,转载请注明。。。。如何实现PUSH和POP转场动画?首先,创建一个NSObject的类,分别用来实现PUSH和POP的动画效果创建PUSH...

    之前用OC代码写过PUSH和POP的转场动画,闲来无事,将其转换成Swift语言,希望对大家有帮助,转载请注明。。。。

    如何实现PUSH和POP的转场动画?

    首先,创建一个NSObject的类,分别用来实现PUSH和POP的动画效果

    创建PUSH文件,实现扇形效果,代码如下:

    需要注意的是,代理的实现方法要完整

    var transitionContextT:UIViewControllerContextTransitioning?
        
        func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
            return 0.8
        }
        func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
            
            self.transitionContextT = transitionContext
            
            let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
            let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
            //不添加的话,屏幕什么都没有
            let containerView = transitionContext.containerView
            containerView.addSubview((fromVC?.view)!)
            containerView.addSubview((toVC?.view)!)
            
            let originRect:CGRect = CGRect.init(x: 0, y: 0, width: 50, height: 50)
            let maskStartPath = UIBezierPath.init(ovalIn: originRect)
            //OC中CGRectInset(originRect, -2000, -2000)的Swift用法:originRect.insetBy(dx: -2000, dy: -2000)
            let maskEndPath = UIBezierPath.init(ovalIn: originRect.insetBy(dx: -2000, dy: -2000))
            
            //创建一个CAShapeLayer来负责展示圆形遮盖
            let maskLayer = CAShapeLayer.init()
            //将他的path指定为最终的path,来避免在动画完成后回弹
            maskLayer.path = maskEndPath.cgPath
            
            toVC?.view.layer.mask = maskLayer
            
            let maskAnimation = CABasicAnimation.init(keyPath: "path")
            maskAnimation.fromValue = maskStartPath.cgPath
            maskAnimation.toValue = maskEndPath.cgPath
            maskAnimation.duration = self.transitionDuration(using: transitionContext)
            maskAnimation.timingFunction = CAMediaTimingFunction.init(name: kCAMediaTimingFunctionEaseInEaseOut)
            maskAnimation.fillMode = kCAFillModeForwards
            maskAnimation.isRemovedOnCompletion = false
            maskAnimation.delegate = self
            maskLayer.add(maskAnimation, forKey: "path")
            
        }
        //MARK:----- CAAnimationDelegate
        func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
            self.transitionContextT?.completeTransition(!(self.transitionContextT?.transitionWasCancelled)!)
            //去除mask
            self.transitionContextT?.viewController(forKey: UITransitionContextViewControllerKey.from)?.view.layer.mask = nil;
            self.transitionContextT?.viewController(forKey: UITransitionContextViewControllerKey.to)?.view.layer.mask = nil;
        }

    然后,同理创建POP文件,实现弹跳的效果,代码如下:

    var transitionContext:UIViewControllerContextTransitioning?
        
        func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
            return 0.8
        }
        func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
            
            self.transitionContext = transitionContext
            
            let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
            let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
            //不添加的话,屏幕什么都没有
            let containerView = transitionContext.containerView
            containerView.addSubview((fromVC?.view)!)
            containerView.addSubview((toVC?.view)!)
            
            let durationN = self.transitionDuration(using: transitionContext)
            let screenBounds:CGRect = UIScreen.main.bounds
            let finalFrame:CGRect = transitionContext.finalFrame(for: toVC!)
            //OC中CGRectOffset(finalFrame, 0, -screenBounds.size.height)的Swift的用法:finalFrame.offsetBy(dx: 0, dy: -screenBounds.size.height)
            toVC?.view.frame = finalFrame.offsetBy(dx: 0, dy: -screenBounds.size.height)

            //添加动画,有弹跳的效果,参数:usingSpringWithDamping的范围为0.0f到1.0f,数值越小「弹簧」的振动效果越明显,当设置为1.0时,就不弹跳
            //toVC?.view.frame = finalFrame
            //transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            UIView.animate(withDuration: durationN, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.0, options: UIViewAnimationOptions.curveLinear, animations: {() -> Void in
                //print("11111111")
                toVC?.view.frame = finalFrame
            }, completion: ({(Bool) -> Void in
                //print("22222222")
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            }))
        }

    最后在需要使用跳转动画的地方添加self.navigationController?.delegate = self代理方法,并实现,代码如下:

    //MARK:-----UINavigationControllerDelegate
        func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            if operation==UINavigationControllerOperation.push {
                return BHPopAnimation()
            }
            else if operation==UINavigationControllerOperation.pop{
                return BHPushAnimation()
            }
            return nil
        }

    最后附上源代码,如果有问题请留言:https://github.com/hbblzjy/SwiftPushAndPopDemo


    展开全文
  • Swift自定义转场动画

    2016-03-19 17:52:16
    1.通过storyBoard加载控制器 let sb = UIStoryboard(name: "WYPopViewController", bundle: nil) // 1.需要勾选箭头 let vc = sb.instantiateInitialViewController()! // 2.... let vc = sb.instantiate

    1.通过storyBoard加载控制器

        let sb = UIStoryboard(name: "WYPopViewController", bundle: nil)
        // 1.需要勾选箭头
        let vc = sb.instantiateInitialViewController()!
        // 2.通过id加载
        let vc = sb.instantiateViewControllerWithIdentifier("123")
    

    2.设置代理

         // 设置代理
        vc.transitioningDelegate = popAnimation
        //
        vc.modalPresentationStyle = .Custom
        // 3.弹出控制器
        presentViewController(vc, animated: true, completion: nil)
    

    3.准守协议,实现方法(UIViewControllerTransitioningDelegate,UIViewControllerAnimatedTransitioning)

    • 负责转场动画的控制器

        // iOS8之后推出的 告诉系统谁来负责转场动画
        func presentationControllerForPresentedViewController(presented: UIViewController, presentingViewController presenting: UIViewController, sourceViewController source: UIViewController) -> UIPresentationController? {
      
            let pop = WYPopPresentationController(presentedViewController: presented, presentingViewController: presenting)
            pop.presentFrame = presentFrame
            return pop
        }
      
    • 告诉系统谁来负责Modal的展现动画

        // MARK: - 只要实现了以下方法, 那么系统自带的默认动画就没有了, "所有"东西都需要程序员自己来实现
        /**
        :param: presented  被展现视图
        :param: presenting 发起的视图
        :returns: 谁来负责
        */
        func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            isPresent = true
      
            // 发出通知,箭头朝上
            NSNotificationCenter.defaultCenter().postNotificationName(PopViewWillShow, object: nil)
            return self
        }
      
    • 告诉系统谁来负责动画的消失
        /**
         - parameter dismissed: 被关闭的视图
         - returns:
         */
        func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            isPresent = false
            // 发出通知,箭头朝下
              NSNotificationCenter.defaultCenter().postNotificationName(PopViewWillDismiss, object: nil)
            return self
        }
      
    • UIViewControllerAnimatedTransitioning的代理方法

        /**
        返回动画时长
      
        :param: transitionContext 上下文, 里面保存了动画需要的所有参数
        :returns: 动画时长
        */
        func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
            return 0.5
        }
      
    • 告诉系统如何动画, 无论是展现还是消失都会调用这个方法
        /**
         :param: transitionContext 上下文, 里面保存了动画需要的所有参数
         */
        func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    
            if isPresent{
                //print("展开")
                let toView = transitionContext.viewForKey(UITransitionContextToViewKey)
    
                toView?.transform = CGAffineTransformMakeScale(1.0, 0.0)
    
                // 将试图添加到容器中
                transitionContext.containerView()?.addSubview(toView!)
    
                // 修改锚点
                toView?.layer.anchorPoint = CGPointMake(0.5, 0)
                // 执行动画
                UIView.animateWithDuration(0.5, animations: { () -> Void in
    
                    toView?.transform = CGAffineTransformIdentity
                    }) { (_) -> Void in
    
                        transitionContext.completeTransition(true)
                }
    
            }else{
                //print("关闭")
                let formView = transitionContext.viewForKey(UITransitionContextFromViewKey)
    
                UIView.animateWithDuration(0.5, animations: { () -> Void in
    
                    formView?.transform = CGAffineTransformMakeScale(1.0, 0.001)
                    }, completion: { (_) -> Void in
    
                        transitionContext.completeTransition(true)
                })
            }
        }
    

    4.自定义转场控制器

     // 展示视图的尺寸
        var presentFrame = CGRectZero
        /**
         - parameter presentedViewController:  被展示的控制器
         - parameter presentingViewController: 执行转场的控制器
    
         */
        override init(presentedViewController: UIViewController, presentingViewController: UIViewController) {
            super.init(presentedViewController: presentedViewController, presentingViewController: presentingViewController)
        }
    
        /**
         即将布局转场子视图时调用
         */
        override func containerViewWillLayoutSubviews() {
    
            // 设置展现视图的尺寸
            if presentFrame == CGRectZero{
            presentedView()?.frame = CGRectMake(100, 60, 200, 300)
            }else{
    
                presentedView()?.frame = presentFrame
            }
            // 在容器上添加一个蒙版,用来移除modal出来的控制器
            containerView?.insertSubview(cover, atIndex: 0)
        }
    
        func close () {
                presentedViewController.dismissViewControllerAnimated(true, completion: nil)
        }
    
        private lazy var cover : UIView = {
    
            let cover = UIView()
            // 设置尺寸
            cover.frame = UIScreen.mainScreen().bounds
            // 颜色
            cover.backgroundColor = UIColor.clearColor()
    
            // 添加手势
            let tap = UITapGestureRecognizer(target: self, action: "close")
            cover.addGestureRecognizer(tap)
            return cover
        }()



    效果图:


    展开全文
  • iOS 卡片转场动画 Swift

    2016-08-17 12:00:05
    Design ,所以 就研究实现相关的 动画逻辑,具体 什么设计咱不谈了。 说说需求吧:两个控制器之间需要有一些连接,比如,卡片 飞入。卡片是A,B的共同元素。  然后 就用Swift写了一个,OC版的 已经有大神写了,我...

            最近 产品要iOS 使用 Google那套 Material Design   ,所以 就研究实现相关的 动画逻辑,具体 什么设计咱不谈了。

    说说需求吧:两个控制器之间需要有一些连接,比如,卡片 飞入。卡片是A,B的共同元素。

           然后 就用Swift写了一个,OC版的 已经有大神写了,我在下面的连接 有跳转。

           先看效果吧:


           

         

          主要实现就是 实现了 分别为push 和 pop 实现了 UIViewControllerAnimatedTransitioning 协议,并在相关方法中完成动画设计。

         1、push动画

    <span style="font-size:18px;">//
    //  JYMagicMoveTransion.swift
    //  JYMagicMove
    //
    //  Created by 杨勇 on 16/8/16.
    //  Copyright © 2016年 JackYang. All rights reserved.
    //
    
    import UIKit
    
    class JYMagicMoveTransion: NSObject ,UIViewControllerAnimatedTransitioning{
        
        func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
            return 0.5;
        }
        
        func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
            
            //拿到 fromVC 和 toVC 以及 容器
            let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as! ViewController
            let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! JYDetailController
            let container = transitionContext.containerView();
            
            //拿到 cell上的 imageView的快照 隐藏 cell上的imageView
            let snapshotView = fromVC.selectCell.imageView.snapshotViewAfterScreenUpdates(false)
            //设置快照的frame
            snapshotView.frame = container!.convertRect(fromVC.selectCell.imageView.frame, fromView: fromVC.selectCell.imageView.superview)
            //隐藏
            fromVC.selectCell.imageView.hidden = true
            
            //设置toVC 的位置 并设置为透明
            toVC.view.frame = transitionContext.finalFrameForViewController(toVC)
            toVC.view.alpha = 0
            toVC.avaterImageView.hidden = true
            
            //把 toVC的 view 和 快照 加到 容器 上,顺序!
            container?.addSubview(toVC.view)
            container?.addSubview(snapshotView)
            
            //做动画前先把avaterImageView 的frame 更新一下 不然 storyboard 尺寸没有更新
            toVC.avaterImageView.layoutIfNeeded()
            UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0, options: UIViewAnimationOptions.CurveEaseInOut, animations: { 
                () -> Void in
                //动画
                snapshotView.frame = toVC.avaterImageView.frame
                toVC.view.alpha = 1
            }) { (finish:Bool) -> Void in
                fromVC.selectCell.imageView.hidden = false // 把之前隐藏的 显示出来
                toVC.avaterImageView.hidden = false
                snapshotView.removeFromSuperview()
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
            }
        }
        
    }</span>
    

         代码注释的比较详细,我不在冗余的介绍。


        2、pop动画(注释较少,主要是和push基本逻辑相等。)

       

    <span style="font-size:18px;">//
    //  JYMagicPopTransion.swift
    //  JYMagicMove
    //
    //  Created by 杨勇 on 16/8/16.
    //  Copyright © 2016年 JackYang. All rights reserved.
    //
    
    import UIKit
    
    class JYMagicPopTransion: NSObject ,UIViewControllerAnimatedTransitioning{
        
        func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
            return 0.5
        }
        
        func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
            
            //拿到 fromVC 和 toVC 以及 容器
            let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as! JYDetailController
            let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! ViewController
            let container = transitionContext.containerView()
            
            //拿到快照
            let snapshotView = fromVC.avaterImageView.snapshotViewAfterScreenUpdates(false)
            snapshotView.frame = container!.convertRect(fromVC.avaterImageView.frame, fromView: fromVC.avaterImageView.superview)
            fromVC.avaterImageView.hidden = true
            
            toVC.view.frame = transitionContext.finalFrameForViewController(toVC)
            toVC.selectCell.imageView.hidden = true
            
            container?.insertSubview(toVC.view, belowSubview: fromVC.view)
            container?.addSubview(snapshotView)
            
            UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0, options: UIViewAnimationOptions.CurveEaseInOut, animations: { 
                () -> Void in
                snapshotView.frame = container!.convertRect(toVC.selectCell.imageView.frame, fromView: toVC.selectCell.imageView.superview)
                fromVC.view.alpha = 0
            }) { (finish: Bool) -> Void in
                toVC.selectCell.imageView.hidden = false
                snapshotView.removeFromSuperview()
                fromVC.avaterImageView.hidden = false
                
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
            }
            
        }
    }</span>
    

        3、第三点就是滑动 的进度 我通过UIPercentDrivenInteractiveTransition实现。并且 给 第二个控制器加一个 手势(UIScreenEdgePanGestureRecognizer)

    <span style="font-size:18px;">//
    //  JYDetailController.swift
    //  JYMagicMove
    //
    //  Created by 杨勇 on 16/8/16.
    //  Copyright © 2016年 JackYang. All rights reserved.
    //
    
    import UIKit
    
    class JYDetailController: UIViewController ,UINavigationControllerDelegate{
    
        @IBOutlet weak var avaterImageView: UIImageView!
        
        private var percentDrivenTransition :UIPercentDrivenInteractiveTransition?
        
        override func viewDidLoad() {
            super.viewDidLoad()
            self.navigationController?.delegate = self
            let edgePan = UIScreenEdgePanGestureRecognizer.init(target: self, action:Selector("edgePanGesture:"))
            edgePan.edges = UIRectEdge.Left
            self.view.addGestureRecognizer(edgePan)
        }
        
        func edgePanGesture(edgePan:UIScreenEdgePanGestureRecognizer){
            let progress = edgePan.translationInView(self.view).x / self.view.bounds.width
            if edgePan.state == UIGestureRecognizerState.Began {
                self.percentDrivenTransition = UIPercentDrivenInteractiveTransition()
                self.navigationController?.popViewControllerAnimated(true)
            } else if edgePan.state == UIGestureRecognizerState.Changed {
                self.percentDrivenTransition?.updateInteractiveTransition(progress)
            } else if edgePan.state == UIGestureRecognizerState.Cancelled || edgePan.state == UIGestureRecognizerState.Ended {
                if progress > 0.5 {
                    self.percentDrivenTransition?.finishInteractiveTransition()
                } else {
                    self.percentDrivenTransition?.cancelInteractiveTransition()
                }
                self.percentDrivenTransition = nil
            }
        }
    
        func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            if operation == UINavigationControllerOperation.Pop {
                return JYMagicPopTransion()
            } else {
                return nil
            }
        }
        
        func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
            if animationController is JYMagicPopTransion {
                return self.percentDrivenTransition
            } else {
                return nil
            }
        }
        
    }</span>
        可以看到,不断更新 progress,然后把percentDrivenTransition 通过 navigation的代理告诉它,完成动画更新。

        OC版 是 KittenYang写的,我参考他的。原文有下载地址


    展开全文
  • 效果图项目地址:ImageMaskTransition转场原理对于模态展示(Modal)iOS 8之后,可以通过设置ViewController的转场代理transitioningDelegate这个转场代理是一个协议类型UIViewControllerTransitioningDelegate....

    效果图

    项目地址:ImageMaskTransition


    转场原理

    对于模态展示(Modal)

    iOS 8之后,可以通过设置ViewController的转场代理

    transitioningDelegate

    这个转场代理是一个协议类型UIViewControllerTransitioningDelegate.由于我们是非交互式转场,所以只需要实现协议的两个方法即可

    // MARK: - UIViewControllerTransitioningDelegate -
    func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
       //这里返回present的动画
    }
    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        //这里返回dismiss的动
     }

    对于NavigationController来说,则可以设置NavigationController的delegate来返回自定义的动画。

    navigationController.delegate

    这里的delegateUINavigationControllerDelegate类型,在不考虑交互式转场的情况下,我们只需要实现以下方法即可

    //返回动画
    func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        switch operation {
        case .Pop:
           //返回Pop时候
        case .Push:
            //返回Push时候的动画
        default:
             return nil
        }
    }

    细心的朋友应该发现了,不管是模态还是Push,都是返回一个UIViewControllerAnimatedTransitioning协议类型的对象


    通用的Animator

    定义一个类,让其遵循UIViewControllerAnimatedTransitioning协议,来实现实际的动画,

    class ImageMaskAnimator: NSObject,UIViewControllerAnimatedTransitioning {}

    由于实现了UIViewControllerAnimatedTransitioning协议,所以要提供两个方法

    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
      //这里返回动画的时间
    }
    
    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
      //这里进行实际的动画
    }

    转场的原理

    在上文提到的transitionDuration方法中,可以看到有一个参数是transitionContext,这个是转场上下文。通过转场上下文,可以获得fromView,toView以及containView,

    let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
    let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
    let containView = transitionContext.containerView()!

    其中,关系如图

    转场原理(之前做的老图)

    • 转场开始的时候,上下文自动把FromView添加到转场ContainView
    • 转场结束的时候,上下文自动把FromView移除ContainView

    所以,开发者要做的就是

    • 把toView添加到转场ContainView中,并且定义好toView的初始位和状态
    • 定义好FromView和ToView的转场结束时候的状态
    • 创建fromView到toView动画

    用CIFilter截图并添加高斯模糊

    细心的朋友能看到,在最上面转场的时候,不管是present还是dismiss,第一个Controller的看起来都是”模糊”的。其实是采用截图,然后添加一个ImageView覆盖到转场的ContainView上实现的效果。其中,截图,并添加模糊的代码如下

    extension UIView{
        func blurScreenShot(blurRadius:CGFloat)->UIImage?{
            guard self.superview != nil else{
                return nil
            }
            UIGraphicsBeginImageContextWithOptions(CGSize(width: frame.width, height: frame.height), false, 1)
            layer.renderInContext(UIGraphicsGetCurrentContext()!)
            let image = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext();
            guard let blur = CIFilter(name: "CIGaussianBlur") else{
                return nil
            }
            blur.setValue(CIImage(image: image), forKey: kCIInputImageKey)
            blur.setValue(blurRadius, forKey: kCIInputRadiusKey)
            let ciContext  = CIContext(options: nil)
            let result = blur.valueForKey(kCIOutputImageKey) as! CIImage!
            let boundingRect = CGRect(x:0,
                                      y: 0,
                                      width: frame.width,
                                      height: frame.height)
    
            let cgImage = ciContext.createCGImage(result, fromRect: boundingRect)
            let filteredImage = UIImage(CGImage: cgImage)
            return filteredImage
        }
    }

    Tips:在模拟器上,CIFilter的效率很差,但是真机上很快


    Present/Push动画过程

    首先,我们要做一些准备工作,其中,核心是坐标系转换

    Tips:
    Swift通过以下代码块来实现条件编译,可以通过条件编译来适配多版本Swift

    #if 
    //...
    #else
    .. 
    #endif

    整个动画的准备过程

          //获取必要参数
           let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
            let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
            let containView = transitionContext.containerView()!
            let frame = UIScreen.mainScreen().bounds
            maskContentView = UIImageView(frame: frame)
            maskContentView.backgroundColor = UIColor.lightGrayColor()
            //如果是Present或者push
            if self.transitionType == .Present {
                //模拟器上禁用Blur
                #if (arch(i386) || arch(x86_64)) && os(iOS)
                    print("Wow,CIFilter is too slow on simulator,So I disable blur on Simulator")
                #else
                   //截图,生成blur
                    self.blurImage = fromView.blurScreenShot(3.0)
                    maskContentView.image = fromView.blurScreenShot(3.0)
                #endif
                //Blur作为背景,添加到ContainView
                maskContentView.frame = containView.bounds
                containView.addSubview(self.maskContentView)
    
                let fromImageView = self.config.fromImageView
                //Frame的坐标系适配
                let adjustFromRect = fromImageView.convertRect(fromImageView.bounds, toView: containView)
    
                let toImageView = self.config.toImageView!
                let adjustToRect = toImageView.convertRect(toImageView.bounds, toView: containView)
                //添加一个ImageView,来显示动画
                imageView = UIImageView(frame: adjustFromRect)
                imageView.image = fromImageView.image
                containView.addSubview(imageView)
    
                //设置阴影
                imageView.layer.shadowColor = UIColor.blackColor().CGColor
                imageView.layer.shadowOffset = CGSizeMake(2.0, 2.0)
                imageView.layer.shadowRadius = 10.0
                imageView.layer.shadowOpacity = 0.8
               //开始动画
               //动画代码....

    到这里,视图的层次结构如下

    • ContainView
      • fromView
      • maskContentView
      • imageView

    整个Present的动画分为三个部分

      //第一步,移动ImageView到toView的对应Frame,同时变化Scale
      UIView.animateWithDuration(0.5 / 1.6 * self.config.presentDuration, animations: {
                    self.imageView.frame = adjustToRect
                    self.imageView.transform = CGAffineTransformMakeScale(1.2, 1.2)
                }) { (finished) in
                      //第二步,恢复ImageView的Transfrom
                    UIView.animateWithDuration(0.3 / 1.6 * self.config.presentDuration, animations: {
                        self.imageView.transform = CGAffineTransformIdentity
                        self.imageView.layer.shadowOpacity = 0.0
                    }) { (finished) in
                        //第三步,添加toView,然后开始mask动画
                        containView.addSubview(toView)
                        containView.bringSubviewToFront(self.imageView)
                        let adjustFrame = self.imageView.convertRect(self.imageView.bounds, toView: self.maskContentView)
                        toView.maskFrom(adjustFrame, duration: 0.8 / 1.6 * self.config.presentDuration,complete: {
                            //所有动画完成,进行清理
                            self.maskContentView.removeFromSuperview()
                            self.imageView.removeFromSuperview()
                            self.maskContentView = nil
                            self.imageView = nil
                            //通知上下文,转场完成
                            transitionContext.completeTransition(true)
                        })
                    }
                }

    在最后一步进行Mask的时候,视图的层次如下

    • ContainView
      • fromView
      • maskContentView
      • toView
      • imageView

    Mask动画

    Mask动画其实比较简单,我们都知道CALayer有一个mask属性,它也是一个CALayer,整个Mask动画的过程如下

    • 用CAShapeLayer作为对应View的mask
    • 通过动画CAShapeLayer的path属性-实现一个逐渐扩大的圆来实现对应的mask动画

    代码如下

    maskLayer.path = toPath.CGPath
    let basicAnimation = CABasicAnimation(keyPath: "path")
    basicAnimation.duration = duration
    basicAnimation.fromValue = fromPath.CGPath
    basicAnimation.toValue = toPath.CGPath
    basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
    maskLayer.addAnimation(basicAnimation, forKey: "pathMask")
    self.layer.mask = maskLayer

    其中,fromPath是开始的圆环,toPath是结束的圆环。

    圆环的绘制采用贝赛尔曲线

    let fromPath = UIBezierPath(arcCenter: fromCenter, radius: fromRadius, startAngle: 0, endAngle: CGFloat(M_PI) * 2, clockwise: true)
    

    事务

    这里用事务进行包裹,进而在动画完成的时候进行反馈

    CATransaction.begin()
    CATransaction.setCompletionBlock {
        //这里动画完成了
     }
    //上文的Mask代码
    CATransaction.commit()

    Dismiss/Pop的原理类似,不再赘述


    最后

    完整代码地址:ImageMaskTransition
    欢迎Follow我的Github,LeoMobileDeveloper

    展开全文
  • XLBubbleTransition 是一个适用于UINavigation的转场动画,在Push和Pop这两个动作时显示
  • 项目中有要求要用一个切换特效,研究了下发现是拿转场动画做的,所以把之前的知识重新复习下,这里提供一个简单的思路实现转场动画,具体的效果大概是跳转的新的控制器页面从左上角开始向右边拉伸到充满屏幕,同时底部有...
  • 转场动画涉及到的包括导航控制器的Push动画和Pop动画,以及普通控制器的Present和Dismiss动画,主要就是通过控制器遵守UIViewControllerTransitioningDelegate,并实现对应的方法,返回一个遵守...
  • IOS常用的转场方式: 1:UINavigationController的pushViewController打开,popViewController关闭; 2:UIViewController的presentViewController打开,dismissViewController关闭; 当然你也可以直接使用...
  • 转场动画在iOS开发中非常常见, 其原理大概如下图: 一切都是从图中的 * Transition Animation * 开始.本文主要基于以上这张图, 讲解了transitionFromViewController, CATransition, TransitionAnimation三种转场实现...
  • 首先自定义转场得遵循UINavigationControllerDelegate(push、pop转场)、UIViewControllerAnimatedTransitioning(present、dismiss转场).对应到属性就是UIViewController的transitioningDelegate与...
  • 一、前言 用过格瓦拉电影,或者其他app可能都知道,一种点击按钮用放大效果实现转场的动画现在很流行,效果大致如下 ...在iOS中,在同一个导航控制器你可以自定义转场动画实现两个viewController之间的过
  • 看效果先: 实现上面的效果需要用到一个类(UIPercentDrivenInteractiveTransition)和两个协议(UINavigationControllerDelegate,UIViewControllerAnimatedTransitioning) 首先 ...
  • 关于自定义转场动画,我都告诉你(上)
  • 关于自定义转场动画,我都告诉你 作者:@翁呀伟呀  概述 这篇文章,我将讲述几种转场动画的自定义方式,并且每种方式附上一个示例,毕竟代码才是我们的语言,这样比较容易上手。其中主要有以下三种...
  • CATransition是CAAnimation的子类,用于过渡动画或转场动画。为视图层移入移除屏幕提供转场动画。首先来看一下简单的Demo: CATransition*animation=[CATransitionanimation];animation.type=kCATransitionFade;...
  • 前言在平时的开发中,我们进行界面跳转时一般都是采用系统默认的转场动画(Push 和 Present),iOS7之后,苹果开放了相关API,让我们可以自定义转场动画,让APP更有活力。本文将介绍我的一个开源项目一个自定义转场...
  • 在需要使用的控制器中添加#import "BSYAnimator.h"和#import "BSYTransitionAnimator.h"两个头文件: @property(nonatomic,strong)BSYAnimator Animator;@property(nonatomic,strong)BSYTransitionAnimator *...
1 2 3 4 5 ... 20
收藏数 853
精华内容 341