原文:ios-animation-tutorial-custom-view-controller-presentation-transitions
作者:Marin Todorov
譯者:kmyhy
當你顯現相機、通訊錄、或某種自定義模式窗口時,你每次都會調用同1個 UIKit 方法 present(_:animated:completion:)。這個方法將當前屏幕“讓給”另外一個 view controller。
默許的顯現動畫簡單地用新視圖推開當前視圖。下圖演示了“新建聯系人” view controller 在聯系人列表視圖上層向上滑出:
在本教程中,你將用自己的自定義顯現動畫替換默許動畫,并完本錢教程中的項目。
下載本文的開始項目 Beginner Cook。打開 Main.storyboard :
第1個 view controller(即 ViewController)包括了 app 的標題和主要介紹和底部的1個 scroll view,用于顯示1個有用的香草列表。
當用戶點擊了列表中的圖片,main view controller 會顯現1個 HerbDetailsViewController;這個 view controller 有1個背景、1個標題、1個描寫和幾個按鈕用于注明圖片的所有者。
在 ViewController.swift 和 HerbDetailsViewController.swift 已有部份代碼了,足以保持 app 運行。運行程序,app 是這個模樣:
點擊某個香草圖片,細節頁面以標準的彈出動畫方式顯現。對1般的 app 來講這也足夠了,但對你的 app 則需要做得更好!
你的任務是創建自定義顯現動畫讓你的 app 更加殘暴奪目!你需要將目前內置的動畫替換成:用所點擊的香草的圖片展開至全屏!
擼起手袖,系緊圍裙,準備動手開始定制顯現控制器!
UIKit 允許你通過拜托模型定制化 view controller 的顯現進程;你可讓 main view controller(或可以用另外一個類專門來干這個)采取 UIViewControllerTransitioningDelegate 協議。
每當你顯現1個新的 view controller 時,UIKit 會詢問它的拜托是不是需要使用自定義動畫。自定義動畫的第1步是這樣的:
UIKit 會調用 animationController(forPresented:presenting:source:) 方法,看是不是有1個 UIViewControllerAnimatedTransitioning 對象返回。如果這個方法返回空,UIKit 使用默許動畫,否則,UIKit 使用返回的對象作為這次轉換的動畫控制器。
UIKit 首先詢問動畫控制器(簡稱為 animator),動畫需要幾秒鐘?然后調用它的animateTransition(using:) 方法。這時候你的自定義動畫開始生效了。
在 animateTransition(using:) 方法中,你可以同時訪問到正在顯示的 view controller 和行將顯現的新 view controller。你可以淡入、縮放、旋轉并為所欲為地操作已有的視圖和新的視圖。
你已大致了解了之定義顯現控制器是如何工作的了,現在,開始來創建我們自己的吧!
由于拜托的責任是管理動畫控制器 animator 對象,而由 animator 來履行真實的動畫,因此在編寫拜托代碼之前的第1件事情就是創建1個 animator 類。
打開 Xcode 菜單 File\New\File… 選擇模板: iOS\Source\Cocoa Touch Class。
類名設置為 PopAnimator,語言選擇 Swift,繼承于 NSObject。
打開 PopAnimator.swift 修改類定義,實現 UIViewControllerAnimatedTransitioning 協議:
class PopAnimator: NSObject, UIViewControllerAnimatedTransitioning {
}
Xcode 會抱怨沒有實現必須的拜托方法,等下我們會解決這個。
在類中添加以下方法:
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0
}
動畫時長返回 0 只是臨時的;后面會修改這個為真實的時長。
繼續新增以下方法:
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
}
這個方法將放入動畫代碼,暫時是空實現,以消除 Xcode 的報錯。
有了基本的 animator 類以后,你可以在 view controller 中實現拜托方法了。
打開 ViewController.swift 新增以下擴大:
extension ViewController: UIViewControllerTransitioningDelegate {
}
這聲明對 transitioning delegate 協議的實現。等會我們再來添加這些方法。
找到 didTapImageView(_:) 方法。在方法底部,你看到了顯現詳情 view controller 的代碼。herbDetails 是新 view controller 的實例;你需要將它的 transitioning 拜托設置為 main controller。
在這個方法最后1行,即調用 present(…) 方法以后加入以下代碼:
// ...
present(herbDetails, animated: true, completion: nil)
herbDetails.transitioningDelegate = self // 加入這行
現在 UIKit 會在每次顯現 details view controller 的時候都索要1個 animator 對象。但你還沒有實現任何 UIViewControllerTransitioningDelegate 方法,所以 UIKit 還是會使用默許的動畫。
接下來應當實例化1個 animator 對象并在 UIKit 詢問的時候返給它。
在 ViewController 中添加1個屬性:
let transition = PopAnimator()
這個是1個 PopAnimator 對象,用于驅動你的 view controller 動畫。你只需要1個 PopAnimator 對象,由于你可以在每次顯現 view controller 時都使用同1個 animator 對象,由于每次的動畫都是同1個。
在 ViewController 的擴大中加入第1個拜托方法:
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return transition
}
這個方法提供幾個參數,你可以根據它們來決定是返回1個自定義的動畫還是不。在本文中,你總是返回同1個 PopAnimator 實例,由于你只有1個顯現動畫。
你已添加了1個用于顯現 view controller 的拜托方法,那末用于解散的呢?
這是另外一個拜托方法:
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return nil
}
這個方法和前1個方法基本是干同1件事情:判斷要解散的是哪一個 view controller,覺此來決定是不是返回 nil,返回 nil 表示采取默許的解散動畫,或返回1個自定義的 animator。這里你返回的是 nil,由于你還沒有實現解散動畫。
你已具有了1個自定義的 animator 來負責自定義動畫,但它是如何工作的呢?
運行程序,點擊任何1張香草圖片:
甚么也沒產生。為啥?你有1個用于驅動動畫的自定義 animator,但是……等等,在 animator 類中還沒有編寫代碼!你會在下1節完成這個任務。
打開 PopAnimator.swift; 這里我們將加入兩個 view controller 之間進行轉換的代碼。
首先,加入幾個屬性:
let duration = 1.0
var presenting = true
var originFrame = CGRect.zero
duration 變量會用到幾個地方,比如告知 UIKit 動畫時長,和創建動畫時。
我們還定義了1個 presenting 變量,用于告知 animator 類,當前是在顯現還是解散進程。我們需要記住這個變量,由于我們將以正面順序履行顯現,而以相反順序履行解散。
最后,我們用 originFrame 變量保存原來用戶所點到的圖片的 frame 形狀——我們會將圖片由這個 frame 以動畫方式放大到全屏,反過來則履行相反動作。當你獲得當前所選圖片并將它的 frame 傳遞給 animator 實例時,需要注意這個 originFrame。
現在,你可以回到 UIViewControllerAnimatedTransitioning 方法來了。
在 transitionDuration() 方法中,用下句替換:
return duration
重用 duration 屬性,能讓你很容易可以調試 transition 動畫。你可以簡單修改這個值,使動畫變快變慢。
現在為 animateTransition 注入魔力。這個方法有1個 UIViewControllerContextTransitioning 參數,通過它你能訪問和轉換相干的 view controller 和參數。
在開始編寫代碼之前,1個重要的問題就是理解 animation context 的實際上是甚么。
當兩個 view controller 之間開始轉換時,原來的 view 被添加到 transition container 轉換容器,新的 view controller 的 view 被創建出來,但依然不可見,以下圖所示:
因此你的任務是在 animateTransition() 方法中將新的 view 添加到轉換容器,“以動畫方式”顯示它,如果有必要的話,將原本的 view 以動畫方式移除。
默許,當轉換動畫完成時,原有 view 從轉換容器中移除。
在你能夠“烹制”出更多的食品之前,你需要創建1個簡單的動畫,看看它是如何實現的,然后再實現更酷的、同時也是更復雜的轉換。
開始,我們用1個簡單的淡出動畫來實現自定義動畫。在 animateTransition 方法中加入:
let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!
首先,獲得容器 view,你的動畫將在這個 view 中產生。然后獲得新的 view 并賦給 toView 變量。
轉換上下文有兩個非常方便的方法,允許你訪問動畫的參與者
這里,你要同時用到 container view 和要顯現的 view 。然后你將要顯現的 view 添加為 container view 的 subview 并以某種方式動畫。
在 animateTransition() 中添加:
containerView.addSubview(toView)
toView.alpha = 0.0
UIView.animate(withDuration: duration,
animations: {
toView.alpha = 1.0
},
completion: { _ in
transitionContext.completeTransition(true)
}
)
注意,你需要在動畫完成塊中調用轉換上下文的 completeTransition() 方法,這是為了通知 UIKit 你的轉換動畫已完成,UIKit 可以完成這次 view controller 轉換了。
運行程序,點擊某張香草圖片,你會看到香草的介紹以淡入的方式出現在主 view controller 中:
這個轉換委曲過得去,你已大概弄清了在 animateTransition 方法中應當干些甚么——你將在里面加入1些更好的東西!
新的動畫需要重新調劑1下代碼結構,因此將 animateTransition() 中的代碼替換為:
let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!
let herbView = presenting ? toView :
transitionContext.view(forKey: .from)!
containerView 是你的動畫行將產生的地方,toView 是需要顯現的新視圖。如果你正在顯現,herbView 就是 toView,否則它應當從上下文中獲得。對解散和顯現,herbView 都會是你將履行動畫的 view。
當你顯現細節頁面時,它會拉伸到全部屏幕大小。解散時,它又會縮小到原始 frame 大小。
在 animateTransition() 添加:
let initialFrame = presenting ? originFrame : herbView.frame
let finalFrame = presenting ? herbView.frame : originFrame
let xScaleFactor = presenting ?
initialFrame.width / finalFrame.width :
finalFrame.width / initialFrame.width
let yScaleFactor = presenting ?
initialFrame.height / finalFrame.height :
finalFrame.height / initialFrame.height
在上述代碼中,我們需要根據條件取得本來的 frame 和終究動畫結束時的 frame,然后計算兩個 view 之間的橫縱比例。
現在我們需要關注新 view 的位置,由于它需要顯示在所點擊的 image 上方,看起來就像是被點的圖象拉伸到了全屏大小。
在 animateTransition() 中添加:
let scaleTransform = CGAffineTransform(scaleX: xScaleFactor,
y: yScaleFactor)
if presenting {
herbView.transform = scaleTransform
herbView.center = CGPoint(
x: initialFrame.midX,
y: initialFrame.midY)
herbView.clipsToBounds = true
}
當顯現新 view 時,我們設置了它的 scale 和位置以便和原圖 frame 的位置大小匹配。
現在在 animateTransition() 中加入最后的代碼:
containerView.addSubview(toView)
containerView.bringSubview(toFront: herbView)
UIView.animate(withDuration: duration, delay:0.0,
usingSpringWithDamping: 0.4, initialSpringVelocity: 0.0,
animations: {
herbView.transform = self.presenting ?
CGAffineTransform.identity : scaleTransform
herbView.center = CGPoint(x: finalFrame.midX, y: finalFrame.midY)
},
completion:{_ in
transitionContext.completeTransition(true)
}
)
首先將 toView 添加到 container。然后,讓 herbView 放在 subview 的最上層,由于你只會對這個 view 進行動畫。記住,在解散時,toView 是原始 view,因此在第1個行代碼,你會將 toView 加在最上層,這樣你的動畫會被隱藏在下層看不見,所以你需要將 herbView 放到上層。
然后,開始動畫。這里使用了1個 spring 動畫,這會帶來1種彈簧效果。
在 animations 塊中,我們修改 herbView 的 transform 屬性和位置。在顯現時,你將底部的小尺寸動畫到全屏,因此目標 transform 就是 identity transform。在解散時,你將它的大小縮小到原始圖片大小。
這里,我們已準備好了將新 view 的位置對齊被點到的圖片,在原來的 frame 和終究的 frame 之間進行動畫,最后調用 completeTransition() 方法將控制轉給 UIKit。讓我們來看看代碼的實際效果!
運行程序,點擊第1個香草圖片,看看你的動畫效果:
是的,它還不是10分完善。但當你修改了這些瑕疵,你的動畫就會同你想象的1模1樣!
當前你的動畫是從左上角開始;由于 originFrame 默許的 origin 是(0,0)——你并沒有修改過這個值。
打開 ViewController.swift 在 animationController(forPresented:) 頭部加入:
transition.originFrame =
selectedImage!.superview!.convert(selectedImage!.frame, to: nil)
transition.presenting = true
selectedImage!.isHidden = true
將 transition 的 originFrame 設置為 selectedImage 即你剛剛點擊的圖片的 frame。然后將 presenting 設置為 true,在動畫期間隱藏所選圖片。
運行程序,點擊列表中的不同香草,你會看到:
剩下來的事情就是解散詳情頁面。實際上大部份工作都已在 animator 中做完了——轉換動畫中的代碼中開始、結束 frame 都已設置正確,你最后的工作就是在顯現和解散時適時地播放動畫。開心吧?
打開 ViewController.swift 修改 animationController(forDismissed:) 方法為:
transition.presenting = false
return transition
這將告知 animator 對象,你將解散1個 view controller,這樣動畫代碼會以正確的方式進行。
運行程序,點擊1張香草圖片然后點擊屏幕任何地方解散它:
轉換動畫看起來沒甚么問題,但請注意,你選擇的香草從 scroll view 中消失了!當你解散細節頁面時,你需要讓所點擊的圖片重新顯示。
打開 PopAnimator.swift 添加1個新的閉包屬性:
var dismissCompletion: (()->Void)?
這將允許解散動畫完成時履行你傳入的代碼。
然后,找到 animateTransition() 方法在完成塊中,在調用 completeTransition() 之前加入:
if !self.presenting {
self.dismissCompletion?()
}
當解散完成,調用 dismissCompletion —— 這里恰好可以顯示原來的圖片。
打開 ViewController.swift 在 viewDidLoad() 中加入:
transition.dismissCompletion = {
self.selectedImage!.isHidden = false
}
這里當轉換動畫完成重新顯示了原來的圖片,以替換詳情頁面。
運行程序,體驗轉換動畫,包括顯現和解散。現在,香草不會在平白無故消失了!
注意: 這部份內容是可選的。如果你對裝備方向改變不感興趣的話,請跳到挑戰部份。
你可以將裝備方向改變看成是1種顯現,從1個 view controller 轉換到它自己,僅僅是 size 不同。
iOS 8 中出現的 viewWillTransition(to size:coordinator:)方法,允許你以1種簡單直白的方式處理裝備方向的變化。你不再需要為橫屏豎屏分別設計不同的布局,相反,你只需改變 view controller 的視圖 size。
打開 ViewController.swift ,實現 viewWillTransition(to:with:) 方法:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
}
第1個參數 size 通知你 view controller 當前正在轉換到哪一個 size。第2個參數 coordinator 是1個 transition coordinator 對象,通過它可以訪問該轉換的許多屬性。
當橫屏的時候,你需要做的僅僅是下降 app 背景圖片的 alpha 值,提高文字的可讀性。
在 viewWillTransitionToSize 加入:
coordinator.animate(
alongsideTransition: {context in
self.bgImage.alpha = (size.width>size.height) ? 0.25 : 0.55
},
completion: nil
)
animate(alongsideTransition:) 允許你在旋屏進程中同時履行你指定的動畫,也就是在 UIKit 履行默許旋屏動畫的同時。
你的動畫塊會收到1個 transitionging 上下文,這和你在顯現 view controller 時使用的上下文是1樣的。這里,你沒有 from 和 to 視圖控制器了,由于它們是同1個,但你可以取得比如動畫時長等屬性。
在動畫塊中,我們判斷目標 size 的寬度是不是大于高度,如果是,下降背景圖的 alpha 值為 0.25。這將使橫屏下的背景變淡。如果是豎屏模式,alpha 值設為 0.55。
運行程序,旋轉裝備(如果是摹擬器,按 command+左箭頭),查看實際效果。
你將看到當旋轉到橫屏時背景變暗。這使得長文本更容易瀏覽。
如果你點擊圖片,你會注意到動畫有點亂。由于屏幕旋轉為橫屏后,圖片依然是豎向的大小。在原始圖片和拉伸至全屏的圖象之間的轉換其實不流暢。
不要擔心——你有1個新方法 viewWillTransition(to:with:) 能夠解決這個問題。
ViewController 有1個成員方法叫 positionListItems(),它負責香草圖片的大小和位置。這個方法在 app 1啟動時,被 viewDidLoad()方法所調用。
在 animate(alongsideTransition:) 方法的動畫塊中,在設置 alpha 值以后加入以下代碼:
self.positionListItems()
這將在裝備旋轉后改變香草圖片的 size 和位置。當屏幕完成旋轉后,香草圖片也會被重新改變大小:
由于這些圖片都已有了1個橫屏布局,因此你的轉換動畫就可以正常運行了。試試看!
從這里下載終究完成的項目。
這里,你可以對這個轉換進行大量的改進。例如,這些點子:
希望你喜歡本教程,如果有任何問題和建議,請在下面留言!