iOS~系统or自定义?总有一款转场动画让你独一无二(思路篇)

在APP同质化日益严重的情况下,一款特别的转场动画会让用户有耳目一新的感觉,此篇文章重点讲解实现转场动画的思路,也为自己的学习之路做下笔记。哈哈,废话不多说,直接进入正题.......

1.系统提供的CATransition

CATransition是CAAnimation的派生类,提供了很多不同风格的动画,功能十分强大,完全可以应付日常开发。下面看看具体如何使用:

CATransition *animation = [CATransition animation];    
//动画时间    
animation.duration = 1.0f;    
//display mode, slow at beginning and end    
animation.timingFunction = UIViewAnimationCurveEaseInOut;    
//在动画执行完时是否被移除    
animation.removedOnCompletion = NO;    
//过渡效果    
animation.type = @"pageCurl";  
/* 以下是基本的四种效果
kCATransitionPush 推入效果
kCATransitionMoveIn 移入效果
kCATransitionReveal 截开效果
kCATransitionFade 渐入渐出效果
以下API效果可以安全使用
cube 方块
suckEffect 三角
rippleEffect 水波抖动
pageCurl 上翻页
pageUnCurl 下翻页
oglFlip 上下翻转
cameraIrisHollowOpen 镜头快门开
cameraIrisHollowClose 镜头快门开 */  

//过渡方向    
animation.subtype = kCATransitionFromRight;    
//暂时不知,感觉与Progress一起用的,如果不加,Progress好像没有效果    
animation.fillMode = kCAFillModeForwards; 
//动画开始(在整体动画的百分比).    
animation.startProgress = 0.3;    
//动画停止(在整体动画的百分比).    
animation.endProgress = 0.7;

//添加到图层上
view.layer.actions = ["backgroundColor":transition]
view.layer.backgroundColor = UIColor.yellow.cgColor
 
//添加到导航栏容器视图的图层上,在push和pop时会出现动画(暂时没有找到push对应的actionKey)
self.navigationController?.view.layer.add(transition, forKey: nil)
2.自定义转场动画

自定义转场动画又可区分为非交互式和交互式,其区别为用户是否可以通过交互来实时的控制动画的进度,由浅到深,依次介绍;

2.1非交互式自定义转场动画
iOS~系统or自定义?总有一款转场动画让你独一无二(思路篇)_第1张图片
非交互式.gif

此处用一个在导航控制器跳转页面时添加淡入淡出效果的案例展示其思路:
首先我们要实现导航控制器返回转场动画的代理方法

import UIKit

class VDCustomNavigationViewController: UINavigationController,UINavigationControllerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
    
        //设置代理
        self.delegate = self
    }

    //实现代理方法判断跳转状态并返回相应动画
    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?{
    
        switch operation {
         case .push:
            return presentAnimator()
         case .pop:
            return dismissAnimator()
         default:
            return nil
        } 
    }
}

这个代理方法需要我们返回一个遵循了UIViewControllerAnimatedTransitioning协议的对象,此协议一共有三个方法,其中两个是必须要实现的:
• transitionDuration: 需要返回动画时长
• animateTransition: 需要返回一个实现了动画逻辑的转场上下文
• animationEnded:. 动画结束时候调用,此方法可以选择实现
实现代码:

class presentAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    
    /// 动画时长
    ///
    /// - parameter transitionContext: 转场上下文
    ///
    /// - returns: 动画时长
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        
        return 2.0
    }
    
    /// 转场动画实现方法 - 一旦实现此函数,系统的动画方法,将由程序员负责
    ///
    /// - parameter transitionContext: 转场上下文 - 提供转场动画的所有细节
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        
        // 1 获取视图
        let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)!
        
        // 2 添加到容器视图
        transitionContext.containerView.addSubview(toView)
        
        // 3 设置透明度
        toView.alpha = 0
        
        // 4 动画转场
        UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
            
            //淡入
            toView.alpha = 1
        }) { _ in
            
            //返回完成状态
            transitionContext.completeTransition(true)
        }
    }
}

class dismissAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    
    /// 动画时长
    ///
    /// - parameter transitionContext: 转场上下文
    ///
    /// - returns: 动画时长
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        
        return 2.0
    }
    
    /// 转场动画实现方法 - 一旦实现此函数,系统的动画方法,将由程序员负责
    ///
    /// - parameter transitionContext: 转场上下文 - 提供转场动画的所有细节
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        
        //1 获取toView
        let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)!
        
        // 2 添加到容器视图
        transitionContext.containerView.addSubview(toView)
        
        // 3 获取fromView
        let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from)!
        
        // 4 添加到容器视图
        transitionContext.containerView.addSubview(fromView)
        
        // 5 动画转场
        UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
            
            //淡出
            fromView.alpha = 0
        }) { _ in
           
            //返回完成状态
            transitionContext.completeTransition(true)
        }
    }
}

在present控制器时添加自定义转场动画与导航控制器有所区别:

class fromViewController: UIViewController, UIViewControllerTransitioningDelegate{
     override func viewDidLoad() {
            super.viewDidLoad()
        
            //设置转场动画代理
            fromVC.transitioningDelegate = self
         }
   
    /// 返回`提供展现动画`的对象
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        
        return presentAnimator()
    }
    
    /// 返回`提供解除转场动画`的对象
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        
       return dismissAnimator()
    }
 }
2.2交互式转场动画
iOS~系统or自定义?总有一款转场动画让你独一无二(思路篇)_第2张图片
交互式.gif

所谓交互式动画就是可以和用户实时交互,用户可以通过手势,重力等等控制动画的进度,因此我们要根据用户输入实时的更新动画进度,此处以present控制器时为例:
与非交互式转场动画不同的地方在于返回Animator时要多返回一个动画控制器

    /// 返回`提供解除转场动画`的对象
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        
    }
    ///返回解除转场的动画控制器
    func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        
    }

那么这个动画控制器如何去实现呢?首先点进UIViewControllerInteractiveTransitioning协议中并没有发现有更新或者停止动画的方法,那我们继续进入UIViewControllerContextTransitioning转场上下文中看一下,发现了三个方法:

   ///更新转场动画
    public func updateInteractiveTransition(_ percentComplete: CGFloat)
   ///完成转场动画
    public func finishInteractiveTransition()
   ///取消转场动画
    public func cancelInteractiveTransition()

通过这三个方法我们可以根据用户输入实时的更新动画进度,但是这三个方法是iOS2.0就出现的,有点久远了,我通过查阅资料发现苹果在iOS7.0的SDK就已为我们提供了封装好的动画控制器UIPercentDrivenInteractiveTransition,UIPercentDrivenInteractiveTransition遵循了UIViewControllerInteractiveTransitioning协议,并封装了控制转场动画的方法:

    //动画进度0~1
    open var percentComplete: CGFloat { get }
    //动画速度
    open var completionSpeed: CGFloat
    //更新动画需传入动画进度0~1
    open func update(_ percentComplete: CGFloat)
    ///取消动画,回到初始状态
    open func cancel()
    //完成动画,结束转场
    open func finish()

因此我们只需要创建一个继承UIPercentDrivenInteractiveTransition的类,在这个类中拿到要进行转场动画的视图控制器,添加手势并根据手势触发状态控制转场动画进度即可:

class VDPanInteractiveTransition: UIPercentDrivenInteractiveTransition {
    
    /// 需要转场的视图控制器
    var presentingVC : UIViewController?
    
    /// 是否完成
    var shouldComplete = true
    
    /// 是否正在转场
    var interacting = false
    
    /// 设置需要转场的视图控制器
    ///
    /// - Parameter presentingVC: 需要转场的视图控制器
    func setUpPresentingVC(presentingVC: UIViewController) {
        
        self.completionSpeed = 1 - self.percentComplete
        
        let pan = UIPanGestureRecognizer(target: self, action: #selector(pan(panGesture:)))
        
        self.presentingVC = presentingVC
        
        self.presentingVC?.view.addGestureRecognizer(pan)
    }
    
    /// 拖拽手势触发方法
    ///
    /// - Parameter panGesture: 拖拽手势
    func pan(panGesture : UIPanGestureRecognizer) {
        
        //获取拖拽点
        let translation = panGesture.translation(in: panGesture.view?.superview)
        
        //判断手势状态并更新动画
        switch panGesture.state {
        case .began:
            
            self.interacting = true
            self.presentingVC?.dismiss(animated: true, completion: nil)
            break
        case .changed:
            
            let pre = translation.y / 300
            
            self.shouldComplete = pre > 0.5
            
            self.update(pre)
            
            print(translation.y / 300)
            break
        case .ended:
            
            self.interacting = false
            if (!self.shouldComplete || panGesture.state == .cancelled) {
                self.cancel()
            } else {
                self.finish()
            }
            break
        case.cancelled:
            
            self.interacting = false
            if (!self.shouldComplete) {
                self.cancel()
            } else {
                self.finish()
            }
            break
        
        default:
            break
        }
    }
}

相应的interactiveDismissAnimator代码:

class interactiveDismissAnimator: NSObject,UIViewControllerAnimatedTransitioning {
    
    //设置动画时间
    public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        
        return 2.0
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        
        //获取到fromVc
        let fromVc = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
        
        //获取到toVc
        let toVc = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
        
        //设置finalFrame
        let screenBounds = UIScreen.main.bounds
        let initFrame = transitionContext.initialFrame(for: fromVc!)
        let finalFrame = initFrame.offsetBy(dx: 0, dy: screenBounds.size.height)
        
        //添加toView到上下文中
        let containerView = transitionContext.containerView
        containerView.addSubview((toVc?.view)!)
        containerView.sendSubview(toBack: (toVc?.view)!)
        
        //开始动画
        UIView.animate(withDuration: transitionDuration(using: transitionContext), animations:{
            
            fromVc?.view.frame = finalFrame
            
        }) { _ in
            
            //返回完成状态
            transitionContext.completeTransition(!(transitionContext.transitionWasCancelled))
        }
    }
}

相应的presentingVC代码:

class VDPresentingViewController: UIViewController,UIViewControllerTransitioningDelegate {

    let panInteractiveTransition = VDPanInteractiveTransition()
    let titleLabel = UILabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        //设置视图
        self.setUpUI()
        
        //把自己传递给转场动画控制器
        panInteractiveTransition.setUpPresentingVC(presentingVC: self)
        
        //设置代理
        self.transitioningDelegate = self
    }
 
    //设置视图
    func setUpUI() {
        
        view.backgroundColor = UIColor.yellow
        view.addSubview(titleLabel)
        titleLabel.center = view.center
        titleLabel.text = "向下拖拽"
        titleLabel.font = UIFont.boldSystemFont(ofSize: 20)
        titleLabel.textColor = UIColor.black
        titleLabel.sizeToFit()
    }
    
    // 返回`提供解除转场动画`的对象
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        
        return interactiveDismissAnimator()
    }
    
    //返回解除转场动画控制器
    func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        
        return self.panInteractiveTransition.interacting ? self.panInteractiveTransition : nil
    }
}

那么将这个封装好的交互式动画控制器添加到导航控制中也是类似的,此处不再赘述;本文只是用了两个最简单的转场效果讲述了实现思路,至于更加酷炫的的转场动画就要靠大家的脑洞了。
学习参考资料:
喵神的博客

3.源码

源码放在GitHub上了,欢迎指正,记得star哦!

你可能感兴趣的:(iOS~系统or自定义?总有一款转场动画让你独一无二(思路篇))