iOS动画和特效(四)controller间的自定义过渡效果

来源:互联网 发布:dsp算法工程师 编辑:程序博客网 时间:2024/06/06 15:46

iOS动画和特效(四)controller间的自定义过渡效果

前面动画和特效系类文章中有一篇写了UIView的过渡效果,而这一篇主要说的是UIViewController的自定义过渡效果和过渡交互

先看看完成后的效果图

  • 点击模态controller,会弹出一个新的绿色UIViewController,手指下滑可以dismiss这个controller
  • 四个角的按钮可以自定义圆形切换的过渡效果切换的一个红色的UIViewController,点击返回用同样的方式切换回来

出场人物介绍

介绍下Controller过渡和交互用到的类

presention and presented

A中模态显示B,那么A就是presention,b就是presented,后续内容会使用这种叫法称呼Modal下的2个controller

UIViewControllerTransitioningDelegate

controller modal过渡的presented和dismiss的动画交互协议,你需要实现协议,它会询问你:

  • 当PresentedController时,你要使用怎样的动画类(UIViewControllerAnimatedTransitioning)展示过渡效果?
  • 当DismissedController时,你要使用怎样的动画类(UIViewControllerAnimatedTransitioning)展示过渡效果?
  • 当PresentedController时,你要使用怎样的过渡交互类(UIViewControllerInteractiveTransitioning)处理过渡交互?
  • 当DismissedController时,你要使用怎样的过渡交互类(UIViewControllerInteractiveTransitioning)处理过渡交互?
  • presentationControllerForPresentedViewController:这篇文章没有用到,应该是自定义modal状态呈现和被呈现的controller,类似于controller的PresentationStyle,想了解的可以参考下面两篇文章 Present ViewController Modally 和 iOS 8的PresentationController 

UIViewControllerAnimatedTransitioning

过渡动画效果的具体实现的接口,需要实现它的3个方法,即可完成一个controller过渡动画效果

  • func animationEnded :过渡动画完成后要执行的代码可以写到这个方法中
  • func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval :给定一个过渡动画的执行时间
  • func animateTransition(transitionContext: UIViewControllerContextTransitioning) :具体过渡动画都在这个方法里面实现,在这个方法中可以通过transitionContext拿到一切你需要的对象,后面会有讲到

UINavigationControllerDelegate

controller 非模态状态下的的过渡动画,就不能使用之前说的那个UIViewControllerTransitioningDelegate委托解决了,就需要用UINavigationControllerDelegate,接口方法比较类似,但也不完全一样

  • 你要使用怎样的动画类(UIViewControllerAnimatedTransitioning)展示过渡效果?
  • 你要使用怎样的过渡交互类(UIViewControllerInteractiveTransitioning)处理过渡交互?
  • willShowViewController,didShowViewController : 生命周期事件
  • navigationControllerSupportedInterfaceOrientations: 屏幕支持的方向

UIViewControllerInteractiveTransitioning

这个类用于实现在转场过路效果中的交互,比如在demo中,用它实现了一个手指下滑解除modal状态的效果

UIViewControllerContextTransitioning

UIViewControllerAnimatedTransitioning协议的关键方法animateTransition(transitionContext: UIViewControllerContextTransitioning)里面可以得到,使用transitionContext可以获取一些重要的上下文信息,比如前后的controller,转换时的容器等,示例如下

        //拿到前后的两个controller        let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!        let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!        //拿到Presenting的最终Frame        let finalFrameForVC = transitionContext.finalFrameForViewController(toVC)        //拿到转换的容器view        let containerView = transitionContext.containerView()        //拿到过渡动画执行时间        transitionDuration(transitionContext)

controller modal过渡效果

我们先来实现一个简单的示例,点击一个按钮,出现一个modal controller,自定义从下往上弹出并且有些回弹效果的过渡动画。从这个例子中我们可以了解

  • 如何实现UIViewControllerTransitioningDelegate
  • 如何实现UIViewControllerAnimatedTransitioning
  • 如何组合在一起完成功能

示例效果见demo点击后,弹出的绿色界

步骤1:界面画出按钮,点击之后用 modal 显示 To2ViewController,并设置transitioningDelegate指向自己

    //模态视图切换效果    @IBAction func Transitioning2(sender: AnyObject) {        let toVC = To2ViewController()        //设置transitioning委托为自己        toVC.transitioningDelegate = self        navigationController?.presentViewController(toVC, animated: true, completion: nil)    }

步骤2:controller 实现 UIViewControllerTransitioningDelegate

demo中使用了extension的方式继承UIViewControllerTransitioningDelegate,好处是代码逻辑分离

//模态视图切换效果extension ControllerTransitioningDemoViewController:UIViewControllerTransitioningDelegate{    //返回Presented使用的UIViewControllerAnimatedTransitioning类    func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning?{        return PresentedAnimation() // PresentedAnimation 是自定义的过渡动画效果的实现类,继承自UIViewControllerAnimatedTransitioning 步骤3中介绍它    }}

步骤3: 实现一个过渡效果为自下而上弹出并有些晃动的UIViewControllerAnimatedTransitioning类

就如之前对UIViewControllerAnimatedTransitioning介绍的那样,需要继承自UIViewControllerAnimatedTransitioning,然后实现它的三个委托方法,具体实现请看代码注释

import UIKitpublic class PresentedAnimation: NSObject,UIViewControllerAnimatedTransitioning {    public func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval{        //转场过渡动画的执行时间        return 0.6    }    // This method can only  be a nop if the transition is interactive and not a percentDriven interactive transition.    //在进行切换的时候将调用该方法,我们对于切换时的UIView的设置和动画都在这个方法中完成。    public func animateTransition(transitionContext: UIViewControllerContextTransitioning){        //拿到前后的两个controller        let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!        let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!        //拿到Presenting的最终Frame        let finalFrameForVC = transitionContext.finalFrameForViewController(toVC)        //拿到转换的容器view        let containerView = transitionContext.containerView()        let bounds = UIScreen.mainScreen().bounds        toVC.view.frame = CGRectOffset(finalFrameForVC, 0, bounds.size.height)        containerView!.addSubview(toVC.view)        //自下而上弹出toVC的动画        UIView.animateWithDuration(transitionDuration(transitionContext),                                    delay: 0.0,                                    usingSpringWithDamping: 0.7,                                    initialSpringVelocity: 0.0,                                    options: .CurveLinear,                                    animations: {                                    fromVC.view.alpha = 0.5                                    toVC.view.frame = finalFrameForVC                                    }, completion: {                                        finished in                                        transitionContext.completeTransition(true)                                        fromVC.view.alpha = 1.0                                    })         NSLog("animateTransition")    }    //执行完成后的回调    public func animationEnded(transitionCompleted: Bool){            NSLog("animation ended")    }}

madal的转场动画分为2类,present和dismiss,刚才我们实现的是animationControllerForPresentedController,这是present类,下节我们实现dissmiss的转场过渡效果

controller dismiss过渡效果

present的过渡效果实现和modal过渡效果类似,也是设置委托、实现委托、实现动画,就不详细说明了,大家可以参考demo。这里我们只说说和present过渡的区别

区别1 委托入口不同

    //Presented使用的委托    func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning?{    }    //Dismiss使用的委托    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {        //返回一个UIViewControllerAnimatedTransitioning类型        return DismissAnimation()    }

区别2 动画效果区别(这点其实不算真正的区别,因为你也可以设置为相同的效果):demo中present动画是从下而上,而dismiss的动画是自上而下。

DismissAnimation的实现

import UIKitclass DismissAnimation:NSObject,UIViewControllerAnimatedTransitioning {    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval{        return 0.6    }    func animateTransition(transitionContext: UIViewControllerContextTransitioning){        let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!        let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!        let screenBounds = UIScreen.mainScreen().bounds        let initFrame = transitionContext.initialFrameForViewController(fromVC)        let finalFrame = CGRectOffset(initFrame, 0, screenBounds.size.height)        let containerView = transitionContext.containerView()!        containerView.addSubview(toVC.view)        containerView.sendSubviewToBack(toVC.view)        let duration: NSTimeInterval = self.transitionDuration(transitionContext)        UIView.animateWithDuration(duration, animations: {            fromVC.view.frame = finalFrame            }, completion: {                (finished: Bool) in transitionContext.completeTransition(!transitionContext.transitionWasCancelled())        })     }}

controller push、pop过渡效果

push、pop和present、dismiss的过渡走的是两个完全不同的委托,委托里面的方法有相似之处,比如都可以分为过渡和交互两类。交互的内容后面再说,先说过渡效果的区别。

实现的委托不同: push、pop自定义过渡动画,需要实现UINavigationControllerDelegate,而present、dismiss实现的是UIViewControllerTransitioningDelegate

区分类型方式不同: UIViewControllerTransitioningDelegate通过2个委托present和dismiss区分开来,而在UINavigationControllerDelegate中,对应转场过渡动画只有一个委托,通过委托中的参数operation: UINavigationControllerOperation 区分pop和push

//push、pop视图切换extension ControllerTransitioningDemoViewController:UINavigationControllerDelegate{    func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?{        let transitioningAnimation = ExpandAnimation(type:operation)        transitioningAnimation.sender = transitioningSender        //返回动画的实现类        return transitioningAnimation    }}

UIViewControllerAnimatedTransitioning动画类的实现,完成点击后圆形区域放大过渡的动画效果

步骤1添加一个按钮,点击使用push的方式跳转到页面To1ViewController,并设置委托

    //推出视图切换效果    @IBAction func Transitioning1(sender: AnyObject) {        let toVC = To1ViewController()        //设置委托        navigationController?.delegate = self        //主要是动画实现圆形扩大效果,需要知道一个初始园的位置,所以把uiview传过去。这种方式传递uiview不是一个很好的方式,这里为了demo能尽量的简单,所以这么做了        transitioningSender = sender as! UIView        navigationController?.pushViewController(toVC, animated: true)    }

步骤2实现UINavigationControllerDelegate

//push、pop视图切换extension ControllerTransitioningDemoViewController:UINavigationControllerDelegate{    func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?{        let transitioningAnimation = ExpandAnimation(type:operation)        transitioningAnimation.sender = transitioningSender        //返回动画的实现类        return transitioningAnimation    }}

步骤3 完成点击后圆形区域放大过渡的动画效果的实现类ExpandAnimation

这个动画效果我就简单说说实现步骤,其余的大家看看代码。下面的参考文章中,有一篇对这个效果说的比较详细,大家可以去阅读

实现这样一个效果,基本原理是使用遮罩层,罩住presenting,有一个小圆是初始按钮点击的圆形路径,一个大圆是大于presenting的圆形路径。大圆和小圆作为遮罩的路径。判断过渡类型是presention -》 presenting还是presenting -》 presention,分别做不同的处理

presention -》 presenting : 小圆作为初始遮罩层路径,罩住presenting,使用baseAnimation动画把遮罩层的路径从小圆路径变为大圆路径,presenting即可显示出来,完成过渡效果。 小圆的位置是通过跳转点击按钮决定的,小圆位置不同会影响到大圆结束的位置,所以分了左上、左下、右上、右下四个位置分别处理。这个步骤由于偷懒在presenting -》 presention这个过程里面被省略了,每次都指定了固定的小圆位置

presenting -》 presention : 使用大圆遮住presenting,使用baseAnimation动画把遮罩层的路径从大圆路径变为小圆路径,presenting慢慢变小到看不见,presention慢慢即可显示出来,完成过渡效果。

import UIKitclass ExpandAnimation: NSObject, UIViewControllerAnimatedTransitioning {    //保存上下文    var transitionContext:UIViewControllerContextTransitioning!    //Pop or push    var type:UINavigationControllerOperation!    //初始点击的uiview对象,需要他的frame作为初始位置    var sender:UIView?    convenience init(type:UINavigationControllerOperation) {        self.init()        self.type = type    }    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval{        return 0.5    }    func animateTransition(transitionContext: UIViewControllerContextTransitioning){        self.transitionContext = transitionContext        NSLog("animateTransition")        if(type == .Push){            PushTransition(transitionContext)        }else if(type == .Pop){            PopTransition(transitionContext)        }    }    func animationEnded(transitionCompleted: Bool){        NSLog("animation ended")    }    //弹出效果 在固定位置进行的动画,可以根据需要改成动态位置触发    func PopTransition(transitionContext: UIViewControllerContextTransitioning){        let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!        let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!        let containerView = transitionContext.containerView()        let view = toVC.view!        containerView!.addSubview(toVC.view)        containerView!.addSubview(fromVC.view)        //遮罩层        let mask = CAShapeLayer()        fromVC.view.layer.mask = mask        //画出小圆        let s_center = CGPoint(x: 50, y: 50)        let s_radius:CGFloat =  sqrt(800)        let s_maskPath = UIBezierPath(ovalInRect:CGRectInset(CGRect(x: s_center.x, y: s_center.y, width: 1, height: 1), -s_radius, -s_radius))        //        mask.path = s_maskPath.CGPath        //画出大圆        let l_center = CGPoint(x: 50, y: 50)        let l_radius = sqrt( pow(view.bounds.width - l_center.x, 2) + pow(view.bounds.height - l_center.y, 2) ) + 150        let l_maskPath = UIBezierPath(ovalInRect:CGRectInset(CGRect(x: l_center.x, y: l_center.y, width: 1, height: 1), -l_radius, -l_radius))        let baseAnimation = CABasicAnimation(keyPath: "path")        baseAnimation.duration = transitionDuration(transitionContext)        baseAnimation.fromValue = l_maskPath.CGPath        baseAnimation.toValue = s_maskPath.CGPath        baseAnimation.delegate = self        baseAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)        mask.addAnimation(baseAnimation, forKey: "path")    }    //present 动画,根据触发点的位置开始启动动画    func PushTransition(transitionContext: UIViewControllerContextTransitioning){        let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!        let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!        let finalFrame = transitionContext.finalFrameForViewController(toVC)        let containerView = transitionContext.containerView()        let view = toVC.view!        containerView!.addSubview(toVC.view)        //小圆路径        let s_maskPath = UIBezierPath(ovalInRect:(sender?.frame)!)        //大圆路径        let l_center =  (sender?.center)!        var l_radius:CGFloat        if(sender!.frame.origin.x > (toVC.view.bounds.size.width / 2)){            if (sender!.frame.origin.y < (toVC.view.bounds.size.height / 2)) {                //右上角                l_radius = sqrt( pow(0 - l_center.x, 2) + pow(CGRectGetMaxY(view.frame) - l_center.y, 2) )            }else{                //右下角                l_radius = sqrt( pow(0 - l_center.x, 2) + pow(0 - l_center.y, 2) )            }        }else{            if (sender!.frame.origin.y < (toVC.view.bounds.size.height / 2)) {                //左上角                l_radius = sqrt( pow(CGRectGetMaxX(view.frame) - l_center.x, 2) + pow(CGRectGetMaxY(view.frame) - l_center.y, 2) )            }else{                //左下角                l_radius = sqrt( pow(CGRectGetMaxX(view.frame) - l_center.x, 2) + pow(0 - l_center.y, 2) )            }        }        l_radius += 50 //稍微增加一些位置        let l_maskPath = UIBezierPath(ovalInRect:CGRectInset(CGRect(x: l_center.x, y: l_center.y, width: 1, height: 1), -l_radius, -l_radius))        //遮罩层        let mask = CAShapeLayer()        mask.path = l_maskPath.CGPath        view.layer.mask = mask        ////错误用法,animationWithDuration不能通过操作layer产生动画        //UIView.animateWithDuration(5) { () -> Void in        //     mask.path = b_maskPath.CGPath        //}        let baseAnimation = CABasicAnimation(keyPath: "path")        baseAnimation.duration = transitionDuration(transitionContext)        baseAnimation.fromValue = s_maskPath.CGPath        baseAnimation.toValue = l_maskPath.CGPath        baseAnimation.delegate = self        baseAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)        mask.addAnimation(baseAnimation, forKey: "path")    }    override func animationDidStop(anim: CAAnimation, finished flag: Bool) {        //动画完成后去处遮罩        self.transitionContext.completeTransition(true)        //动画完成后去处遮罩        self.transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)?.view.layer.mask = nil        self.transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)?.view.layer.mask = nil    }}

自定义过渡交互,实现一个下滑手势解除modal状态的效果

UINavigationControllerDelegate和UIViewControllerTransitioningDelegate委托都有对交互的方法支持,返回一个UIViewControllerInteractiveTransitioning对象,他是实现过渡交互的具体实现。

这里我们只实现UIViewControllerTransitioningDelegate的交互,UINavgationController的实现和它类似。这个demo参考了猫神的文章:iOS7中的ViewController切换 区别是,这里使用swift实现。猫神在他的文章中有几个地方没看明白,后来自己写了一遍才明白,这里会仔细说明下。

第一步:实现interactionControllerForDismissal委托

   //present、dismiss视图切换效果   extension ControllerTransitioningDemoViewController:UIViewControllerTransitioningDelegate{    ...    //返回dismiss使用的UIViewControllerAnimatedTransitioning类    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {        return DismissAnimation()    }    //返回dismiss交互时的使用的UIViewControllerInteractiveTransitioning类    func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {       return interactiveTransition.isInteracting ? interactiveTransition : nil    }   }

第二步:完成DismissAnimation动画效果

DismissAnimation动画和前文中的PresentedAnimation动画效果类似,只是一个自下而上一个自上而下

import UIKitclass DismissAnimation:NSObject,UIViewControllerAnimatedTransitioning {    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval{        return 0.6    }    func animateTransition(transitionContext: UIViewControllerContextTransitioning){        let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!        let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!        let screenBounds = UIScreen.mainScreen().bounds        let initFrame = transitionContext.initialFrameForViewController(fromVC)        let finalFrame = CGRectOffset(initFrame, 0, screenBounds.size.height)        let containerView = transitionContext.containerView()!        containerView.addSubview(toVC.view)        containerView.sendSubviewToBack(toVC.view)        let duration: NSTimeInterval = self.transitionDuration(transitionContext)        UIView.animateWithDuration(duration, animations: {            fromVC.view.frame = finalFrame            }, completion: {                (finished: Bool) in transitionContext.completeTransition(!transitionContext.transitionWasCancelled())        })     }}

第三步:完成SwipeUpInteractiveTransition交互处理

SwipeUpInteractiveTransition继承自UIPercentDrivenInteractiveTransition,UIPercentDrivenInteractiveTransition继承UIViewControllerInteractiveTransitioning,使用UIPercentDrivenInteractiveTransition可以帮你省许多事情。

实现的原理是让SwipeUpInteractiveTransition监控presenting view的手势,检测手指y位置的移动,如果超过200,则标志为完成。大家可以看下代码,交互实现主要是那三个方法的运用。

  • updateInteractiveTransition() : 更新界面,实际效果就是手指上下移动时presenting会跟着上下移动
  • cancelInteractiveTransition(): 取消交互,页面恢复presenting
  • finishInteractiveTransition(): 完成交互,页面切到presention
 import UIKit class SwipeUpInteractiveTransition: UIPercentDrivenInteractiveTransition {     var vc:UIViewController?     //是否正在交互     var isInteracting:Bool = false     //是否判断交互完成     var shouldComplete:Bool = false     init(vc:UIViewController) {         super.init()         self.vc = vc         //添加手势         vc.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: "panGestureHandler:"))     }     //处理滑动手势     func panGestureHandler(gesture:UIPanGestureRecognizer){         let translation = gesture.translationInView(gesture.view)         switch(gesture.state){         case .Began:             //标记交互开始,dismiss model             isInteracting = true             vc?.dismissViewControllerAnimated(true, completion: nil)         case .Changed:             var fraction = Float(translation.y / 400)             //限制fraction值在0-1之间             fraction = fminf(fmaxf(fraction, 0.0), 1.0)             shouldComplete = fraction > 0.5             updateInteractiveTransition(CGFloat(fraction))             NSLog("x:%f y:%f" , translation.x,translation.y)             NSLog("fraction:%f" , fraction)             NSLog("shouldComplete:%@" ,shouldComplete)         case .Ended , .Cancelled:              isInteracting = false              if(!shouldComplete || gesture.state == .Cancelled){                  cancelInteractiveTransition()              }else{                  finishInteractiveTransition()              }         default:break         }     } }

这里要注意下,为什么.Began时候就要执行 vc?.dismissViewControllerAnimated(true, completion: nil) ,因为执行dismiss的时候不会直接dismiss,会进入interactionControllerForDismissal委托问你要UIViewControllerInteractiveTransitioning, 因为设置了标志位isInteracting,所以会返回SwipeUpInteractiveTransition,接着处理手指移动和移动结束事件。涉及到很多委托方法的先后发生顺序的问题,请看下节内容

controller过渡和UIViewController的委托事件发生的先后顺序

presention -》 presenting :弹出模态视图过程

     presenting viewWillAppear     animateTransition start     presenting viewDidAppear     presention viewDidDisappear     animateTransition ended

presenting -》presention :解除模态视图过程

    presention viewWillAppear    animateTransition start    presention viewDidAppear    presenting viewDidDisappear    animateTransition ended

可以看出来,animate 发生在 viewWillAppear和viewDidAppear之间的,并且在viewDidDisappear后,animateTransition才结束。了解这些的发生先后顺序,对理解整个过渡动画的处理过程很好帮助

demo



0 0
原创粉丝点击