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非交互式自定义转场动画
非交互式.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交互式转场动画
交互式.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哦!

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

推荐阅读更多精彩内容