iOS中應(yīng)該知道的自定義各種Controller的轉(zhuǎn)場過渡動畫

前言

正如標(biāo)題所示,iOS開發(fā)中, 自定義轉(zhuǎn)場的過渡動畫確實是必須要了解的, 在iOS7之后實現(xiàn)也是很簡單的. 如果會使用它, 可以實現(xiàn)很多比較實用的功能. 比如:
  • 如果覺得系統(tǒng)的UIAlertController不能滿足需求, 那么你可以使用自定義轉(zhuǎn)場過渡動畫的方式來實現(xiàn)彈出自定義的控制器(同時實現(xiàn)比較實用的動畫效果).
  • 系統(tǒng)默認(rèn)的present是從下方彈出控制器, 可以通過自定義轉(zhuǎn)場過渡動畫的方式來自定義切換頁面的動畫
  • 利用手勢實現(xiàn)tabbarController滑動切換頁面
  • 利用手勢實現(xiàn)navigationController全屏返回的功能
  • ......
    本篇中首先介紹自定義present/dismiss的轉(zhuǎn)場動畫的方式 Demo地址swift3.0, [Demo地址swift2.3](jasnig:FullScreenPopNavigationController zeroj$ git push github master)

最終效果如下

present.gif
push.gif

一` 在iOS7以后Apple提供了很方便的接口來實現(xiàn)自定義轉(zhuǎn)場動畫, 使用起來很是簡單方便,在實現(xiàn)過程中會接觸到三個對象.

  • Delegate: 一個繼承自NSObject的代理, 并且需要遵守相關(guān)的協(xié)議, 用來指定動畫中需要的其他兩個對象(下面提到的兩個), 需要遵守相關(guān)的協(xié)議如下
    • (UIViewControllerTransitioningDelegate -- 自定義present/dismiss的時候)
    • UINavigationControllerDelegate --- 自定義navigationController轉(zhuǎn)場動畫的時候
    • UITabBarControllerDelegate --- 自定義tabbarController轉(zhuǎn)場動畫的時候
    • ......
  • UIViewControllerAnimatedTransitioning: 這個協(xié)議中提供了接口, 遵守這個協(xié)議的對象實現(xiàn)動畫的具體內(nèi)容
  • UIViewControllerInteractiveTransitioning: 這個協(xié)議中提供了手勢交互動畫的接口, 不過, 我們大多都是使用它的一個子類UIPercentDrivenInteractiveTransition來更簡單的實現(xiàn)手勢交互動畫

二` 了解UIViewControllerTransitioningDelegate

  • 這個代理需要提供兩種類型的對象給系統(tǒng)來實現(xiàn)自定義動畫, 如果沒有提供, 將會使用系統(tǒng)默認(rèn)的動畫效果
  • 第一種類型對象是遵守UIViewControllerAnimatedTransitioning協(xié)議的對象
// 自定義present彈出控制器時的動畫需要提供的遵守UIViewControllerAnimatedTransitioning對象
    optional public func animationController(forPresentedController presented: UIViewController, presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning?
// 自定義dismiss移除控制器時的動畫需要提供的遵守UIViewControllerAnimatedTransitioning對象
    @available(iOS 2.0, *)
    optional public func animationController(forDismissedController dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
  • 第二種類型對象是遵守UIViewControllerInteractiveTransitioning的對象
// 自定義交互動畫(手勢, 或者重力感應(yīng)...)需要提供的遵守UIViewControllerInteractiveTransitioning對象
    optional public func interactionController(forDismissal animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?

三` 了解UIViewControllerAnimatedTransitioning

  • 這個協(xié)議是上面提到的代理來獲取到具體的動畫操作的
  • 遵守這個協(xié)議的對象來只需要實現(xiàn)兩個必須的方法
// 通過這個方法獲取到動畫執(zhí)行的時間
    public func transitionDuration(_ transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
// 在這個方法中通過獲取到源控制器和目標(biāo)控制器等來執(zhí)行動畫
    // This method can only  be a nop if the transition is interactive and not a percentDriven interactive transition.
    public func animateTransition(_ transitionContext: UIViewControllerContextTransitioning)

四` 了解UIPercentDrivenInteractiveTransition

  • UIPercentDrivenInteractiveTransition 是實現(xiàn)了
    UIViewControllerInteractiveTransitioning這個協(xié)議的
    我們使用UIPercentDrivenInteractiveTransition可以簡單的
    通過調(diào)用提供的幾個函數(shù)來執(zhí)行具體的動畫
    (會調(diào)用UIViewControllerAnimatedTransitioning里面實現(xiàn)的動畫)
  • 一般可以通過繼承(也可不繼承)它來實現(xiàn)可交互動畫
  • 在子類中通過添加手勢(或者其他方式)到相應(yīng)的view上面, 在手勢的響應(yīng)方法
    中根據(jù)不同的手勢狀態(tài)來進(jìn)行不同的交互動畫的操作, 一般使用到如下三個函數(shù)
// 更新動畫進(jìn)度
   public func update(_ percentComplete: CGFloat)
// 取消交互動畫
    public func cancel()
// 完成交互動畫
    public func finish()

五` 了解UIViewControllerContextTransitioning

在UIViewControllerAnimatedTransitioning協(xié)議的
實現(xiàn)具體動畫的函數(shù)中

func animateTransition(_ transitionContext:UIViewControllerContextTransitioning)

我們會接觸到UIViewControllerContextTransitioning
這個接口用來提供切換上下文給開發(fā)者使用,包含了從哪個VC到哪個VC等
各類信息, 我們可以很方便的獲取到源控制器和目標(biāo)控制器...很多我們需要的屬性

* 使用viewControllerForKey: 獲取到源控制器和目標(biāo)控制器
* 使用containerView獲取到當(dāng)前的containerView, 將要執(zhí)行動畫的view都在這個containerView上進(jìn)行
* 使用viewForKey: 獲取到將要添加或者移除的view(一般是控制器的view)
* 使用finalFrameForViewController:獲取到將要添加或者移除的view的最終frame
* 注意 'from' -> 指的的當(dāng)前正在屏幕上顯示的控制器(present和dismiss的時候是不一樣的)

六` 自定義present/dismiss動畫的系統(tǒng)調(diào)用過程

  1. 首先設(shè)置controller的代理transitioningDelegate為我們自定義的, 如果我們的代理里面沒有提供上面所需要的對象, 那么將會使用系統(tǒng)默認(rèn)的
prenting動畫執(zhí)行過程
  • UIKit首先會調(diào)用代理的
    animationControllerForPresentedController:presentingController:sourceController:方法取得自定義的動畫對象
  • UIKit接著調(diào)用代理的 interactionControllerForPresentation: 方法看是否支持交互性動畫, 如果返回nil表示不支持
  • UIKit接著調(diào)用代理的 transitionDuration: 方法獲取動畫執(zhí)行的時間
  • 如果是不可交互的動畫UIKit會調(diào)用代理的animateTransition:方法來執(zhí)行真正的動畫,
    如果是可交互的動畫, UIKit會調(diào)用代理的startInteractiveTransition:方法開始動畫
  • 接著是執(zhí)行動畫的操作, 并且等待代理調(diào)用completeTransition:結(jié)束動畫(所以我們一定需要在動畫執(zhí)行完畢后調(diào)用這個方法, 告訴系統(tǒng)我們的動畫執(zhí)行完畢或者中途取消了)

dismiss動畫執(zhí)行過程和上面只有第一步和第二步調(diào)用的代理方法不一樣
例如第一步調(diào)用(animationControllerForDismissedController:), 其他是相同的過程

七` 下面以自定義present/dismiss動畫過程示例上面提到的各種用法(注意: 使用的swift3.0 xcode8, 如果是使用oc或者swift低版本的朋友請對應(yīng)轉(zhuǎn)換相應(yīng)的語法)

  • 首先新建一個CustomAnimator繼承自NSObject, 并且遵守UIViewControllerAnimatedTransitioning協(xié)議, 來處理動畫的實現(xiàn)
class CustomAnimator:NSObject, UIViewControllerAnimatedTransitioning {
  • 然后實現(xiàn)這個協(xié)議中必須的兩個方法來實現(xiàn)具體的動畫
class CustomAnimator:NSObject, UIViewControllerAnimatedTransitioning {
    
    let duration = 0.35
// 返回動畫時間
    func transitionDuration(_ transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return duration
    }
 // 處理具體動畫, 通過transitionContext可以獲取到很多我們需要的東西
    func animateTransition(_ transitionContext: UIViewControllerContextTransitioning) {
        // fromVc 總是獲取到正在顯示在屏幕上的Controller
        let fromVc = transitionContext.viewController(forKey: UITransitionContextFromViewControllerKey)!
        // toVc 總是獲取到將要顯示的controller
        let toVc = transitionContext.viewController(forKey: UITransitionContextToViewControllerKey)!
        let containView = transitionContext.containerView()
        
        let toView: UIView
        let fromView: UIView
        
        if transitionContext.responds(to:NSSelectorFromString("viewForKey:")) {
            // 通過這種方法獲取到view不一定是對應(yīng)controller.view
            toView = transitionContext.view(forKey: UITransitionContextToViewKey)!
            fromView = transitionContext.view(forKey: UITransitionContextFromViewKey)!
        } else { // Apple文檔中提到不要直接使用這種方法來獲取fromView和toView
            toView = toVc.view
            fromView = fromVc.view
        }
        //  添加toview到最上面(fromView是當(dāng)前顯示在屏幕上的view不用添加)
        containView.addSubview(toView)
        
        // 最終顯示在屏幕上的controller的frame
        let visibleFrame = transitionContext.initialFrame(for: fromVc)
        // 隱藏在右邊的controller的frame
        let rightHiddenFrame = CGRect(origin: CGPoint(x: visibleFrame.width, y: visibleFrame.origin.y) , size: visibleFrame.size)
        // 隱藏在左邊的controller的frame
        let leftHiddenFrame = CGRect(origin: CGPoint(x: -visibleFrame.width, y: visibleFrame.origin.y) , size: visibleFrame.size)

        // toVc.presentingViewController --> 彈出toVc的controller
        // 所以如果是present的時候  == fromVc
        // 或者可以使用 fromVc.presentedViewController == toVc
        
        let isPresenting = toVc.presentingViewController == fromVc
        
        if isPresenting {// present Vc左移
            toView.frame = rightHiddenFrame
            fromView.frame = visibleFrame
        } else {// dismiss Vc右移
            fromView.frame = visibleFrame
            toView.frame = leftHiddenFrame
            // 有時需要將toView添加到fromView的下面便于執(zhí)行動畫
//            containView.insertSubview(toView, belowSubview: fromView)
        }
        UIView.animate(withDuration: duration, delay: 0.0, options: [.curveLinear], animations: {
            if isPresenting {
                toView.frame = visibleFrame
                fromView.frame = leftHiddenFrame
            } else {
                fromView.frame = rightHiddenFrame
                toView.frame = visibleFrame
            }
        }) { (_) in
            let cancelled = transitionContext.transitionWasCancelled()
            if cancelled {
                // 如果中途取消了就移除toView(可交互的時候會發(fā)生)
                toView.removeFromSuperview()
            }
            // 通知系統(tǒng)動畫是否完成或者取消了
            transitionContext.completeTransition(!cancelled)
        }
    }
}
  • 接著新建一個CustomDelegate繼承自NSObject,并且遵守
    UIViewControllerTransitioningDelegate協(xié)議, 來實現(xiàn)動畫的代理的工作
class CustomDelegate: NSObject, UIViewControllerTransitioningDelegate
  • 接著實現(xiàn)需要自定義的相應(yīng)的方法, 并且返回所需的執(zhí)行對象
class CustomDelegate: NSObject, UIViewControllerTransitioningDelegate {
    private lazy var customAnimator = CustomAnimator()
    // 提供present的時候使用到的動畫執(zhí)行對象
    func animationController(forPresentedController presented: UIViewController, presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return customAnimator
    }
    // 提供dismiss的時候使用到的動畫執(zhí)行對象
    func animationController(forDismissedController dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {  
        return customAnimator
    }
}
  • 到這里為止就已經(jīng)實現(xiàn)了自定義的不可交互的轉(zhuǎn)場動畫, 可以使用了, 效果和我們圖片示例的一樣

class Test1Controller: UIViewController {
  // 動畫代理
    let deletage = CustomDelegate()
    
    @IBAction func present(_ sender: UIButton) {
        let testVc = TestController()
        testVc.view.backgroundColor = UIColor.red()
        testVc.modalPresentationStyle = .fullScreen
        // 因為transitioningDelegate是weak 所以這里不能使用局部變量 CustomDelegate()
//        testVc.transitioningDelegate = CustomDelegate()
      // 設(shè)置代理為我們自定義的
        testVc.transitioningDelegate = deletage
// 彈出控制器
        present(testVc, animated: true, completion: nil)

    }
  • 然后我們添加可交互的對象, 首先新建 Interactive:繼承自
    UIPercentDrivenInteractiveTransition
class Interactive: UIPercentDrivenInteractiveTransition
  • 接著添加手勢, 并且在手勢處理過程中根據(jù)不同的手勢狀態(tài)執(zhí)行不同的操作
class Interactive: UIPercentDrivenInteractiveTransition {
// pan手勢
    lazy var panGesture: UIPanGestureRecognizer = UIPanGestureRecognizer(target: self, action:  #selector(self.handlePan(gesture:)))
// 用于添加手勢
    var containerView: UIView!
// 將要被dismiss的控制器, 在動畫的delegate中傳入
    var dismissedVc: UIViewController! = nil {
        didSet {
            containerView = dismissedVc.view
            containerView.addGestureRecognizer(panGesture)
        }
    }
// 是否執(zhí)行交互動畫
    var isInteracting = false
    
    override init() {
        super.init()
        
    }
    // 處理手勢
    func handlePan(gesture: UIPanGestureRecognizer) {
        //動畫是否完成或者取消
        func finishOrCancel() {
            let translation = gesture.translation(in: containerView)
            let percent = translation.x / containerView.bounds.width
            let velocityX = gesture.velocity(in: containerView).x
            let isFinished: Bool
            if velocityX <= 0 {
                isFinished = false
            } else if velocityX > 100 {
                isFinished = true
            } else if percent > 0.3 {
                isFinished = true
            } else {
                isFinished = false
            }
            
            isFinished ? finish() : cancel()
        }
        
        switch gesture.state {

            case .began:
// 手勢開始, 開啟交互動畫, 并且dismiss(需要設(shè)置animated: true)
                isInteracting = true
                // dimiss
                dismissedVc.dismiss(animated: true, completion: nil)
            case .changed:
// 手勢改變狀態(tài), 計算動畫的進(jìn)度
                if isInteracting {// 開始執(zhí)行交互動畫的時候才設(shè)置為非nil
                    let translation = gesture.translation(in: containerView)
                    var percent = translation.x / containerView.bounds.width
                    if percent < 0 {
                        percent = 0
                    }
// 更新動畫
                    update(percent)
                    
                }
            case .cancelled:
                if isInteracting {
                    finishOrCancel()
                    isInteracting = false
                    
                }
            case .ended:
                if isInteracting {
                    finishOrCancel()
                    isInteracting = false
                    
                }
            default:
                break
        }
    }
}
  • 接著在CustomDelegate里面增加實現(xiàn)可交互動畫的執(zhí)行對象和接口
// 注意在present接口里面設(shè)置了
//  interactive.dismissedVc = presented
    private lazy var interactive = Interactive()

    // 提供dismiss的時候使用到的可交互動畫執(zhí)行對象
    func interactionController(forDismissal animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        // 因為執(zhí)行自定義動畫會先調(diào)用這個方法, 如果返回不為nil, 那么將不會執(zhí)行非交互的動畫!!
        // 所以isInteracting只有在手勢開始的時候才被設(shè)置為true
        // 返回nil便于不是可交互的時候就直接執(zhí)行不可交互的動畫
        return interactive.isInteracting ? interactive : nil
    }

就是這樣就實現(xiàn)了利用手勢滑動返回的可交互動畫, 現(xiàn)在運行, 將會看到圖片的示例效果, 還是很簡單?!!!!

這里以自定義present/dismiss為例詳細(xì)的介紹了自定義轉(zhuǎn)場動畫的使用, 那么到現(xiàn)在, 你是可以很自由的去實現(xiàn)各種需要的自定義動畫(navigationController, tabBarController...), 并且增加各種交互動畫(滑動, 捏合, 甚至設(shè)備搖晃...), 希望你會很愉快的使用它 Demo地址swift3.0, [Demo地址swift2.3](jasnig:FullScreenPopNavigationController zeroj$ git push github master) 歡迎關(guān)注, 歡迎star

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,991評論 2 374

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