轉場動畫這事,說簡單也簡單,能夠經過presentViewController:animated:completion:
和dismissViewControllerAnimated:completion:
這一組函數以模態視圖的方式展示、隱藏視圖。若是用到了navigationController
,還能夠調用pushViewController:animated:
和popViewController
這一組函數將新的視圖控制器壓棧、彈棧。git
下圖中全部轉場動畫都是自定義的動畫,這些效果若是不用自定義動畫則很難甚至沒法實現:github
因爲錄屏的緣由,有些效果沒法徹底展示,好比它其實還支持橫屏。spring
自定義轉場動畫的效果實現起來比較複雜,若是僅僅是拷貝一份可以運行的代碼卻不懂其中原理,就有可能帶來各類隱藏的bug。本文由淺入深介紹下面幾個知識:swift
我爲這篇教程製做了一個demo,您能夠去在個人github上clone下來:CustomTransition,若是以爲有幫助還望給個star以示支持。本文以Swift+純代碼實現,對應的OC+Storyboard版本在demo中也能夠找到,那是蘋果的官方示範代碼,正確性更有保證。demo中用到了CocoaPods,您也許須要執行pod install
命令並打開.xcworkspace
文件。緩存
在開始正式的教程前,您首先須要下載demo,在代碼面前文字是蒼白的,demo中包含的註釋足以解釋本文全部的知識點。其次,您還得了解這幾個背景知識。閉包
在代碼和文字中,常常會出現fromView
和toView
。若是錯誤的理解它們的含義會致使動畫邏輯徹底錯誤。fromView
表示當前視圖,toView
表示要跳轉到的視圖。若是是從A視圖控制器present到B,則A是from,B是to。從B視圖控制器dismiss到A時,B變成了from,A是to。用一張圖表示:app
這也是一組相對的概念,它容易與fromView
和toView
混淆。簡單來講,它不受present或dismiss的影響,若是是從A視圖控制器present到B,那麼A老是B的presentingViewController
,B老是A的presentedViewController
。ide
這是一個枚舉類型,表示present時動畫的類型。其中能夠自定義動畫效果的只有兩種:FullScreen
和Custom
,二者的區別在於FullScreen
會移除fromView
,而Custom
不會。好比文章開頭的gif中,第三個動畫效果就是Custom
。函數
最簡單的轉場動畫是使用transitionFromViewController
方法:佈局
這個方法雖然已通過時,可是對它的分析有助於後面知識的理解。它一共有6個參數,前兩個表示從哪一個VC開始,跳轉到哪一個VC,中間兩個參數表示動畫的時間和選項。最後兩個參數表示動畫的具體實現細節和回調閉包。
這六個參數其實就是一次轉場動畫所必備的六個元素。它們能夠分爲兩組,前兩個參數爲一組,表示頁面的跳轉關係,後面四個爲一組,表示動畫的執行邏輯。
這個方法的缺點之一是可自定義程度不高(在後面您會發現能自定義的不只僅是動畫方式),另外一個缺點則是重用性很差,也能夠說是耦合度比較大。
在最後兩個閉包參數中,能夠預見的是fromViewController
和toViewController
參數都會被用到,並且他們是動畫的關鍵。假設視圖控制器A能夠跳轉到B、C、D、E、F,並且跳轉動畫基本類似,您會發現transitionFromViewController
方法要被複制屢次,每次只會修改少許內容。
出於解耦和提升可自定義程度的考慮,咱們來學習轉場動畫的正確使用姿式。
首先要了解一個關鍵概念:轉場動畫代理,它是一個實現了UIViewControllerTransitioningDelegate
協議的對象。咱們須要本身實現這個對象,它的做用是爲UIKit提供如下幾個對象中的一個或多個:
它是實現了UIViewControllerAnimatedTransitioning
協議的對象,用於控制動畫的持續時間和動畫展現邏輯,代理能夠爲present和dismiss過程分別提供Animator,也能夠提供同一個Animator。
它能夠對present過程更加完全的自定義,好比修改被展現視圖的大小,新增自定義視圖等,後面會有詳細介紹。
在這一小節中,咱們首先介紹最簡單的Animator。回顧一下轉場動畫必備的6個元素,它們被分爲兩組,彼此之間沒有關聯。Animator的做用等同於第二組的四個元素,也就是說對於同一個Animator,能夠適用於A跳轉B,也能夠適用於A跳轉C。它表示一種通用的頁面跳轉時的動畫邏輯,不受限於具體的視圖控制器。
若是您讀懂了這段話,整個自定義的轉場動畫邏輯就很清楚了,以視圖控制器A跳轉到B爲例:
presentViewController:animated:completion:
並把參數animated設置爲true用具體的例子解釋就是:
// 這個類至關於A
class CrossDissolveFirstViewController: UIViewController, UIViewControllerTransitioningDelegate {
// 這個對象至關於B
crossDissolveSecondViewController.transitioningDelegate = self
// 點擊按鈕觸發的函數
func animationButtonDidClicked() {
self.presentViewController(crossDissolveSecondViewController,
animated: true, completion: nil)
}
// 下面這兩個函數定義在UIViewControllerTransitioningDelegate協議中
// 用於爲present和dismiss提供animator
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
// 也可使用CrossDissolveAnimator,動畫效果各有不一樣
// return CrossDissolveAnimator()
return HalfWaySpringAnimator()
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CrossDissolveAnimator()
}
}
複製代碼
動畫的關鍵在於animator如何實現,它實現了UIViewControllerAnimatedTransitioning
協議,至少須要實現兩個方法,我建議您仔細閱讀animateTransition
方法中的註釋,它是整個動畫邏輯的核心:
class HalfWaySpringAnimator: NSObject, UIViewControllerAnimatedTransitioning {
/// 設置動畫的持續時間
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 2
}
/// 設置動畫的進行方式,附有詳細註釋,demo中其餘地方的這個方法再也不解釋
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
let containerView = transitionContext.containerView()
// 須要關注一下from/to和presented/presenting的關係
// For a Presentation:
// fromView = The presenting view.
// toView = The presented view.
// For a Dismissal:
// fromView = The presented view.
// toView = The presenting view.
var fromView = fromViewController?.view
var toView = toViewController?.view
// iOS8引入了viewForKey方法,儘量使用這個方法而不是直接訪問controller的view屬性
// 好比在form sheet樣式中,咱們爲presentedViewController的view添加陰影或其餘decoration,animator會對整個decoration view
// 添加動畫效果,而此時presentedViewController的view只是decoration view的一個子視圖
if transitionContext.respondsToSelector(Selector("viewForKey:")) {
fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)
toView = transitionContext.viewForKey(UITransitionContextToViewKey)
}
// 咱們讓toview的origin.y在屏幕的一半處,這樣它從屏幕的中間位置彈起而不是從屏幕底部彈起,彈起過程當中逐漸變爲不透明
toView?.frame = CGRectMake(fromView!.frame.origin.x, fromView!.frame.maxY / 2, fromView!.frame.width, fromView!.frame.height)
toView?.alpha = 0.0
// 在present和,dismiss時,必須將toview添加到視圖層次中
containerView?.addSubview(toView!)
let transitionDuration = self.transitionDuration(transitionContext)
// 使用spring動畫,有彈簧效果,動畫結束後必定要調用completeTransition方法
UIView.animateWithDuration(transitionDuration, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0, options: .CurveLinear, animations: { () -> Void in
toView!.alpha = 1.0 // 逐漸變爲不透明
toView?.frame = transitionContext.finalFrameForViewController(toViewController!) // 移動到指定位置
}) { (finished: Bool) -> Void in
let wasCancelled = transitionContext.transitionWasCancelled()
transitionContext.completeTransition(!wasCancelled)
}
}
}
複製代碼
animateTransition
方法的核心則是從轉場動畫上下文獲取必要的信息以完成動畫。上下文是一個實現了UIViewControllerContextTransitioning
的對象,它的做用在於爲animateTransition
方法提供必備的信息。您不該該緩存任何關於動畫的信息,而是應該老是從轉場動畫上下文中獲取(好比fromView和toView),這樣能夠保證老是獲取到最新的、正確的信息。
獲取到足夠信息後,咱們調用UIView.animateWithDuration
方法把動畫交給Core Animation處理。千萬不要忘記在動畫調用結束後,執行completeTransition
方法。
本節的知識在Demo的Cross Dissolve文件夾中有詳細的代碼。其中有兩個animator文件,這說明咱們能夠爲present和dismiss提供同一個animator,或者分別提供各自對應的animator。若是二者動畫效果相似,您能夠共用同一個animator,唯一的區別在於:
toView
加入到container的視圖層級。fromView
從container的視圖層級中移除。若是您被前面這一大段代碼和知識弄暈了,或者暫時用不到這些具體的知識,您至少須要記住自定義動畫的基本原理和流程:
presentedViewController
)的transitioningDelegate
presentingViewController
),也能夠是本身建立的對象,它須要爲轉場動畫提供一個animator對象。animateTransition
是整個動畫的核心邏輯。剛剛咱們說到,設置了toViewController
的transitioningDelegate
屬性而且present時,UIKit會從代理處獲取animator,其實這裏還有一個細節:UIKit還會調用代理的interactionControllerForPresentation:
方法來獲取交互式控制器,若是獲得了nil則執行非交互式動畫,這就回到了上一節的內容。
若是獲取到了不是nil的對象,那麼UIKit不會調用animator的animateTransition
方法,而是調用交互式控制器(還記得前面介紹動畫代理的示意圖麼,交互式動畫控制器和animator是平級關係)的startInteractiveTransition:
方法。
所謂的交互式動畫,一般是基於手勢驅動,產生一個動畫完成的百分比來控制動畫效果(文章開頭的gif中第二個動畫效果)。整個動畫再也不是一次性、連貫的完成,而是在任什麼時候候均可以改變百分比甚至取消。這須要一個實現了UIPercentDrivenInteractiveTransition
協議的交互式動畫控制器和animator協同工做。這看上去是一個很是複雜的任務,但UIKit已經封裝了足夠多細節,咱們只須要在交互式動畫控制器和中定義一個時間處理函數(好比處理滑動手勢),而後在接收到新的事件時,計算動畫完成的百分比而且調用updateInteractiveTransition
來更新動畫進度便可。
用下面這段代碼簡單表示一下整個流程(刪除了部分細節和註釋,請不要以此爲正確參考),完整的代碼請參考demo中的Interactivity文件夾:
// 這個至關於fromViewController
class InteractivityFirstViewController: UIViewController {
// 這個至關於toViewController
lazy var interactivitySecondViewController: InteractivitySecondViewController = InteractivitySecondViewController()
// 定義了一個InteractivityTransitionDelegate類做爲代理
lazy var customTransitionDelegate: InteractivityTransitionDelegate = InteractivityTransitionDelegate()
override func viewDidLoad() {
super.viewDidLoad()
setupView() // 主要是一些UI控件的佈局,能夠無視其實現細節
/// 設置動畫代理,這個代理比較複雜,因此咱們新建了一個代理對象而不是讓self做爲代理
interactivitySecondViewController.transitioningDelegate = customTransitionDelegate
}
// 觸發手勢時,也會調用animationButtonDidClicked方法
func interactiveTransitionRecognizerAction(sender: UIScreenEdgePanGestureRecognizer) {
if sender.state == .Began {
self.animationButtonDidClicked(sender)
}
}
func animationButtonDidClicked(sender: AnyObject) {
self.presentViewController(interactivitySecondViewController, animated: true, completion: nil)
}
}
複製代碼
非交互式的動畫代理只須要爲present和dismiss提供animator便可,可是在交互式的動畫代理中,還須要爲present和dismiss提供交互式動畫控制器:
class InteractivityTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate {
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return InteractivityTransitionAnimator(targetEdge: targetEdge)
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return InteractivityTransitionAnimator(targetEdge: targetEdge)
}
/// 前兩個函數和淡入淡出demo中的實現一致
/// 後兩個函數用於實現交互式動畫
func interactionControllerForPresentation(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return TransitionInteractionController(gestureRecognizer: gestureRecognizer, edgeForDragging: targetEdge)
}
func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return TransitionInteractionController(gestureRecognizer: gestureRecognizer, edgeForDragging: targetEdge)
}
}
複製代碼
animator中的代碼略去,它和非交互式動畫中的animator相似。由於交互式的動畫只是一種錦上添花,它必須支持非交互式的動畫,好比這個例子中,點擊按鈕依然出發的是非交互式的動畫,只是手勢滑動纔會觸發交互式動畫。
class TransitionInteractionController: UIPercentDrivenInteractiveTransition {
/// 當手勢有滑動時觸發這個函數
func gestureRecognizeDidUpdate(gestureRecognizer: UIScreenEdgePanGestureRecognizer) {
switch gestureRecognizer.state {
case .Began: break
case .Changed: self.updateInteractiveTransition(self.percentForGesture(gestureRecognizer)) //手勢滑動,更新百分比
case .Ended: // 滑動結束,判斷是否超過一半,若是是則完成剩下的動畫,不然取消動畫
if self.percentForGesture(gestureRecognizer) >= 0.5 {
self.finishInteractiveTransition()
}
else {
self.cancelInteractiveTransition()
}
default: self.cancelInteractiveTransition()
}
}
private func percentForGesture(gesture: UIScreenEdgePanGestureRecognizer) -> CGFloat {
let percent = 根據gesture計算得出
return percent
}
}
複製代碼
交互式動畫是在非交互式動畫的基礎上實現的,咱們須要建立一個繼承自UIPercentDrivenInteractiveTransition
類型的子類,而且在動畫代理中返回這個類型的實例對象。
在這個類型中,監聽手勢(或者下載進度等等)的時間變化,而後調用percentForGesture
方法更新動畫進度便可。
在進行轉場動畫的同時,您還能夠進行一些同步的,額外的動畫,好比文章開頭gif中的第三個例子。presentedView
和presentingView
能夠更改自身的視圖層級,添加額外的效果(陰影,圓角)。UIKit使用轉成協調器來管理這些額外的動畫。您能夠經過須要產生動畫效果的視圖控制器的transitionCoordinator
屬性來獲取轉場協調器,轉場協調器只在轉場動畫的執行過程當中存在。
想要完成gif中第三個例子的效果,咱們還須要使用UIModalPresentationStyle.Custom
來代替.FullScreen
。由於後者會移除fromViewController
,這顯然不符合需求。
當present的方式爲.Custom
時,咱們還可使用UIPresentationController
更加完全的控制轉場動畫的效果。一個 presentation controller具有如下幾個功能:
presentedViewController
的視圖大小presentedView
的外觀您能夠認爲,. FullScreen
以及其餘present風格都是swift爲咱們實現提供好的,它們是.Custom
的特例。而.Custom
容許咱們更加自由的定義轉場動畫效果。
UIPresentationController
提供了四個函數來定義present和dismiss動畫開始先後的操做:
presentationTransitionWillBegin
: present將要執行時presentationTransitionDidEnd
:present執行結束後dismissalTransitionWillBegin
:dismiss將要執行時dismissalTransitionDidEnd
:dismiss執行結束後下面的代碼簡要描述了gif中第三個動畫效果的實現原理,您能夠在demo的Custom Presentation文件夾下查看完成代碼:
// 這個至關於fromViewController
class CustomPresentationFirstViewController: UIViewController {
// 這個至關於toViewController
lazy var customPresentationSecondViewController: CustomPresentationSecondViewController = CustomPresentationSecondViewController()
// 建立PresentationController
lazy var customPresentationController: CustomPresentationController = CustomPresentationController(presentedViewController: self.customPresentationSecondViewController, presentingViewController: self)
override func viewDidLoad() {
super.viewDidLoad()
setupView() // 主要是一些UI控件的佈局,能夠無視其實現細節
// 設置轉場動畫代理
customPresentationSecondViewController.transitioningDelegate = customPresentationController
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func animationButtonDidClicked() {
self.presentViewController(customPresentationSecondViewController, animated: true, completion: nil)
}
}
複製代碼
重點在於如何實現CustomPresentationController
這個類:
class CustomPresentationController: UIPresentationController, UIViewControllerTransitioningDelegate {
var presentationWrappingView: UIView? // 這個視圖封裝了原視圖,添加了陰影和圓角效果
var dimmingView: UIView? = nil // alpha爲0.5的黑色蒙版
// 告訴UIKit爲哪一個視圖添加動畫效果
override func presentedView() -> UIView? {
return self.presentationWrappingView
}
}
// 四個方法自定義轉場動畫發生先後的操做
extension CustomPresentationController {
override func presentationTransitionWillBegin() {
// 設置presentationWrappingView和dimmingView的UI效果
let transitionCoordinator = self.presentingViewController.transitionCoordinator()
self.dimmingView?.alpha = 0
// 經過轉場協調器執行同步的動畫效果
transitionCoordinator?.animateAlongsideTransition({ (context: UIViewControllerTransitionCoordinatorContext) -> Void in
self.dimmingView?.alpha = 0.5
}, completion: nil)
}
/// present結束時,把dimmingView和wrappingView都清空,這些臨時視圖用不到了
override func presentationTransitionDidEnd(completed: Bool) {
if !completed {
self.presentationWrappingView = nil
self.dimmingView = nil
}
}
/// dismiss開始時,讓dimmingView徹底透明,這個動畫和animator中的動畫同時發生
override func dismissalTransitionWillBegin() {
let transitionCoordinator = self.presentingViewController.transitionCoordinator()
transitionCoordinator?.animateAlongsideTransition({ (context: UIViewControllerTransitionCoordinatorContext) -> Void in
self.dimmingView?.alpha = 0
}, completion: nil)
}
/// dismiss結束時,把dimmingView和wrappingView都清空,這些臨時視圖用不到了
override func dismissalTransitionDidEnd(completed: Bool) {
if completed {
self.presentationWrappingView = nil
self.dimmingView = nil
}
}
}
extension CustomPresentationController {
}
複製代碼
除此之外,這個類還要處理子視圖佈局相關的邏輯。它做爲動畫代理,還須要爲動畫提供animator對象,詳細代碼請在demo的Custom Presentation文件夾下閱讀。
到目前爲止,全部轉場動畫都是適用於present和dismiss的,其實UINavigationController
也能夠自定義轉場動畫。二者是平行關係,不少均可以類比過來:
class FromViewController: UIViewController, UINavigationControllerDelegate {
let toViewController: ToViewController = ToViewController()
override func viewDidLoad() {
super.viewDidLoad()
setupView() // 主要是一些UI控件的佈局,能夠無視其實現細節
self.navigationController.delegate = self
}
}
複製代碼
與present/dismiss不一樣的時,如今視圖控制器實現的是UINavigationControllerDelegate
協議,讓本身成爲navigationController
的代理。這個協議相似於此前的UIViewControllerTransitioningDelegate
協議。
FromViewController
實現UINavigationControllerDelegate
協議的具體操做以下:
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController)
-> UIViewControllerAnimatedTransitioning? {
if operation == .Push {
return PushAnimator()
}
if operation == .Pop {
return PopAnimator()
}
return nil;
}
複製代碼
至於animator,就和此前沒有任何區別了。可見,一個封裝得很好的animator,不只能在present/dismiss時使用,甚至還能夠在push/pop時使用。
UINavigationController也能夠添加交互式轉場動畫,原理也和此前相似。
對於非交互式動畫,須要設置presentedViewController
的transitioningDelegate
屬性,這個代理須要爲present和dismiss提供animator。在animator中規定了動畫的持續時間和表現邏輯。
對於交互式動畫,須要在此前的基礎上,由transitioningDelegate
屬性提供交互式動畫控制器。在控制器中進行事件處理,而後更新動畫完成進度。
對於自定義動畫,能夠經過UIPresentationController
中的四個函數自定義動畫執行先後的效果,能夠修改presentedViewController
的大小、外觀並同步執行其餘的動畫。
自定義動畫的水仍是比較深,本文僅適合作入門學習用,歡迎互相交流。