自定義轉(zhuǎn)場(chǎng)動(dòng)畫
相對(duì)于OC來說,在Swift中編寫iOS的轉(zhuǎn)場(chǎng)動(dòng)畫要顯得更為簡(jiǎn)單
- 我們?cè)谶@里模擬一個(gè)場(chǎng)景:
"collectionViewController通過點(diǎn)擊一個(gè)cell來modal出來一個(gè)查看大圖的控制器,查看大圖的控制器通過觸摸屏幕來將自己dismiss掉"
通過這個(gè)場(chǎng)景來看一下,在Swift中實(shí)現(xiàn)轉(zhuǎn)場(chǎng)動(dòng)畫的基本思路
參與動(dòng)畫執(zhí)行的控制器
因?yàn)楣P者比較懶,這里就僅把demo中參與執(zhí)行動(dòng)畫的類拿出來,依次做個(gè)介紹好了:
-
LYUMainCVC:繼承自UICollectionViewController負(fù)責(zé)顯示縮略圖片:
LYUMainCVC -
LYUBrowserVC:繼承自UIViewController,內(nèi)部懶加載一個(gè)UICollectionView,負(fù)責(zé)顯示大圖片并可以實(shí)現(xiàn)大圖片的左右切換:
LYUBrowserVC - LYUTransitionAnimater:繼承自NSObject,負(fù)責(zé)執(zhí)行動(dòng)畫(將這個(gè)類單獨(dú)抽取出來只是為了減輕LYUMainCVC的重量級(jí)),我們這次利用LYUTransitionAnimater來實(shí)現(xiàn)的目標(biāo)轉(zhuǎn)場(chǎng)動(dòng)畫效果如下:
轉(zhuǎn)場(chǎng)動(dòng)畫效果
第一步:監(jiān)聽cell的點(diǎn)擊
"代碼位置:LYUMainCVC"
在collectionView的代理方法中來監(jiān)聽cell點(diǎn)擊,這里做了下面三件事
- 創(chuàng)建大圖控制器(browserVC)
- 大圖控制器modal動(dòng)畫由animater來處理
- 彈出大圖控制器(browserVC)
// MARK:- collectionViewDelegate
extension LYUMainCVC{ //當(dāng)前的代碼在LYUMainCVC中
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
//創(chuàng)建一個(gè)大圖控制器
let browserVC = LYUBrowserVC()
//給大圖控制器傳值indexPath,這是為了告訴大圖控制器應(yīng)該顯示我當(dāng)前點(diǎn)擊的這張圖片
browserVC.indexPath = indexPath
//給大圖控制器傳值模型數(shù)組,數(shù)組里保存的網(wǎng)絡(luò)獲取的圖片url
browserVC.items = items
//設(shè)置彈出控制器的風(fēng)格,默認(rèn)情況下,modal成功后,modal出來的控制器以外的控件都會(huì)被移除掉,當(dāng)我們將其修改為.Custom后browserVC背后的控件不會(huì)被移除
browserVC.modalPresentationStyle = .Custom
//設(shè)置執(zhí)行動(dòng)畫的代理,animater是一個(gè)LYUTransitionAnimater類型的懶加載的屬性,由他來負(fù)責(zé)轉(zhuǎn)場(chǎng)動(dòng)畫的實(shí)現(xiàn),后面有詳細(xì)說明
browserVC.transitioningDelegate = animater
//下面這兩個(gè)代理運(yùn)用到了一些面向接口開發(fā)的思路,目的是拿到執(zhí)行動(dòng)畫的一些數(shù)據(jù),后面有詳細(xì)說明
animater.presentDelegate = self //自己作為彈出動(dòng)畫的代理
animater.dismissDelegate = browserVC //大圖控制器作為消失動(dòng)畫的代理
//indexPath用于計(jì)算動(dòng)畫初始位置等參數(shù),后面有詳細(xì)說明
animater.indexPath = indexPath
self.presentViewController(browserVC, animated: true, completion: nil)
}
}
第二步:轉(zhuǎn)場(chǎng)動(dòng)畫的思路框架
"代碼地點(diǎn):LYUTransitionAnimater"
上文中animater既然成為了轉(zhuǎn)場(chǎng)的代理,那么就一定更要遵守它的代理協(xié)議(UIViewControllerTransitioningDelegate),那么這里我們先將所需要的代理方法統(tǒng)統(tǒng)實(shí)現(xiàn)出來
- 首先在當(dāng)前類中創(chuàng)建下面這個(gè)屬性:
//控制present或dismiss
var isPresenting = true
- 其次實(shí)現(xiàn)必要的代理方法
// MARK:- transtionDelegate
extension LYUTransitionAnimater : UIViewControllerTransitioningDelegate{
//這里的兩個(gè)代理分別告訴系統(tǒng)誰來負(fù)責(zé)彈出/消失動(dòng)畫的制作
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
isPresenting = true
return self
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
isPresenting = false
return self
}
}
//上面已經(jīng)寫到讓self來負(fù)責(zé)動(dòng)畫制作,那么self就一定要遵守執(zhí)行動(dòng)畫的協(xié)議,如下
// MARK:- animatedTransitioning
extension LYUTransitionAnimater : UIViewControllerAnimatedTransitioning{
//控制動(dòng)畫時(shí)間
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 1.5
}
//控制動(dòng)畫效果
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
if isPresenting {
//彈出動(dòng)畫
}
else {
//消失動(dòng)畫
}
}
}
第三步:制作彈出動(dòng)畫
"代碼地點(diǎn):LYUTransitionAnimater"
首先要明確,示例程序中的動(dòng)畫是通過更改一個(gè)圖片的frame來完成的,那么在制作動(dòng)畫前我們就一定要拿到三樣?xùn)|西:
- 執(zhí)行動(dòng)畫的imageView
- imageView的初始frame
- imageView的終止frame
然而,這三樣?xùn)|西似乎都是collectionView中才能獲取到的,于是這里就用到了一點(diǎn)"面向接口開發(fā)"的思路:我們創(chuàng)建一個(gè)協(xié)議來獲取我們需要的數(shù)據(jù),并且反過來讓collectionView成為我們的代理
///定義協(xié)議:負(fù)責(zé)獲取跳轉(zhuǎn)動(dòng)畫相關(guān)的參數(shù)
protocol LYUPresentAnimationDelegate {
func getImageView(indexPath : NSIndexPath) -> UIImageView
func getStartRect(indexPath : NSIndexPath) -> CGRect
func getEndRect(indexPath : NSIndexPath) -> CGRect
}
這個(gè)時(shí)候我們需要在當(dāng)前類中添加兩個(gè)屬性
//present代理
var presentDelegate : LYUPresentAnimationDelegate?
//有外界傳值,負(fù)責(zé)確定跳轉(zhuǎn)動(dòng)畫的初始位置
var indexPath : NSIndexPath?
這樣一來,只要有代理人(我們先不看代理方法的實(shí)現(xiàn))幫我們拿到制作動(dòng)畫所需要的全部參數(shù),那么制作動(dòng)畫簡(jiǎn)直是小菜一碟的,對(duì)吧?現(xiàn)在就將上面代碼塊中的"彈出動(dòng)畫"的位置換成下邊這段代碼吧
//拿到即將跳轉(zhuǎn)的view
let presentView = transitionContext.viewForKey(UITransitionContextToViewKey)!
//防呆
guard let presentDelegate = presentDelegate , indexPath = indexPath else {
return
}
//拿到用于執(zhí)行動(dòng)畫的imageView
let animationImageView = presentDelegate.getImageView(indexPath)
//動(dòng)畫開始時(shí),讓用戶看不到collectionView中的內(nèi)容
transitionContext.containerView()?.backgroundColor = UIColor.blackColor()
//獲取imageView的初始位置,以此來做動(dòng)畫
animationImageView.frame = presentDelegate.getStartRect(indexPath)
transitionContext.containerView()?.addSubview(animationImageView)
//獲取動(dòng)畫時(shí)間
let duration = transitionDuration(transitionContext)
UIView.animateWithDuration(duration, animations: {
animationImageView.frame = presentDelegate.getEndRect(indexPath)
}, completion: { (_) in
transitionContext.containerView()?.backgroundColor = UIColor.clearColor() //重新透明化
animationImageView.removeFromSuperview() //移除制作動(dòng)畫的animationImageView
transitionContext.containerView()?.addSubview(presentView)
transitionContext.completeTransition(true) //完成動(dòng)畫
})
外部是怎么獲取到那三個(gè)關(guān)鍵的參數(shù)的?如下:
"代碼地點(diǎn):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
//負(fù)責(zé)執(zhí)行動(dòng)畫的imageView中的圖片與cell當(dāng)前顯示的圖片相同
imageView.image = cell.imageView.image
return imageView
}
func getStartRect(indexPath: NSIndexPath) -> CGRect {
//當(dāng)indexPath不在當(dāng)前顯示cell范圍內(nèi)時(shí),return零點(diǎn)
guard let cell = collectionView?.cellForItemAtIndexPath(indexPath) else {
return CGRectZero
}
//將cell的坐標(biāo)轉(zhuǎn)換為這個(gè)cell在當(dāng)前窗口中所處的坐標(biāo)點(diǎn)
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
}
//這里的計(jì)算方法與查看大圖的計(jì)算方法相同,目的是讓兩者最終尺寸相同,實(shí)際開發(fā)中應(yīng)將其抽取為一個(gè)全局函數(shù)作為工具
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)
}
}
第四步:制作消失動(dòng)畫
"代碼地點(diǎn):LYUTransitionAnimater"
消失動(dòng)畫依然是一張圖片的frame動(dòng)畫,但拿到這個(gè)圖片之前要先解決一個(gè)問題:這張圖片的indexPath是什么?
顯然經(jīng)過用戶在大圖控制器中的多次拖動(dòng)后,當(dāng)前cell的indexPath就只有大圖控制器中的collectionView才知道了,于是我們這回又要讓大圖控制器成為消失動(dòng)畫的代理嘍
///負(fù)責(zé)消失動(dòng)畫相關(guān)的參數(shù)
protocol LYUDismissAnimationDelegate {
func getIndexPath() -> NSIndexPath
func getImageView() -> UIImageView
}
在當(dāng)前類中添加屬性代理屬性:
//dismiss代理
var dismissDelegate : LYUDismissAnimationDelegate?
這回好了,代理可以拿到我們需要的參數(shù)(我們依舊最后來看代理方法的實(shí)現(xiàn)),那么let's制作動(dòng)畫吧:
//拿到即將消失的view,并直接移除
let dismissView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
dismissView.removeFromSuperview()
guard let dismissDelegate = dismissDelegate else {
return
}
//由代理獲取imageView和indexPath
let imageView = dismissDelegate.getImageView() //注意:這里獲取的imageView是帶有默認(rèn)尺寸的
let indexpath = dismissDelegate.getIndexPath()
//獲取動(dòng)畫結(jié)束時(shí)imageView的最終尺寸
let endRect = presentDelegate?.getStartRect(indexpath)
//開始動(dòng)畫
transitionContext.containerView()?.addSubview(imageView)
let duration = transitionDuration(transitionContext)
UIView.animateWithDuration(duration, animations: {
//判斷indexPath指向的cell在LYUMainCVC中是否越界,根據(jù)不同情況執(zhí)行不同動(dòng)畫
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)
})
那么最后就剩下代理方法的實(shí)現(xiàn)了,勤勞的代理是怎么拿到indexPath和imageView的呢?如下:
"代碼地點(diǎn):LYUBrowserVC"
// MARK:- dismissAnimationDelegate
extension LYUBrowserVC : LYUDismissAnimationDelegate{
func getIndexPath() -> NSIndexPath {
//獲取當(dāng)前正在顯示的cell
let cell = collectionView.visibleCells().first as! LYUBigImageCell
//拿到這個(gè)cell的indexPath,這個(gè)demo中用到的兩個(gè)collectionView的任何一個(gè)indexPath所指向的模型都是相同的
let indexPath = collectionView.indexPathForCell(cell)
return indexPath!
}
func getImageView() -> UIImageView {
//獲取當(dāng)前的cell,利用當(dāng)前cell的圖片來創(chuàng)建一個(gè)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 ^ ^