首页投稿(暂停使用,暂停投稿)iOS DeveloperiOS 开发

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

2016-08-06  本文已影响311人  S_Lyu

自定义转场动画

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

参与动画执行的控制器

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

第一步:监听cell的点击

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

// 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来完成的,那么在制作动画前我们就一定要拿到三样东西:

///定义协议:负责获取跳转动画相关的参数
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链接:

上一篇 下一篇

猜你喜欢

热点阅读