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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容