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

作者: S_Lyu | 来源:发表于2016-08-06 23:37 被阅读311次

    自定义转场动画

    相对于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链接:

    相关文章

      网友评论

        本文标题:Swift超基础实用技术(自定义转场动画)

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