swift自定义presentViewController动画和dismiss(视图控制器切换动画)

我们在左一些app的时候经常会用到详情页,评价页 , 总之就是点一个按钮 就展示一些信息,在做一些简单的展示或者小逻辑。一般都会presentViewController。默认的动画是从下往上,但是我们想要自己主宰它的动画方式怎么弄呢?

图片来自网络

本小节将会以一个点击获取英雄详情的demo来介绍一个自定义presentViewController动画(视图控制器切换动画)

本文源码:https://github.com/smalldu/IOS-Animations
中的AnimationDemo11    

简单的效果

简单的效果

进阶效果

进阶效果

我们在左一些app的时候经常会用到详情页,评价页 , 总之就是点一个按钮 就展示一些信息,在做一些简单的展示或者小逻辑。一般都会presentViewController

大家可以下载我的代码,看看一些跟过渡动画没有关系的设置,比如文字,和半透明背景 ,下面UIScrollView等等 , 因为他们不是本节要介绍的重点,本节要介绍的重点是自定义过渡动画。

首先,创建一个Single View Application,然后在Main.storyboard中定义好搞两个界面,定义好约束 。 不懂的可以下载我源码。看源码上,也可以不搞这么复杂,随便搞两个页面 练习过渡动画就行。

我的页面结构

页面构建

页面所有元素都是基于AutoLayout约束的

然后就是创建了一盒Hero.swift 用于存放英雄的基本信息 , 然后在ViewController中将这些英雄的图像加到UIScrollView上,计算好他们的位置。

每个图像都添加 imageView.userInteractionEnabled = true 属性,可交互,然后添加点击的手势

imageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: Selector("didTapImageView:")))

然后就是在点击的时候展示详情页 , 详情页。就会传一个Hero对象的参数。没有什么非常特别的。

 func didTapImageView(tap: UITapGestureRecognizer) {
        selectedImage = tap.view as? UIImageView
        
        let index = tap.view!.tag
        let selectedHerb = he[index-1]
        //present details view controller
        let details = storyboard?.instantiateViewControllerWithIdentifier("detailViewController") as! DetailViewController
        details.he = selectedHerb
        presentViewController(details, animated: true, completion: nil)
    }

这里我把第二个控制器的设置如图

t4.png

这时候执行还是默认的效果

默认效果

如果你的控制器要设置自己的动画需要实现UIViewControllerTransitioningDelegate协议。每次你present一个新的ViewController的时候,UIKit就会看这个delegate是否使用自定义过渡。

UIKit通过调用
animationControllerForPresentedController(:_presentingController:sourceController:);方法,如果这个方法返回nil ,就会调用默认的present 。如果返回的是一个非空对象,就会使用这个对象的控制过渡 ,这个对象必须是实现UIViewControllerAnimatedTransitioning协议的对象。

UIViewControllerAnimatedTransitioning这个有两个必须的方法

  • transitionDuration 这个方法需要提供返回一个时间,动画持续时间

  • animateTransition 这个是动画的主体方法

我们新建一个PopAnimation.swift的类让它继承NSObject ,然后实现UIViewControllerAnimatedTransitioning协议 实现了协议就自然要实现那两个方法,第一个方法简单返回一个时间就行了,这里暂且返回1。

第二个方法有传一个参数 transitionContext: UIViewControllerContextTransitioning

UIViewControllerContextTransitioning是个什么东西呢 ?
当两个ViewController之间过渡的时候,刚开始新的控制器已经被创建只是还不可见,因此你的任务就是在animateTransition()方法中把新的控制器添加到transition的容器中,把它以动画的方式添加进来 , 把旧控制器以动画方式移除去

transitionContext提供两个非常便捷的方法让你获得transition对象:

  • viewForKey() :这个可以通过UITransitionContextFromViewKey 和
    UITransitionContextToViewKey 获得新旧视图]
  • viewControllerForKey(): UITransitionContextFromViewControllerKey 和
    UITransitionContextToViewControllerKey 获得新旧试图控制器

所以 我们这里先上一个最简单的动画

import UIKit

class PopAnimator: NSObject,UIViewControllerAnimatedTransitioning {

    let duration = 1.0
    //动画持续时间
    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
        return duration
    }
    
    //动画执行的方法
    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        
        let containerView = transitionContext.containerView()
        let toView = transitionContext.viewForKey(UITransitionContextToViewKey)
        containerView!.addSubview(toView!)
        toView!.alpha = 0.0
        UIView.animateWithDuration(duration,
            animations: {
                toView!.alpha = 1.0
            }, completion: { _ in
                transitionContext.completeTransition(true)
        })
    }
    
}

将它的不透明度由0变为1 ,然后在完成的时候调用动画完成方法
我们首先在ViewController中声明let transition = PopAnimator()

我们前面说了,我们的ViewController还需要实现UIViewControllerTransitioningDelegate协议

为了代码整洁,我们在ViewController最下面添加

extension ViewController:UIViewControllerTransitioningDelegate{
   
    //Present的时候 使用自定义的动画
    func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        
        return transition
    }
    
    //使用默认的动画
    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return nil
    }
}

这两个代理方法,第一个是Presentpresent返回我们自定义的,第二个暂且返回默认的。

最后别忘了加代理


 func didTapImageView(tap: UITapGestureRecognizer) {
        selectedImage = tap.view as? UIImageView
        
        let index = tap.view!.tag
        let selectedHerb = he[index-1]
        //present details view controller
        let details = storyboard?.instantiateViewControllerWithIdentifier("detailViewController") as! DetailViewController
        print(details)
        details.he = selectedHerb
        details.transitioningDelegate = self //设置过渡代理
        presentViewController(details, animated: true, completion: nil)
    }

效果

t2.gif

这样一个简单的效果就实现了。

要实现复杂的效果,我们需要一些计算。首先在PopAnimation中新增两个变量

    var presenting = true  //是否在presenting 
    var originFrame = CGRect.zero

presenting主要用来区分是present还是dismiss

然后在animateTransition 中

        let containerView = transitionContext.containerView()
        let toView = transitionContext.viewForKey(UITransitionContextToViewKey)
        let detailView = presenting ? toView :
            transitionContext.viewForKey(UITransitionContextFromViewKey)!

前两个没变,后面一个变了,如果当前是present , detail就是toView如果不是detail就是from 。

然后加上下面三句

 let initialFrame = presenting ? originFrame : detailView!.frame
 let finalFrame = presenting ? detailView!.frame : originFrame
 let xScaleFactor = presenting ?
            initialFrame.width / finalFrame.width :
            finalFrame.width / initialFrame.width

let yScaleFactor = presenting ?
                initialFrame.height / finalFrame.height :
                finalFrame.height / initialFrame.height

如果是present初始就是originFrame原如果不是初始就是detail的frame 。final同理

最后那个算出现在缩放比例

然后添加代码

 let scaleTransform = CGAffineTransformMakeScale(xScaleFactor,
            yScaleFactor)
  if presenting {
            detailView!.transform = scaleTransform
            detailView!.center = CGPoint(
            x: CGRectGetMidX(initialFrame),
            y: CGRectGetMidY(initialFrame))
            detailView!.clipsToBounds = true
 }

定义一个变换,如果present的话 先把detail先缩放(按照山上面计算的比例缩放),然后设置center。为了定位到当前点击的小图的位置。

最后一段


  containerView!.addSubview(toView!)
       containerView!.bringSubviewToFront(detailView!)
        
        UIView.animateWithDuration(duration, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0, options: UIViewAnimationOptions.AllowAnimatedContent, animations: {
                detailView!.transform = self.presenting ?
                CGAffineTransformIdentity : scaleTransform
                detailView!.center = CGPoint(x: CGRectGetMidX(finalFrame),
                y: CGRectGetMidY(finalFrame))
                
            }) { (_) -> Void in
                    
                transitionContext.completeTransition(true)
        }

第一句无可厚非,为啥要加第二句呢?containerView!.bringSubviewToFront(detailView!)

因为如果是present的话本来就应该放在最前面 ,如果是dismiss的话,不放在最前面开不到变小的效果。

最后动画如果是present就动画还原detail , 如果是dismiss 就把detail缩放,设置center 。

这时候你运行代码 , 并没有从图哪里扩大,而使从0,0点 。

因为这里还有一件事情要做,转换坐标。

在ViewController中

//Present的时候 使用自定义的动画
    func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
       
        transition.originFrame =
            selectedImage!.superview!.convertRect(selectedImage!.frame,
            toView: nil)
        transition.presenting = true
        selectedImage!.hidden = true

        return transition
    }

  //使用默认的动画
    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        selectedImage!.hidden = false
        transition.presenting = false
        return transition
    }

第一行是把选择的试图的坐标转换成父试图的坐标,然后transition.presenting 设置了状态,还隐藏了选择的图,这个是为了。dismiss的时候,下面没有图,dismis完成的时候这个图才能显示 。

这个也很简单,在动画完成的时候判断下,如果是dismiss就执行一段代码就行了,可以用代理我这里直接用了闭包

声明一个闭包在PopAnimation

var hideImage:(()->())?

然后动画完成

  if !self.presenting{
                    self.hidIt()
                }

   func hidIt(){
        hideImage?()
    }

最后viewc中dsmiss的时候加上那段就行了

 //使用默认的动画
    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        
        transition.hideImage={
            self.selectedImage!.hidden = false
        }
        transition.presenting = false
        return transition
    }

效果

最终效果

到这里 基本就实现了,主要是后面的算,其实自定义过渡没啥。就那几步。希望大家从中能学到一些东西,这个动画还能更完善就是dismiss之后的圆角。看官们自己搞搞吧

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

推荐阅读更多精彩内容