美文网首页Swift学习
iOS-Swift转场动画详解

iOS-Swift转场动画详解

作者: 正直的瓜子脸 | 来源:发表于2020-11-27 16:31 被阅读0次

    一、什么是转场

    转场:通俗讲,转场就是从当前页面跳转到下一个页面的过程
    转场动画: 在当前视图消失和下一个视图出现的过程中,执行的动画,就是转场动画。
    动画代理: UIViewControllerAnimatedTransitioning

    二、转场的类型

    按照页面切换类型分为(前两种属于容器VC转场):
    1)、Modal转场: presentation、dismissal
    转场代理: UIViewControllerTransitioningDelegate
    2)、UINavigationController转场: push、pop
    转场代理: UINavigationControllerDelegate
    3)、UITabBarController转场: Tab切换
    转场代理: UITabBarControllerDelegate

    按照转场进度是否可控分为:
    1)、非交互式转场
    2)、交互式转场

    三、转场结果

    非交互式转场: 完成
    交互式转场: 完成、取消

    -------------这是一根牛逼闪闪放光彩的分割线------------------

    上面扯了那么多,现在接上正文,下面我们将实现Navi、Modal、Tab、三种方式的转场动画,前两种都是模仿的系统动画,旨再阐述你如何实现转场动画。如果你要其他酷炫的动画,只要在UIViewControllerAnimatedTransitioning协议的方法里,修改成你需要的酷炫动画就行了。
    首先,为了方便调用,我们来封装一个工具类,把三种转场类型的代理、转场动画封装到一起,也就是说这个工具类需要实现UIViewControllerTransitioningDelegate, UINavigationControllerDelegate, UITabBarControllerDelegate以及UIViewControllerAnimatedTransitioning四个代理方法。

    封装转场工具类

    ///通用转场工具类
    class TransitionUtil: NSObject {
        ///转场类型
        var transitionType: TransitionType?
        //交互转场
        var interactive = false
        let interactionTransition = UIPercentDrivenInteractiveTransition()
        
        override init() {
            super.init()
            
        }
    }
    
    ///转场类型
    enum TransitionType {
        //导航栏
        case navigation(_ operation: UINavigationController.Operation)
        //tabBar切换
        case tabBar(_ direction: TabBarOperationDirection)
        //模态跳转
        case modal(_ operation: ModalOperation)
    }
    
    enum TabBarOperationDirection {
        case left
        case right
    }
    
    enum ModalOperation {
        case presentation
        case dismissal
    }
    
    1)、模态转场代理: UIViewControllerTransitioningDelegate
    ///自定义模态转场动画时使用
    extension TransitionUtil: UIViewControllerTransitioningDelegate {
        func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            self.transitionType = .modal(.presentation)
            return self
        }
        
        func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            self.transitionType = .modal(.dismissal)
            return self
        }
        
        //interactive false:非交互转场, true: 交互转场
        func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
            return interactive ? self.interactionTransition : nil
        }
        
        //interactive false:非交互转场, true: 交互转场
        func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
            return interactive ? self.interactionTransition : nil
        }
    }
    
    2) 、导航转场代理: UINavigationControllerDelegate
    /// 自定义navigation转场动画时使用
    extension TransitionUtil: UINavigationControllerDelegate {
        func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            self.transitionType = .navigation(operation)
            return self
        }
        
        func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
            return interactive ? self.interactionTransition : nil
        }
    }
    
    3)、TabBar转场代理: UITabBarControllerDelegate
    /// 自定义tab转场动画时使用
    extension TransitionUtil: UITabBarControllerDelegate {
        func tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            let fromIndex = tabBarController.viewControllers?.firstIndex(of: fromVC) ?? 0
            let toIndex = tabBarController.viewControllers?.firstIndex(of: toVC) ?? 0
            let direction: TabBarOperationDirection = fromIndex < toIndex ? .right : .left
            self.transitionType = .tabBar(direction)
            return self
        }
        
        func tabBarController(_ tabBarController: UITabBarController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
            return interactive ? self.interactionTransition : nil
        }
    }
    
    4)、转场动画代理

    尽管三大转场代理协议的方法不尽相同,但它们返回的动画控制对象遵守的是同一个协议: UIViewControllerAnimatedTransitioning

    extension TransitionUtil: UIViewControllerAnimatedTransitioning {
        //控制转场动画执行时间
        func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
            return 0.5
        }
        
        //执行动画的地方,最核心的方法。
        func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
            transitionAnimation(transitionContext: transitionContext)
        }
    }
    
    //MARK:  ------------  不同类型的转场  ------------
        
        private func transitionAnimation(transitionContext: UIViewControllerContextTransitioning) {
            //获得容器视图(转场动画发生的地方)
            let containerView = transitionContext.containerView
            //动画执行时间
            let duration = self.transitionDuration(using: transitionContext)
            
            //fromVC (即将消失的视图)
            let fromVC = transitionContext.viewController(forKey: .from)!
            let fromView = fromVC.view!
            //toVC (即将出现的视图)
            let toVC = transitionContext.viewController(forKey: .to)!
            let toView = toVC.view!
            
            var offset = containerView.frame.width
            var fromTransform = CGAffineTransform.identity
            var toTransform = CGAffineTransform.identity
            
            switch transitionType {
            case .modal(let operation):
                offset = containerView.frame.height
                let fromY = operation == .presentation ? 0 : offset
                fromTransform = CGAffineTransform(translationX: 0, y: fromY)
                let toY = operation == .presentation ? offset : 0
                toTransform = CGAffineTransform(translationX: 0, y: toY)
                if operation == .presentation {
                    containerView.addSubview(toView)
                }
                
            case .navigation(let operation):
                offset = operation == .push ? offset : -offset
                fromTransform = CGAffineTransform(translationX: -offset, y: 0)
                toTransform = CGAffineTransform(translationX: offset, y: 0)
                containerView.insertSubview(toView, at: 0)
                //containerView.addSubview(toView)
                
            case .tabBar(let direction):
                offset = direction == .left ? offset : -offset
                fromTransform = CGAffineTransform(translationX: offset, y: 0)
                toTransform = CGAffineTransform(translationX: -offset, y: 0)
                containerView.addSubview(toView)
                
            case nil:
                break
            }
            
            toView.transform = toTransform
            UIView.animate(withDuration: duration, animations: {
                fromView.transform = fromTransform
                toView.transform = .identity
            }) { (finished) in
                fromView.transform = .identity
                toView.transform = .identity
                //考虑到转场中途可能取消的情况,转场结束后,恢复视图状态。(通知是否完成转场)
                let wasCancelled = transitionContext.transitionWasCancelled
                transitionContext.completeTransition(!wasCancelled)
            }
        }
    

    非交互转场跳转

    封装完工具类,基本就大功搞成了,下面只需要在跳转的时候创建一个TransitionUtil对象,设置为代理就行了。
    这里模态和导航跳转都是模仿的系统动画,如果把动画时间改长一点,你就能确实知道,这是我们自己设计的动画,而不是系统动画.
    模态跳转:

    @IBAction func modalTransitionClicked(_ sender: Any) {
            let vc = ModalTransitionViewController()
            //设置转场代理(必须在跳转前设置)
            //与容器 VC 的转场的代理由容器 VC 自身的代理提供不同,Modal 转场的代理由 presentedVC 提供
            vc.transitioningDelegate = transitionUtil
            /*
            .FullScreen 的时候,presentingView 的移除和添加由 UIKit 负责,在 presentation 转场结束后被移除,dismissal 转场结束时重新回到原来的位置;
            .Custom 的时候,presentingView 依然由 UIKit 负责,但 presentation 转场结束后不会被移除。
            */
            vc.modalPresentationStyle = .custom
            //animated: 一定要给true,不然不会出现转场动画
            self.present(vc, animated: true, completion: nil)
        }
    

    导航跳转:

    @IBAction func naviTransitionClicked(_ sender: Any) {
            let vc = NaviTransitionViewController()
            vc.hidesBottomBarWhenPushed = true
            //设置转场代理
            self.navigationController?.delegate = transitionUtil
            self.navigationController?.pushViewController(vc, animated: true)
        }
    

    Tab切换:
    创建一个继承UITabBarController的ScrollTabBarViewController类,设置代理为创建的TransitionUtil对象就行了。然后把项目中的系统类UITabBarController换成ScrollTabBarViewController就行了。
    这样点击切换tab时就会又一个左滑右滑的动画,效果如下:

    未命名.gif

    交互转场跳转

    交互转场无非就是用户自己控制转场进度,那么如何控制呢,一般都是通过一个滑动手势。不管是模态、导航、tab,这些转场类型要添加交互功能,都只需要在self.view上添加一个pan手势就行。
    下面以tab的交互转场为例:

    class ScrollTabBarViewController: UITabBarController {
        //滑动手势
        private var panGesture = UIPanGestureRecognizer()
        //转场动画
        private let transitionUtil = TransitionUtil()
        
        private var vcCount: Int {
            guard let vcs = self.viewControllers else { return 0 }
            return vcs.count
        }
        
    
        override func viewDidLoad() {
            super.viewDidLoad()
            //设置转场代理
            self.delegate = transitionUtil
            self.tabBar.tintColor = .green
            
            //添加交互手势
            panGesture.addTarget(self, action: #selector(panHandle(_:)))
            self.view.addGestureRecognizer(panGesture)
        }
      
    }
    
    @objc func panHandle(_ pan: UIPanGestureRecognizer) {
            let translationX = panGesture.translation(in: view).x
            let absX = abs(translationX)
            let progress = absX / view.frame.width
            
            switch panGesture.state {
            case .began:
                transitionUtil.interactive = true
                //速度
                let velocityX = panGesture.velocity(in: view).x
                if velocityX < 0 {
                    if selectedIndex < vcCount - 1 {
                        selectedIndex += 1
                    }
                }else {
                    if selectedIndex > 0 {
                        selectedIndex -= 1
                    }
                }
                
            case .changed:
                //更新转场进度,进度数值范围为0.0~1.0。
                transitionUtil.interactionTransition.update(progress)
                
            case .cancelled, .ended:
                /*
                 这里有个小问题,转场结束或是取消时有很大几率出现动画不正常的问题.
                 解决手段是修改交互控制器的 completionSpeed 为1以下的数值,这个属性用来控制动画速度,我猜测是内部实现在边界判断上有问题。
                 这里其修改为0.99,既解决了 Bug 同时尽可能贴近原来的动画设定.
                 */
                if progress > 0.3 {
                    transitionUtil.interactionTransition.completionSpeed = 0.99
                    //.finish()方法被调用后,转场动画从当前的状态将继续进行直到动画结束,转场完成
                    transitionUtil.interactionTransition.finish()
                }else {
                    //转场取消后,UITabBarController 自动恢复了 selectedIndex 的值,不需要我们手动恢复。
                    transitionUtil.interactionTransition.completionSpeed = 0.99
                    //.cancel()被调用后,转场动画从当前的状态回拨到初始状态,转场取消。
                    transitionUtil.interactionTransition.cancel()
                }
                //无论转场的结果如何,恢复为非交互状态。
                transitionUtil.interactive = false
                
            default:
                break
            }
        }
    

    效果如下:


    未命名.gif

    番外篇: 如何实现一个圆形转场动画

    这种效果有两种方式:
    1)、 不使用mask: 视图+缩放
    2)、 使用mask: UIBezierPath + CAShapeLayer + maskLayer

    第一种实现看起比较丝滑,下面就说一下其如何实现:
    其他地方根上面的实现一模一样,唯一的不同是UIViewControllerAnimatedTransitioning协议里animateTransition(using transitionContext: UIViewControllerContextTransitioning)的实现不同。

    //执行动画的地方,最核心的方法。
        func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
            //圆形遮罩,方式一
            bubble(transitionContext: transitionContext)
        }
    

    具体的动画实现:

    func bubble(transitionContext: UIViewControllerContextTransitioning) {
            //获得容器视图(转场动画发生的地方)
            let containerView = transitionContext.containerView
            
            //动画执行时间
            let duration = self.transitionDuration(using: transitionContext)
            
            //fromVC (即将消失的视图)
            let fromVC = transitionContext.viewController(forKey: .from)!
            let fromView = fromVC.view!
            //toVC (即将出现的视图)
            let toVC = transitionContext.viewController(forKey: .to)!
            let toView = toVC.view!
            let originalCenter = toView.center
            let originalSize = toView.frame.size
            
            //半径
            let radius = self.getRadius(startPoint: startPoint, originalSize: originalSize)
            
            switch transitionType {
            case .modal(let operation):
                if operation == .presentation {
                    let bubble = UIView()
                    bubble.frame = CGRect(x: 0, y: 0, width: radius*2, height: radius*2)
                    bubble.layer.cornerRadius = bubble.frame.size.height / 2
                    bubble.center = startPoint
                    bubble.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
                    bubble.backgroundColor = toView.backgroundColor
                    containerView.addSubview(bubble)
                    
                    toView.center = startPoint
                    toView.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
                    toView.alpha = 0
                    containerView.addSubview(toView)
                    
                                    
                    UIView.animate(withDuration: duration) {
                        bubble.transform = .identity
                        toView.transform = .identity
                        toView.alpha = 1
                        toView.center = originalCenter
                    } completion: { (isFinished) in
                        transitionContext.completeTransition(true)
                        bubble.isHidden = true
                        if toVC.modalPresentationStyle == .custom {
                            toVC.endAppearanceTransition()
                        }
                        fromVC.endAppearanceTransition()
                    }
                }else {
                    if fromVC.modalPresentationStyle == .custom {
                        fromVC.beginAppearanceTransition(false, animated: true)
                    }
                    toVC.beginAppearanceTransition(true, animated: true)
                    
                    let bubble = UIView()
                    bubble.frame = CGRect(x: 0, y: 0, width: radius*2, height: radius*2)
                    bubble.layer.cornerRadius = bubble.frame.size.height / 2
                    bubble.backgroundColor = fromView.backgroundColor
                    bubble.center = startPoint
                    bubble.isHidden = false
                    containerView.insertSubview(bubble, at: 0)
                    
                    UIView.animate(withDuration: duration) {
                        bubble.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
                        fromView.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
                        fromView.center = self.startPoint
                        fromView.alpha = 0
                    } completion: { (isFinished) in
                        transitionContext.completeTransition(true)
                        fromView.removeFromSuperview()
                        bubble.removeFromSuperview()
                        if fromVC.modalPresentationStyle == .custom {
                            fromVC.endAppearanceTransition()
                        }
                        toVC.endAppearanceTransition()
                    }
                }
            default:
                break
            }
        }
    

    计算圆的半径:

    ///获得半径 (不明白的自己画个图,看一下哪一条应该是半径)
        private func getRadius(startPoint: CGPoint, originalSize: CGSize) -> CGFloat {
            let horizontal = max(startPoint.x, originalSize.width - startPoint.x)
            let vertical = max(startPoint.y, originalSize.height - startPoint.y)
            //勾股定理计算半径
            let radius = sqrt(horizontal*horizontal + vertical*vertical)
            return radius
        }
    

    效果如下(可交互):


    未命名22222.gif

    附参考文章:
    iOS 视图控制器转场详解: https://blog.devtang.com/2016/03/13/iOS-transition-guide/

    有不明白的可以查看demo:

    demo地址: https://github.com/jps2782316/JTransition

    相关文章

      网友评论

        本文标题:iOS-Swift转场动画详解

        本文链接:https://www.haomeiwen.com/subject/gcadiktx.html