Swift超基礎(chǔ)實(shí)用技術(shù)(自定義轉(zhuǎn)場(chǎng)動(dòng)畫)

自定義轉(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鏈接:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末洒扎,一起剝皮案震驚了整個(gè)濱河市点额,隨后出現(xiàn)的幾起案子境氢,更是在濱河造成了極大的恐慌斥扛,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,826評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件舞终,死亡現(xiàn)場(chǎng)離奇詭異霸琴,居然都是意外死亡氮双,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門胀滚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來趟济,“玉大人,你說我怎么就攤上這事咽笼∏瓯啵” “怎么了?”我有些...
    開封第一講書人閱讀 164,234評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵剑刑,是天一觀的道長(zhǎng)媳纬。 經(jīng)常有香客問我,道長(zhǎng)施掏,這世上最難降的妖魔是什么钮惠? 我笑而不...
    開封第一講書人閱讀 58,562評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮七芭,結(jié)果婚禮上萌腿,老公的妹妹穿的比我還像新娘。我一直安慰自己抖苦,他們只是感情好毁菱,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著锌历,像睡著了一般贮庞。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上究西,一...
    開封第一講書人閱讀 51,482評(píng)論 1 302
  • 那天窗慎,我揣著相機(jī)與錄音,去河邊找鬼卤材。 笑死遮斥,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的扇丛。 我是一名探鬼主播术吗,決...
    沈念sama閱讀 40,271評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼帆精!你這毒婦竟也來了较屿?” 一聲冷哼從身側(cè)響起隧魄,我...
    開封第一講書人閱讀 39,166評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎隘蝎,沒想到半個(gè)月后购啄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,608評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡嘱么,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評(píng)論 3 336
  • 正文 我和宋清朗相戀三年狮含,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片曼振。...
    茶點(diǎn)故事閱讀 39,926評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡几迄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出拴测,到底是詐尸還是另有隱情乓旗,我是刑警寧澤,帶...
    沈念sama閱讀 35,644評(píng)論 5 346
  • 正文 年R本政府宣布集索,位于F島的核電站屿愚,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏务荆。R本人自食惡果不足惜妆距,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望函匕。 院中可真熱鬧娱据,春花似錦、人聲如沸盅惜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽抒寂。三九已至结啼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間屈芜,已是汗流浹背郊愧。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留井佑,地道東北人属铁。 一個(gè)月前我還...
    沈念sama閱讀 48,063評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像躬翁,于是被迫代替她去往敵國(guó)和親焦蘑。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容