自定义转场动画
相对于OC来说,在Swift中编写iOS的转场动画要显得更为简单
- 我们在这里模拟一个场景:
"collectionViewController通过点击一个cell来modal出来一个查看大图的控制器,查看大图的控制器通过触摸屏幕来将自己dismiss掉"
通过这个场景来看一下,在Swift中实现转场动画的基本思路
参与动画执行的控制器
因为笔者比较懒,这里就仅把demo中参与执行动画的类拿出来,依次做个介绍好了:
-
LYUMainCVC:继承自UICollectionViewController负责显示缩略图片:
-
LYUBrowserVC:继承自UIViewController,内部懒加载一个UICollectionView,负责显示大图片并可以实现大图片的左右切换:
- 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链接:
- DEMO链接:Swift_Transitioning ^ ^