自定義控制器轉場動畫及實現下拉菜單的小Demo | AppCoda翻譯系列

本文翻譯總結自AppCoda以下兩篇文章:

iOS 7開始,蘋果為開發者提供了自定義控制器轉場動畫相關的API,而實現該功能需要以下三個步驟:

  • 創建一個類作為動畫管理器,該類需繼承自NSObject并遵守UIViewControllerAnimatedTransitioning協議,我們在這個類中編寫我們的動畫執行代碼。
  • 為目標控制器指定轉場動畫代理,既可以使用上一步創建的動畫管理器對象,也可以指定來源控制器作為這個代理。
  • 實現代理協議中的相應方法,在方法中返回第一步創建的動畫管理器對象。

準備工作

下載示例程序,地址在這里。(譯注:原文地址需要FQ訪問,本人已轉存到GitHub上,點擊這里。)

示例程序如下圖所示,點擊導航欄上的Action按鈕會modal出一個目標控制器,點擊Dismiss按鈕會返回來源控制器,只不過現在使用的是系統默認的modal動畫,接下來我們就來實現自定義轉場動畫。

創建動畫管理器

創建一個類名稱為CustomPresentAnimationController,繼承自NSObject并遵守UIViewControllerAnimatedTransitioning協議。這個協議有兩個必須實現的方法,我們的實現代碼如下:

func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
    return 2.5
}
    
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        
    let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
    let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
    let finalFrameForVC = transitionContext.finalFrameForViewController(toViewController)
    let containerView = transitionContext.containerView()
    let bounds = UIScreen.mainScreen().bounds
    toViewController.view.frame = CGRectOffset(finalFrameForVC, 0, bounds.size.height)
    containerView.addSubview(toViewController.view)
        
    UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: .CurveLinear, animations: {
        fromViewController.view.alpha = 0.5
        toViewController.view.frame = finalFrameForVC
        }, completion: {
            finished in
            transitionContext.completeTransition(true)
            fromViewController.view.alpha = 1.0
    })
}

第一個方法很簡單,設定動畫執行時間。第二個方法則用來編寫我們自定義的動畫代碼,在這個方法中我們可以利用transitionContext(轉場上下文)來獲得我們將來的來源控制器、目標控制器、動畫完成后的最終frame,還可以獲得用來管理來源或目標視圖的容器視圖。

然后我們將目標視圖調整到屏幕下方并將其添加到容器視圖內。接下來在動畫執行的閉包內,將目標視圖的位置變為最終位置,并將來源視圖的透明度降為0.5,使其在目標視圖進入的過程中產生一個淡出的效果。在動畫完成的閉包內,我們告知transitionContext動畫已完成,并將來源視圖的透明度改回1.0。

設置轉場動畫代理

接下來我們需要為目標控制器設置轉場動畫代理,這里我們指定來源控制器作為我們的代理。在ItemsTableViewController中,讓其遵守UIViewControllerTransitioningDelegate協議,在storyboard中找到我們modal的segue,設置它的Identifier為showAction。然后在ItemsTableViewController中添加如下代碼:

let customPresentAnimationController = CustomPresentAnimationController()
 
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        
    if segue.identifier == "showAction" {
        let toViewController = segue.destinationViewController as UIViewController
        toViewController.transitioningDelegate = self
    }
}

func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    return customPresentAnimationController
}

我們創建了一個動畫管理器對象,設置目標控制器的轉場代理為來源控制器,然后實現代理協議中的animationControllerForPresentedController方法,該方法用于指定modal過程中展示視圖的動畫,在該方法中返回我們自定義的動畫管理器對象。

運行我們的程序,效果如下圖所示:

跟系統默認modal效果差不多,不過帶有彈簧效果。如果你希望有不同的效果,你可以對下面這句代碼進行修改。

toViewController.view.frame = CGRectOffset(finalFrameForVC, 0, bounds.size.height)

比如將其改為如下代碼:

toViewController.view.frame = CGRectOffset(finalFrameForVC, 0, -bounds.size.height)

再次運行程序,我們的modal動畫就變為從上往下了。

自定義modal過程中退出視圖的動畫

我們的程序現在點擊Dismiss退出目標控制器時,仍然是系統默認的動畫,接下來實現這個自定義動畫。

步驟同前面基本一樣,創建一個叫做CustomDismissAnimationController的動畫管理器,實現如下代理方法:

func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
    return 2
}
 
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
    let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
    let finalFrameForVC = transitionContext.finalFrameForViewController(toViewController)
    let containerView = transitionContext.containerView()
    toViewController.view.frame = finalFrameForVC
    toViewController.view.alpha = 0.5
    containerView.addSubview(toViewController.view)
    containerView.sendSubviewToBack(toViewController.view)
    
    UIView.animateWithDuration(transitionDuration(transitionContext), animations: {
        fromViewController.view.frame = CGRectInset(fromViewController.view.frame, fromViewController.view.frame.size.width / 2, fromViewController.view.frame.size.height / 2)
        toViewController.view.alpha = 1.0
    }, completion: {
        finished in
        transitionContext.completeTransition(true)
    })
}

這次我們使用一個新的動畫方式,讓來源視圖從中心點開始逐漸變小直到消失。首先我們將目標控制器設置為最終位置,透明度為0.5,并將其添加到容器視圖的底層中使其開始時不可見。在動畫執行過程中,來源視圖逐漸變小,露出底層的目標視圖,并將目標視圖透明度過渡到1.0。

接下來在ItemsTableViewController中添加如下代碼:

let customDismissAnimationController = CustomDismissAnimationController()

func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    return customDismissAnimationController
}

animationControllerForDismissedController這個代理方法指定了modal過程中退出視圖的動畫。運行程序,你會發現我們的動畫有點小Bug。

我們可以看到,白色的背景視圖確實如我們所愿從中心點逐漸縮小,但是圖片視圖的大小卻保持不變,這是因為改變來源視圖的時候,它的子控件的大小并不會跟著發生改變,我們可以通過視圖快照的技術來解決這一問題。

將animateTransition方法的實現修改為如下代碼:

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
    let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
    let finalFrameForVC = transitionContext.finalFrameForViewController(toViewController)
    let containerView = transitionContext.containerView()
    toViewController.view.frame = finalFrameForVC
    toViewController.view.alpha = 0.5
    containerView.addSubview(toViewController.view)
    containerView.sendSubviewToBack(toViewController.view)
        
    let snapshotView = fromViewController.view.snapshotViewAfterScreenUpdates(false)
    snapshotView.frame = fromViewController.view.frame
    containerView.addSubview(snapshotView)
        
    fromViewController.view.removeFromSuperview()
        
    UIView.animateWithDuration(transitionDuration(transitionContext), animations: {
        snapshotView.frame = CGRectInset(fromViewController.view.frame, fromViewController.view.frame.size.width / 2, fromViewController.view.frame.size.height / 2)
        toViewController.view.alpha = 1.0
    }, completion: {
        finished in
        snapshotView.removeFromSuperview()
        transitionContext.completeTransition(true)
    })  
}

我們給來源視圖生成了一個快照,將它添加到容器視圖中利用它來做動畫,并將來源視圖從父控件中移除。再次運行程序,我們的動畫效果就正常了。

導航控制器的轉場動畫

在UITabBarController和UINavigationController的管理下,你無需為每個目標控制器都設置轉場代理,可以直接設置UITabBarControllerDelegate或UINavigationControllerDelegate即可。

接下來我們演示如何為導航控制器設置自定義轉場動畫。首先,仍然是創建一個動畫管理器類叫做CustomNavigationAnimationController,然后實現UIViewControllerAnimatedTransitioning協議的方法。這里的動畫代碼采用的是一個開源的三維旋轉動畫,讀者可以到這里自行研究。

var reverse: Bool = false
    
func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
    return 1.5
}
    
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    let containerView = transitionContext.containerView()
    let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
    let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
    let toView = toViewController.view
    let fromView = fromViewController.view
    let direction: CGFloat = reverse ? -1 : 1
    let const: CGFloat = -0.005
        
    toView.layer.anchorPoint = CGPointMake(direction == 1 ? 0 : 1, 0.5)
    fromView.layer.anchorPoint = CGPointMake(direction == 1 ? 1 : 0, 0.5)
        
    var viewFromTransform: CATransform3D = CATransform3DMakeRotation(direction * CGFloat(M_PI_2), 0.0, 1.0, 0.0)
    var viewToTransform: CATransform3D = CATransform3DMakeRotation(-direction * CGFloat(M_PI_2), 0.0, 1.0, 0.0)
    viewFromTransform.m34 = const
    viewToTransform.m34 = const
        
    containerView.transform = CGAffineTransformMakeTranslation(direction * containerView.frame.size.width / 2.0, 0)
    toView.layer.transform = viewToTransform
    containerView.addSubview(toView)
        
    UIView.animateWithDuration(transitionDuration(transitionContext), animations: {
        containerView.transform = CGAffineTransformMakeTranslation(-direction * containerView.frame.size.width / 2.0, 0)
        fromView.layer.transform = viewFromTransform
        toView.layer.transform = CATransform3DIdentity
    }, completion: {
        finished in
        containerView.transform = CGAffineTransformIdentity
        fromView.layer.transform = CATransform3DIdentity
        toView.layer.transform = CATransform3DIdentity
        fromView.layer.anchorPoint = CGPointMake(0.5, 0.5)
        toView.layer.anchorPoint = CGPointMake(0.5, 0.5)
    
        if (transitionContext.transitionWasCancelled()) {
            toView.removeFromSuperview()
        } else {
            fromView.removeFromSuperview()
        }
        transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
    })        
}

注意這里我們添加了一個reverse變量,用來指定轉場動畫的方向,這樣我們可以將導航控制器push和pop過程的動畫封裝在一個動畫管理器中。

在ItemsTableViewController中更改它的聲明使其遵守UINavigationControllerDelegate協議,在viewDidLoad方法中設置代理為自己navigationController?.delegate = self,然后添加如下代碼:

let customNavigationAnimationController = CustomNavigationAnimationController()

func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    customNavigationAnimationController.reverse = operation == .Pop
    return customNavigationAnimationController
}

上面這個導航控制器的代理方法用于指定push或pop時的轉場動畫,其中operation參數可以用來判斷轉場的方向。運行程序,如下圖所示:

導航控制器的手勢交互

我們知道蘋果官方為導航控制器添加了一個默認的手勢交互,就是在屏幕左側向右滑動可以返回上一界面并帶有pop動畫,接下來我們為我們的自定義動畫添加手勢交互。

手勢交互的管理器需要遵守的是UIViewControllerInteractiveTransitioning協議,該協議需要實現startInteractiveTransition方法指定開始交互,不過蘋果官方為我們提供了另一個已經實現該協議的交互管理器類UIPercentDrivenInteractiveTransition,并提供以百分比的形式來控制交互過程的功能,比如控制交互的更新、取消、完成等,我們直接使用它來實現我們的交互控制。

創建一個類叫做CustomInteractionController并繼承自UIPercentDrivenInteractiveTransition,添加如下代碼:

var navigationController: UINavigationController!
var shouldCompleteTransition = false
var transitionInProgress = false
var completionSeed: CGFloat {
    return 1 - percentComplete
}
    
func attachToViewController(viewController: UIViewController) {
    navigationController = viewController.navigationController
    setupGestureRecognizer(viewController.view)
}
    
private func setupGestureRecognizer(view: UIView) {
        view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: "handlePanGesture:"))
}
    
func handlePanGesture(gestureRecognizer: UIPanGestureRecognizer) {
    let viewTranslation = gestureRecognizer.translationInView(gestureRecognizer.view!.superview!)
    switch gestureRecognizer.state {
    case .Began:
        transitionInProgress = true
        navigationController.popViewControllerAnimated(true)
    case .Changed:
        var const = CGFloat(fminf(fmaxf(Float(viewTranslation.x / 200.0), 0.0), 1.0))
        shouldCompleteTransition = const > 0.5
        updateInteractiveTransition(const)
    case .Cancelled, .Ended:
        transitionInProgress = false
        if !shouldCompleteTransition || gestureRecognizer.state == .Cancelled {
            cancelInteractiveTransition()
        } else {
            finishInteractiveTransition()
        }
    default:
        println("Swift switch must be exhaustive, thus the default")
    }
}

attachToViewController方法用于將來傳入導航控制器的目標控制器,我們為目標控制器的整個view添加了滑動手勢以便將來可以實現滑動返回的pop動畫,在監聽手勢滑動的方法中,我們根據手勢的狀態做如下處理:

  • 開始滑動:設置transitionInProgress為true,并開始執行導航控制器的pop返回。
  • 滑動過程中:更新交互過程的百分比,我們假設指定滑動200點即為交互完成。
  • 取消或結束:設置transitionInProgress為false,如果交互過程執行50%以上則認為交互完成。

接來下來到我們的ItemsTableViewController,添加如下代碼:

let customInteractionController = CustomInteractionController()

然后修改我們之前實現的導航控制器的代理方法如下:

func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    if operation == .Push {
        customInteractionController.attachToViewController(toVC)
    }
    customNavigationAnimationController.reverse = operation == .Pop
    return customNavigationAnimationController
}

當我們push一個目標控制器時,就為該目標控制器設定交互控制。最后實現導航控制器代理中的另一個方法用于指定交互控制器,代碼如下:

func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
    return customInteractionController.transitionInProgress ? customInteractionController : nil
}

運行程序,如下圖所示:

完整的示例程序鏈接地址請點擊這里

推薦閱讀:

實現下拉菜單的小Demo

Demo實現效果如下圖所示,下載完整的Demo代碼請點擊這里。(譯注:原文地址需要FQ訪問,本人已轉存到GitHub上,點擊這里。)

實現過程同我們前面講的自定義轉場動畫過程一樣,首先創建一個動畫管理器類MenuTransitionManager,然后設置目標控制器的轉場代理,這次我們使用動畫管理器對象作為代理,所以MenuTransitionManager既遵守了UIViewControllerAnimatedTransitioning協議,也遵守了UIViewControllerTransitioningDelegate協議。動畫的執行代碼比較簡單,只是通過改變transform控制來源和目標視圖的上下移動,目標視圖我們仍然使用了快照技術。

我們還為來源視圖的快照添加了一個點擊的手勢,這樣在顯示下拉菜單后,除了點擊相應的菜單選項,點擊下部的快照也可以返回到主頁視圖。只不過點擊手勢的處理我們使用了代理設計模式,而點擊手勢的添加我們使用了Swift的屬性觀察器語法,讀者可以自行研究學習。

最后,希望大家學的愉快!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容