Swift超基础实用技术(自定义转场动画)

自定义转场动画

相对于OC来说,在Swift中编写iOS的转场动画要显得更为简单

  • 我们在这里模拟一个场景:
    "collectionViewController通过点击一个cell来modal出来一个查看大图的控制器,查看大图的控制器通过触摸屏幕来将自己dismiss掉"
    通过这个场景来看一下,在Swift中实现转场动画的基本思路

参与动画执行的控制器

因为笔者比较懒,这里就仅把demo中参与执行动画的类拿出来,依次做个介绍好了:

  • LYUMainCVC:继承自UICollectionViewController负责显示缩略图片:


    LYUMainCVC
  • LYUBrowserVC:继承自UIViewController,内部懒加载一个UICollectionView,负责显示大图片并可以实现大图片的左右切换:


    LYUBrowserVC
  • LYUTransitionAnimater:继承自NSObject,负责执行动画(将这个类单独抽取出来只是为了减轻LYUMainCVC的重量级),我们这次利用LYUTransitionAnimater来实现的目标转场动画效果如下:
    转场动画效果

第一步:监听cell的点击

"代码位置:LYUMainCVC"
在collectionView的代理方法中来监听cell点击,这里做了下面三件事

  • 创建大图控制器(browserVC)
  • 大图控制器modal动画由animater来处理
  • 弹出大图控制器(browserVC)
// MARK:- collectionViewDelegate 
extension LYUMainCVC{  //当前的代码在LYUMainCVC中
    override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {

        //创建一个大图控制器
        let browserVC = LYUBrowserVC()

        //给大图控制器传值indexPath,这是为了告诉大图控制器应该显示我当前点击的这张图片
        browserVC.indexPath = indexPath

        //给大图控制器传值模型数组,数组里保存的网络获取的图片url
        browserVC.items = items

        //设置弹出控制器的风格,默认情况下,modal成功后,modal出来的控制器以外的控件都会被移除掉,当我们将其修改为.Custom后browserVC背后的控件不会被移除
        browserVC.modalPresentationStyle = .Custom

        //设置执行动画的代理,animater是一个LYUTransitionAnimater类型的懒加载的属性,由他来负责转场动画的实现,后面有详细说明
        browserVC.transitioningDelegate = animater

        //下面这两个代理运用到了一些面向接口开发的思路,目的是拿到执行动画的一些数据,后面有详细说明
        animater.presentDelegate = self  //自己作为弹出动画的代理
        animater.dismissDelegate = browserVC  //大图控制器作为消失动画的代理

        //indexPath用于计算动画初始位置等参数,后面有详细说明
        animater.indexPath = indexPath

        self.presentViewController(browserVC, animated: true, completion: nil)
    }
}

第二步:转场动画的思路框架

"代码地点:LYUTransitionAnimater"
上文中animater既然成为了转场的代理,那么就一定更要遵守它的代理协议(UIViewControllerTransitioningDelegate),那么这里我们先将所需要的代理方法统统实现出来

  • 首先在当前类中创建下面这个属性:
//控制present或dismiss
    var isPresenting = true
  • 其次实现必要的代理方法
// MARK:- transtionDelegate
extension LYUTransitionAnimater : UIViewControllerTransitioningDelegate{
//这里的两个代理分别告诉系统谁来负责弹出/消失动画的制作
    func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        isPresenting = true
        return self
    }
    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        isPresenting = false
        return self
    }
}
//上面已经写到让self来负责动画制作,那么self就一定要遵守执行动画的协议,如下
// MARK:- animatedTransitioning
extension LYUTransitionAnimater : UIViewControllerAnimatedTransitioning{
    //控制动画时间
    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
        return 1.5
    }
    //控制动画效果
    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        if isPresenting { 
        //弹出动画
        }
        else {
        //消失动画
        }
    }
}

第三步:制作弹出动画

"代码地点:LYUTransitionAnimater"
首先要明确,示例程序中的动画是通过更改一个图片的frame来完成的,那么在制作动画前我们就一定要拿到三样东西:

  • 执行动画的imageView
  • imageView的初始frame
  • imageView的终止frame
    然而,这三样东西似乎都是collectionView中才能获取到的,于是这里就用到了一点"面向接口开发"的思路:我们创建一个协议来获取我们需要的数据,并且反过来让collectionView成为我们的代理
///定义协议:负责获取跳转动画相关的参数
protocol LYUPresentAnimationDelegate {
    func getImageView(indexPath : NSIndexPath) -> UIImageView
    func getStartRect(indexPath : NSIndexPath) -> CGRect
    func getEndRect(indexPath : NSIndexPath) -> CGRect
}

这个时候我们需要在当前类中添加两个属性

    //present代理
    var presentDelegate : LYUPresentAnimationDelegate?
    //有外界传值,负责确定跳转动画的初始位置
    var indexPath : NSIndexPath?

这样一来,只要有代理人(我们先不看代理方法的实现)帮我们拿到制作动画所需要的全部参数,那么制作动画简直是小菜一碟的,对吧?现在就将上面代码块中的"弹出动画"的位置换成下边这段代码吧

            //拿到即将跳转的view
            let presentView = transitionContext.viewForKey(UITransitionContextToViewKey)!
            //防呆
            guard let presentDelegate = presentDelegate , indexPath = indexPath else {
                return
            }
            //拿到用于执行动画的imageView
            let animationImageView = presentDelegate.getImageView(indexPath)
            //动画开始时,让用户看不到collectionView中的内容
            transitionContext.containerView()?.backgroundColor = UIColor.blackColor()
            //获取imageView的初始位置,以此来做动画
            animationImageView.frame = presentDelegate.getStartRect(indexPath)
            transitionContext.containerView()?.addSubview(animationImageView)
            //获取动画时间
            let duration = transitionDuration(transitionContext)
            UIView.animateWithDuration(duration, animations: { 
                animationImageView.frame = presentDelegate.getEndRect(indexPath)
                }, completion: { (_) in
                    transitionContext.containerView()?.backgroundColor = UIColor.clearColor()  //重新透明化
                    animationImageView.removeFromSuperview()  //移除制作动画的animationImageView
                    transitionContext.containerView()?.addSubview(presentView)
                    transitionContext.completeTransition(true)  //完成动画
            })

外部是怎么获取到那三个关键的参数的?如下:
"代码地点:LYUMainCVC"

// MARK:- presentAnimationDelegate
extension LYUMainCVC : LYUPresentAnimationDelegate {
    func getImageView(indexPath: NSIndexPath) -> UIImageView {
        let imageView = UIImageView()
        imageView.clipsToBounds = true
        imageView.contentMode = .ScaleAspectFill
        let cell = collectionView?.cellForItemAtIndexPath(indexPath) as! LYUSmallImageCell
        //负责执行动画的imageView中的图片与cell当前显示的图片相同
        imageView.image = cell.imageView.image  
        return imageView
    }
    func getStartRect(indexPath: NSIndexPath) -> CGRect {
        //当indexPath不在当前显示cell范围内时,return零点
        guard let cell = collectionView?.cellForItemAtIndexPath(indexPath) else {
            return CGRectZero
        }
        //将cell的坐标转换为这个cell在当前窗口中所处的坐标点
        let startRect = collectionView?.convertRect(cell.frame, toCoordinateSpace: UIApplication.sharedApplication().keyWindow!)
        return startRect!
    }
    func getEndRect(indexPath: NSIndexPath) -> CGRect {
        guard let cell = collectionView?.cellForItemAtIndexPath(indexPath) as? LYUSmallImageCell else {
            return CGRectZero
        }
        //这里的计算方法与查看大图的计算方法相同,目的是让两者最终尺寸相同,实际开发中应将其抽取为一个全局函数作为工具
        let image = cell.imageView.image!
        let w = UIScreen.mainScreen().bounds.width
        let h = w * image.size.height / image.size.width
        let x : CGFloat = 0.0
        let y : CGFloat = (UIScreen.mainScreen().bounds.height - h ) * 0.5
        return CGRectMake(x, y, w, h)
    }
}

第四步:制作消失动画

"代码地点:LYUTransitionAnimater"
消失动画依然是一张图片的frame动画,但拿到这个图片之前要先解决一个问题:这张图片的indexPath是什么?
显然经过用户在大图控制器中的多次拖动后,当前cell的indexPath就只有大图控制器中的collectionView才知道了,于是我们这回又要让大图控制器成为消失动画的代理喽

///负责消失动画相关的参数
protocol LYUDismissAnimationDelegate {
    func getIndexPath() -> NSIndexPath
    func getImageView() -> UIImageView
}

在当前类中添加属性代理属性:

    //dismiss代理
    var dismissDelegate : LYUDismissAnimationDelegate?

这回好了,代理可以拿到我们需要的参数(我们依旧最后来看代理方法的实现),那么let's制作动画吧:

            //拿到即将消失的view,并直接移除
            let dismissView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
            dismissView.removeFromSuperview()
            guard let dismissDelegate = dismissDelegate else {
                return
            }
            //由代理获取imageView和indexPath
            let imageView = dismissDelegate.getImageView()  //注意:这里获取的imageView是带有默认尺寸的
            let indexpath = dismissDelegate.getIndexPath()
            //获取动画结束时imageView的最终尺寸
            let endRect = presentDelegate?.getStartRect(indexpath)
            //开始动画
            transitionContext.containerView()?.addSubview(imageView)
            let duration = transitionDuration(transitionContext)
            UIView.animateWithDuration(duration, animations: {
                //判断indexPath指向的cell在LYUMainCVC中是否越界,根据不同情况执行不同动画
                if endRect == CGRectZero {
                    imageView.frame = CGRectMake(UIScreen.mainScreen().bounds.width * 0.5, UIScreen.mainScreen().bounds.height, 0, 0)
                }
                else {
                    imageView.frame = endRect!
                }
                }, completion: { (_) in
                    imageView.removeFromSuperview()
                    transitionContext.completeTransition(true)
            })

那么最后就剩下代理方法的实现了,勤劳的代理是怎么拿到indexPath和imageView的呢?如下:
"代码地点:LYUBrowserVC"

// MARK:- dismissAnimationDelegate
extension LYUBrowserVC : LYUDismissAnimationDelegate{
    func getIndexPath() -> NSIndexPath {
        //获取当前正在显示的cell
        let cell = collectionView.visibleCells().first as! LYUBigImageCell
        //拿到这个cell的indexPath,这个demo中用到的两个collectionView的任何一个indexPath所指向的模型都是相同的
        let indexPath = collectionView.indexPathForCell(cell)
        return indexPath!
    }
    func getImageView() -> UIImageView {
        //获取当前的cell,利用当前cell的图片来创建一个imageView
        let cell = collectionView.visibleCells().first as! LYUBigImageCell
        let imageView = UIImageView()
        imageView.image = cell.imageView.image
        imageView.frame = cell.imageView.frame
        imageView.clipsToBounds = true
        imageView.contentMode = .ScaleAspectFill
        return imageView
    }
}

最后附上DEMO链接:

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

推荐阅读更多精彩内容