iOS中应该知道的自定义各种Controller的转场过渡动画

前言

正如标题所示,iOS开发中, 自定义转场的过渡动画确实是必须要了解的, 在iOS7之后实现也是很简单的. 如果会使用它, 可以实现很多比较实用的功能. 比如:
  • 如果觉得系统的UIAlertController不能满足需求, 那么你可以使用自定义转场过渡动画的方式来实现弹出自定义的控制器(同时实现比较实用的动画效果).
  • 系统默认的present是从下方弹出控制器, 可以通过自定义转场过渡动画的方式来自定义切换页面的动画
  • 利用手势实现tabbarController滑动切换页面
  • 利用手势实现navigationController全屏返回的功能
  • ......
    本篇中首先介绍自定义present/dismiss的转场动画的方式 Demo地址swift3.0, [Demo地址swift2.3](jasnig:FullScreenPopNavigationController zeroj$ git push github master)

最终效果如下

present.gif
push.gif

一` 在iOS7以后Apple提供了很方便的接口来实现自定义转场动画, 使用起来很是简单方便,在实现过程中会接触到三个对象.

  • Delegate: 一个继承自NSObject的代理, 并且需要遵守相关的协议, 用来指定动画中需要的其他两个对象(下面提到的两个), 需要遵守相关的协议如下
    • (UIViewControllerTransitioningDelegate -- 自定义present/dismiss的时候)
    • UINavigationControllerDelegate --- 自定义navigationController转场动画的时候
    • UITabBarControllerDelegate --- 自定义tabbarController转场动画的时候
    • ......
  • UIViewControllerAnimatedTransitioning: 这个协议中提供了接口, 遵守这个协议的对象实现动画的具体内容
  • UIViewControllerInteractiveTransitioning: 这个协议中提供了手势交互动画的接口, 不过, 我们大多都是使用它的一个子类UIPercentDrivenInteractiveTransition来更简单的实现手势交互动画

二` 了解UIViewControllerTransitioningDelegate

  • 这个代理需要提供两种类型的对象给系统来实现自定义动画, 如果没有提供, 将会使用系统默认的动画效果
  • 第一种类型对象是遵守UIViewControllerAnimatedTransitioning协议的对象
// 自定义present弹出控制器时的动画需要提供的遵守UIViewControllerAnimatedTransitioning对象
    optional public func animationController(forPresentedController presented: UIViewController, presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning?
// 自定义dismiss移除控制器时的动画需要提供的遵守UIViewControllerAnimatedTransitioning对象
    @available(iOS 2.0, *)
    optional public func animationController(forDismissedController dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
  • 第二种类型对象是遵守UIViewControllerInteractiveTransitioning的对象
// 自定义交互动画(手势, 或者重力感应...)需要提供的遵守UIViewControllerInteractiveTransitioning对象
    optional public func interactionController(forDismissal animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?

三` 了解UIViewControllerAnimatedTransitioning

  • 这个协议是上面提到的代理来获取到具体的动画操作的
  • 遵守这个协议的对象来只需要实现两个必须的方法
// 通过这个方法获取到动画执行的时间
    public func transitionDuration(_ transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
// 在这个方法中通过获取到源控制器和目标控制器等来执行动画
    // This method can only  be a nop if the transition is interactive and not a percentDriven interactive transition.
    public func animateTransition(_ transitionContext: UIViewControllerContextTransitioning)

四` 了解UIPercentDrivenInteractiveTransition

  • UIPercentDrivenInteractiveTransition 是实现了
    UIViewControllerInteractiveTransitioning这个协议的
    我们使用UIPercentDrivenInteractiveTransition可以简单的
    通过调用提供的几个函数来执行具体的动画
    (会调用UIViewControllerAnimatedTransitioning里面实现的动画)
  • 一般可以通过继承(也可不继承)它来实现可交互动画
  • 在子类中通过添加手势(或者其他方式)到相应的view上面, 在手势的响应方法
    中根据不同的手势状态来进行不同的交互动画的操作, 一般使用到如下三个函数
// 更新动画进度
   public func update(_ percentComplete: CGFloat)
// 取消交互动画
    public func cancel()
// 完成交互动画
    public func finish()

五` 了解UIViewControllerContextTransitioning

在UIViewControllerAnimatedTransitioning协议的
实现具体动画的函数中

func animateTransition(_ transitionContext:UIViewControllerContextTransitioning)

我们会接触到UIViewControllerContextTransitioning
这个接口用来提供切换上下文给开发者使用,包含了从哪个VC到哪个VC等
各类信息, 我们可以很方便的获取到源控制器和目标控制器...很多我们需要的属性

* 使用viewControllerForKey: 获取到源控制器和目标控制器
* 使用containerView获取到当前的containerView, 将要执行动画的view都在这个containerView上进行
* 使用viewForKey: 获取到将要添加或者移除的view(一般是控制器的view)
* 使用finalFrameForViewController:获取到将要添加或者移除的view的最终frame
* 注意 'from' -> 指的的当前正在屏幕上显示的控制器(present和dismiss的时候是不一样的)

六` 自定义present/dismiss动画的系统调用过程

  1. 首先设置controller的代理transitioningDelegate为我们自定义的, 如果我们的代理里面没有提供上面所需要的对象, 那么将会使用系统默认的
prenting动画执行过程
  • UIKit首先会调用代理的
    animationControllerForPresentedController:presentingController:sourceController:方法取得自定义的动画对象
  • UIKit接着调用代理的 interactionControllerForPresentation: 方法看是否支持交互性动画, 如果返回nil表示不支持
  • UIKit接着调用代理的 transitionDuration: 方法获取动画执行的时间
  • 如果是不可交互的动画UIKit会调用代理的animateTransition:方法来执行真正的动画,
    如果是可交互的动画, UIKit会调用代理的startInteractiveTransition:方法开始动画
  • 接着是执行动画的操作, 并且等待代理调用completeTransition:结束动画(所以我们一定需要在动画执行完毕后调用这个方法, 告诉系统我们的动画执行完毕或者中途取消了)

dismiss动画执行过程和上面只有第一步和第二步调用的代理方法不一样
例如第一步调用(animationControllerForDismissedController:), 其他是相同的过程

七` 下面以自定义present/dismiss动画过程示例上面提到的各种用法(注意: 使用的swift3.0 xcode8, 如果是使用oc或者swift低版本的朋友请对应转换相应的语法)

  • 首先新建一个CustomAnimator继承自NSObject, 并且遵守UIViewControllerAnimatedTransitioning协议, 来处理动画的实现
class CustomAnimator:NSObject, UIViewControllerAnimatedTransitioning {
  • 然后实现这个协议中必须的两个方法来实现具体的动画
class CustomAnimator:NSObject, UIViewControllerAnimatedTransitioning {
    
    let duration = 0.35
// 返回动画时间
    func transitionDuration(_ transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return duration
    }
 // 处理具体动画, 通过transitionContext可以获取到很多我们需要的东西
    func animateTransition(_ transitionContext: UIViewControllerContextTransitioning) {
        // fromVc 总是获取到正在显示在屏幕上的Controller
        let fromVc = transitionContext.viewController(forKey: UITransitionContextFromViewControllerKey)!
        // toVc 总是获取到将要显示的controller
        let toVc = transitionContext.viewController(forKey: UITransitionContextToViewControllerKey)!
        let containView = transitionContext.containerView()
        
        let toView: UIView
        let fromView: UIView
        
        if transitionContext.responds(to:NSSelectorFromString("viewForKey:")) {
            // 通过这种方法获取到view不一定是对应controller.view
            toView = transitionContext.view(forKey: UITransitionContextToViewKey)!
            fromView = transitionContext.view(forKey: UITransitionContextFromViewKey)!
        } else { // Apple文档中提到不要直接使用这种方法来获取fromView和toView
            toView = toVc.view
            fromView = fromVc.view
        }
        //  添加toview到最上面(fromView是当前显示在屏幕上的view不用添加)
        containView.addSubview(toView)
        
        // 最终显示在屏幕上的controller的frame
        let visibleFrame = transitionContext.initialFrame(for: fromVc)
        // 隐藏在右边的controller的frame
        let rightHiddenFrame = CGRect(origin: CGPoint(x: visibleFrame.width, y: visibleFrame.origin.y) , size: visibleFrame.size)
        // 隐藏在左边的controller的frame
        let leftHiddenFrame = CGRect(origin: CGPoint(x: -visibleFrame.width, y: visibleFrame.origin.y) , size: visibleFrame.size)

        // toVc.presentingViewController --> 弹出toVc的controller
        // 所以如果是present的时候  == fromVc
        // 或者可以使用 fromVc.presentedViewController == toVc
        
        let isPresenting = toVc.presentingViewController == fromVc
        
        if isPresenting {// present Vc左移
            toView.frame = rightHiddenFrame
            fromView.frame = visibleFrame
        } else {// dismiss Vc右移
            fromView.frame = visibleFrame
            toView.frame = leftHiddenFrame
            // 有时需要将toView添加到fromView的下面便于执行动画
//            containView.insertSubview(toView, belowSubview: fromView)
        }
        UIView.animate(withDuration: duration, delay: 0.0, options: [.curveLinear], animations: {
            if isPresenting {
                toView.frame = visibleFrame
                fromView.frame = leftHiddenFrame
            } else {
                fromView.frame = rightHiddenFrame
                toView.frame = visibleFrame
            }
        }) { (_) in
            let cancelled = transitionContext.transitionWasCancelled()
            if cancelled {
                // 如果中途取消了就移除toView(可交互的时候会发生)
                toView.removeFromSuperview()
            }
            // 通知系统动画是否完成或者取消了
            transitionContext.completeTransition(!cancelled)
        }
    }
}
  • 接着新建一个CustomDelegate继承自NSObject,并且遵守
    UIViewControllerTransitioningDelegate协议, 来实现动画的代理的工作
class CustomDelegate: NSObject, UIViewControllerTransitioningDelegate
  • 接着实现需要自定义的相应的方法, 并且返回所需的执行对象
class CustomDelegate: NSObject, UIViewControllerTransitioningDelegate {
    private lazy var customAnimator = CustomAnimator()
    // 提供present的时候使用到的动画执行对象
    func animationController(forPresentedController presented: UIViewController, presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return customAnimator
    }
    // 提供dismiss的时候使用到的动画执行对象
    func animationController(forDismissedController dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {  
        return customAnimator
    }
}
  • 到这里为止就已经实现了自定义的不可交互的转场动画, 可以使用了, 效果和我们图片示例的一样

class Test1Controller: UIViewController {
  // 动画代理
    let deletage = CustomDelegate()
    
    @IBAction func present(_ sender: UIButton) {
        let testVc = TestController()
        testVc.view.backgroundColor = UIColor.red()
        testVc.modalPresentationStyle = .fullScreen
        // 因为transitioningDelegate是weak 所以这里不能使用局部变量 CustomDelegate()
//        testVc.transitioningDelegate = CustomDelegate()
      // 设置代理为我们自定义的
        testVc.transitioningDelegate = deletage
// 弹出控制器
        present(testVc, animated: true, completion: nil)

    }
  • 然后我们添加可交互的对象, 首先新建 Interactive:继承自
    UIPercentDrivenInteractiveTransition
class Interactive: UIPercentDrivenInteractiveTransition
  • 接着添加手势, 并且在手势处理过程中根据不同的手势状态执行不同的操作
class Interactive: UIPercentDrivenInteractiveTransition {
// pan手势
    lazy var panGesture: UIPanGestureRecognizer = UIPanGestureRecognizer(target: self, action:  #selector(self.handlePan(gesture:)))
// 用于添加手势
    var containerView: UIView!
// 将要被dismiss的控制器, 在动画的delegate中传入
    var dismissedVc: UIViewController! = nil {
        didSet {
            containerView = dismissedVc.view
            containerView.addGestureRecognizer(panGesture)
        }
    }
// 是否执行交互动画
    var isInteracting = false
    
    override init() {
        super.init()
        
    }
    // 处理手势
    func handlePan(gesture: UIPanGestureRecognizer) {
        //动画是否完成或者取消
        func finishOrCancel() {
            let translation = gesture.translation(in: containerView)
            let percent = translation.x / containerView.bounds.width
            let velocityX = gesture.velocity(in: containerView).x
            let isFinished: Bool
            if velocityX <= 0 {
                isFinished = false
            } else if velocityX > 100 {
                isFinished = true
            } else if percent > 0.3 {
                isFinished = true
            } else {
                isFinished = false
            }
            
            isFinished ? finish() : cancel()
        }
        
        switch gesture.state {

            case .began:
// 手势开始, 开启交互动画, 并且dismiss(需要设置animated: true)
                isInteracting = true
                // dimiss
                dismissedVc.dismiss(animated: true, completion: nil)
            case .changed:
// 手势改变状态, 计算动画的进度
                if isInteracting {// 开始执行交互动画的时候才设置为非nil
                    let translation = gesture.translation(in: containerView)
                    var percent = translation.x / containerView.bounds.width
                    if percent < 0 {
                        percent = 0
                    }
// 更新动画
                    update(percent)
                    
                }
            case .cancelled:
                if isInteracting {
                    finishOrCancel()
                    isInteracting = false
                    
                }
            case .ended:
                if isInteracting {
                    finishOrCancel()
                    isInteracting = false
                    
                }
            default:
                break
        }
    }
}
  • 接着在CustomDelegate里面增加实现可交互动画的执行对象和接口
// 注意在present接口里面设置了
//  interactive.dismissedVc = presented
    private lazy var interactive = Interactive()

    // 提供dismiss的时候使用到的可交互动画执行对象
    func interactionController(forDismissal animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        // 因为执行自定义动画会先调用这个方法, 如果返回不为nil, 那么将不会执行非交互的动画!!
        // 所以isInteracting只有在手势开始的时候才被设置为true
        // 返回nil便于不是可交互的时候就直接执行不可交互的动画
        return interactive.isInteracting ? interactive : nil
    }

就是这样就实现了利用手势滑动返回的可交互动画, 现在运行, 将会看到图片的示例效果, 还是很简单?!!!!

这里以自定义present/dismiss为例详细的介绍了自定义转场动画的使用, 那么到现在, 你是可以很自由的去实现各种需要的自定义动画(navigationController, tabBarController...), 并且增加各种交互动画(滑动, 捏合, 甚至设备摇晃...), 希望你会很愉快的使用它 Demo地址swift3.0, [Demo地址swift2.3](jasnig:FullScreenPopNavigationController zeroj$ git push github master) 欢迎关注, 欢迎star

推荐阅读更多精彩内容