歷時(shí)5天從各種英文教程中學(xué)習(xí)到的過渡動(dòng)畫,是一個(gè)很難忘的探索經(jīng)歷
比較好的參考文章自定義UIViewController過渡入門 ,動(dòng)畫入門。
轉(zhuǎn)場(chǎng)方式
首先讓我們來了解iOS轉(zhuǎn)場(chǎng)的方式:
- UINavigationController push/pop UIViewController導(dǎo)航欄的轉(zhuǎn)場(chǎng)
- UITabBarController 切換Tab的轉(zhuǎn)場(chǎng)
- present/dismiss 模態(tài)的方式轉(zhuǎn)場(chǎng)
這是iOS提供的3種基本轉(zhuǎn)場(chǎng)方式,默認(rèn)的轉(zhuǎn)場(chǎng)方式轉(zhuǎn)場(chǎng)風(fēng)格有限。例如模態(tài)轉(zhuǎn)場(chǎng)中,盡管有modalPresentationStyle和modalTransitionStyle關(guān)于展現(xiàn)風(fēng)格和過渡風(fēng)格的設(shè)置,但是轉(zhuǎn)場(chǎng)仍是死板從底部彈出。這并不能滿足我們?cè)谲浖_發(fā)的需求,側(cè)邊欄、頂部欄的動(dòng)畫效果都無法很好地實(shí)現(xiàn)。在iOS 7.0之后Apple提供一套完整的自定義過渡動(dòng)畫的API,為各種轉(zhuǎn)場(chǎng)動(dòng)畫的實(shí)現(xiàn)帶了無限可能。
這里主要介紹模態(tài)轉(zhuǎn)場(chǎng)的自定義動(dòng)畫。
頂部欄的動(dòng)畫效果:

Modal 轉(zhuǎn)場(chǎng)
模態(tài)轉(zhuǎn)場(chǎng)分為非交互式轉(zhuǎn)場(chǎng)和交互式轉(zhuǎn)場(chǎng)。
非交互式轉(zhuǎn)場(chǎng)也就是普通轉(zhuǎn)場(chǎng),轉(zhuǎn)場(chǎng)的動(dòng)畫無法交互,不能在動(dòng)畫的過程中終止轉(zhuǎn)場(chǎng)。
交互式轉(zhuǎn)場(chǎng)能通過手指觸摸屏幕通過滑動(dòng)體驗(yàn)過渡動(dòng)畫的進(jìn)行,并能終止動(dòng)畫過程。
過渡動(dòng)畫API
我們定義:如果視圖控制器Apresent之后展示視圖控制器B。在后文中,源視圖控制器為fromVC,目標(biāo)視圖控制器為toVC。
| 狀態(tài) | 視圖控制器A | 視圖控制器B |
|---|---|---|
| Present | 源視圖控制器 | 目標(biāo)視圖控制器 |
| Dissmiss | 目標(biāo)視圖控制器 | 源視圖控制器 |
transitioningDelegate 過渡代理
每個(gè)視圖控制器UIViewController都有一個(gè)transitioningDelegate屬性,該代理需遵循UIViewControllerTransitioningDelegate協(xié)議,提供相關(guān)動(dòng)畫控制器。
每當(dāng)您顯示或關(guān)閉視圖控制器時(shí),UIKit都會(huì)要求其過渡代理使用動(dòng)畫控制器。要將默認(rèn)動(dòng)畫替換為您自己的自定義動(dòng)畫,必須實(shí)現(xiàn)過渡代理,并使其返回適當(dāng)?shù)膭?dòng)畫控制器。
AnimationController 動(dòng)畫控制器
過渡代理在present/dismiss時(shí)返回相應(yīng)的動(dòng)畫控制器。動(dòng)畫控制器是過渡動(dòng)畫的核心。它完成了動(dòng)畫過渡的“繁重工作”。
TransitioningContext 過渡語境
過渡語境在過渡過程中實(shí)現(xiàn)并起著至關(guān)重要的作用:它封裝了有關(guān)過渡中涉及的視圖和視圖控制器的信息。過渡語境輔助動(dòng)畫控制器實(shí)現(xiàn)動(dòng)畫。
從圖中可以看出,自己并沒有實(shí)現(xiàn)此協(xié)議。UIKit會(huì)為您創(chuàng)建和配置過渡上下文,并在每次發(fā)生過渡時(shí)將其傳遞給動(dòng)畫控制器。
非交互式過渡動(dòng)畫的過程
以present過渡動(dòng)畫為例:
- 通過代碼或
StoryBoard segue觸發(fā)模態(tài)視圖present過程。 - UIKit向
toVC(目標(biāo)視圖控制器)請(qǐng)求其過渡代理。如果沒有,UIKIt將使用標(biāo)準(zhǔn)的內(nèi)置過渡。 - 然后,UIKit通過來向過渡代理請(qǐng)求動(dòng)畫控制器
animationController(forPresented:presenting:source:)。如果返回nil,則過渡將使用默認(rèn)動(dòng)畫。 - UIKit構(gòu)造過渡語境。UIKit通過調(diào)用向動(dòng)畫控制器詢問動(dòng)畫的持續(xù)時(shí)間
transitionDuration(using:)。UIKitanimateTransition(using:)在動(dòng)畫控制器上調(diào)用以執(zhí)行過渡的動(dòng)畫。 - 最后,動(dòng)畫控制器調(diào)用
completeTransition(_:)過渡上下文以指示動(dòng)畫已完成。
dimiss過渡動(dòng)畫的步驟幾乎相同。
在這種情況下,UIKit向fromVC視圖控制器(正在關(guān)閉的視圖控制器)請(qǐng)求其過渡代理,要求提供動(dòng)畫控制器animationController(forDismissed:)。
非交互式過渡動(dòng)畫需要提供的條件
- 設(shè)置過渡動(dòng)畫代理。設(shè)置(目標(biāo)視圖控制器)的
transitioningDelegate屬性,即設(shè)置過渡動(dòng)畫代理對(duì)象,該代理對(duì)象遵循UIViewControllerTransitioningDelegate協(xié)議,實(shí)現(xiàn)forPresented和forDismissed兩個(gè)方法,分別提供present和dismiss的視圖控制器實(shí)例。 - 創(chuàng)建動(dòng)畫控制器。創(chuàng)建present和dismiss的動(dòng)畫控制器,實(shí)現(xiàn)動(dòng)畫持續(xù)時(shí)間和構(gòu)造動(dòng)畫方法。
這里我們完成一個(gè)簡(jiǎn)單的從左到右的過渡動(dòng)畫。
1. 構(gòu)建present/dimiss場(chǎng)景
這里不再講述構(gòu)建過程,無論是stroyboard還是純代碼都是很好完成的。這里為了方便,使用storyboard完成。
左視圖控制器為:ViewController
右視圖控制器為:LeftViewController
2. 創(chuàng)建Animation Controller
Animation Controller是自定義過渡動(dòng)畫的核心對(duì)象。
AnimationControlle繼承至NSObject,遵循UIViewControllerAnimatedTransitioning協(xié)議,實(shí)現(xiàn)兩個(gè)required方法。
class AnimationController: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
//要求提供動(dòng)畫師對(duì)象的動(dòng)畫持續(xù)時(shí)間屬性
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
//過渡動(dòng)畫的實(shí)現(xiàn)效果,也是自定義過渡動(dòng)畫的核心方法,需要構(gòu)建動(dòng)畫的實(shí)現(xiàn)。
}
}
這里貼出Present狀態(tài)下的AnimationController的代碼,Dismiss狀態(tài)與其類似(動(dòng)畫過程執(zhí)行反過程)。
import UIKit
class PresentAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.6
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// 1
guard let _ = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to) else {
return
}
// 2
let containerView = transitionContext.containerView
containerView.addSubview(toVC.view)
let duration = transitionDuration(using: transitionContext)
toVC.view.frame = CGRect(x: -ScreenWidth, y: 0, width: ScreenWidth, height: ScrennHeight)
// 3
UIView.animate(withDuration: duration, animations: {
toVC.view.frame = CGRect(x: 0, y: 0, width: ScreenWidth, height: ScrennHeight)
}) { (_) in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
在第一個(gè)transitionDuration(using:)方法中設(shè)置動(dòng)畫的持續(xù)時(shí)間。
在第二個(gè)transitionDuration(using:)方法中構(gòu)建動(dòng)畫。
- 獲取過渡動(dòng)畫所需的視圖控制器及snapshot。從過渡語境中
transitionContext.viewController我們可以獲取源視圖控制器fromVC和目標(biāo)視圖控制器toVC,過渡語境封裝了設(shè)計(jì)的視圖控制器的信息,極大地幫助我們處理視圖控制器的動(dòng)畫轉(zhuǎn)化。還可以獲取fromVC和toVC的snapshot(屏幕快照),來構(gòu)造更加復(fù)雜和優(yōu)秀的動(dòng)畫。 - 管理過渡語境的容器視圖 --- containerView和視圖動(dòng)畫的位置初始化。UIKit將整個(gè)過渡封裝在容器視圖中,以簡(jiǎn)化視圖層次結(jié)構(gòu)和動(dòng)畫的管理,容器視圖負(fù)責(zé)管理
fromVC.view和toVC.view。由UIKit創(chuàng)建的容器視圖僅包含fromVC視圖。您必須添加任何其他將參與過渡的視圖。
addSubview(_:)將新視圖置于視圖層次結(jié)構(gòu)中的所有其他視圖之前,因此添加子視圖的順序很重要。
- 設(shè)置動(dòng)畫效果。動(dòng)畫有兩種實(shí)現(xiàn)方法,一種是基礎(chǔ)動(dòng)畫
animate,另一種是關(guān)鍵幀動(dòng)畫animateKeyframes。這里只是簡(jiǎn)單地將fromVC.view從屏幕的左邊界外移動(dòng)到屏幕中。<font color = red>注意:在動(dòng)畫完成后需要調(diào)用completeTransition(_:)通知UIKit動(dòng)畫已完成。這將確保最終狀態(tài)是一致的。</font>
3.設(shè)置過渡動(dòng)畫代理
設(shè)置(目標(biāo)視圖控制器)destinationVC的modalPresentationStyle為枚舉屬性custom,過渡動(dòng)畫的代理為self即(源視圖控制器)ViewController。
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destinationVC = segue.destination as? LeftViewController {
destinationVC.modalPresentationStyle = .custom
destinationVC.transitioningDelegate = self
}
}
然后在(源視圖控制器)添加擴(kuò)展,遵循UIViewControllerTransitioningDelegate協(xié)議,實(shí)現(xiàn)forPresented和forDismissed方法。
extension ViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
//目標(biāo)VC是presented,源VC是presenting
guard let _ = presented as? LeftViewController, let _ = presenting as? ViewController else {
return nil
}
return PresentAnimationController()
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
guard let _ = dismissed as? LeftViewController else {
return nil
}
return DismissAnimationController()
}
}
這樣就構(gòu)建好了一個(gè)簡(jiǎn)單、基礎(chǔ)的自定義轉(zhuǎn)場(chǎng)過渡動(dòng)畫。
交互式過渡動(dòng)畫
交互式動(dòng)畫會(huì)使使用者的動(dòng)畫體驗(yàn)更自然舒適而不顯得尖銳,給控制動(dòng)畫過程留下了余地。iOS原生APP設(shè)置中便有這樣的交互式動(dòng)畫的例子。
過渡動(dòng)畫的進(jìn)度跟隨手指的滑動(dòng)來啟動(dòng)/終止轉(zhuǎn)場(chǎng)動(dòng)畫,這樣可以帶來良好的用戶交互體驗(yàn)。
交互式控制器的工作方式
交互控制器通過加快,減慢甚至反轉(zhuǎn)過渡的過程來響應(yīng)觸摸事件或編程輸入。為了啟用交互式轉(zhuǎn)換,轉(zhuǎn)換代理必須提供一個(gè)交互控制器。您已經(jīng)制作了過渡動(dòng)畫。在過渡動(dòng)畫的基礎(chǔ)上,Apple將交互式動(dòng)畫封裝成交互控制器將響應(yīng)手勢(shì)來管理此動(dòng)畫,而不是讓其像視頻一樣播放。Apple提供了現(xiàn)成的UIPercentDrivenInteractiveTransition類,通過繼承該類來創(chuàng)建自己的交互式控制器。
1. 創(chuàng)建交互式控制器
我們?cè)谥斑^渡動(dòng)畫的基礎(chǔ)上構(gòu)建交互式過渡動(dòng)畫,我們首先需要?jiǎng)?chuàng)建交互式控制器。這里貼出顯示交互式控制器PresentInteractionController的代碼,繼承封裝好的UIPercentDrivenInteractiveTransition類。
class PresentInteractionController: UIPercentDrivenInteractiveTransition {
var interactionInProgress = false
private var shouldCompleteTrantision = false
private weak var viewController: UIViewController!
private weak var toViewController: UIViewController!
init(viewController: UIViewController, toViewController: UIViewController) {
super.init()
self.viewController = viewController
self.toViewController = toViewController
prepareGestureRecognizer(in: self.viewController.view)
}
private func prepareGestureRecognizer(in view: UIView) {
let gesture = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleGsture(_:)))
gesture.edges = .left
view.addGestureRecognizer(gesture)
}
@objc func handleGsture(_ gestureRecognizer: UIScreenEdgePanGestureRecognizer) {
let translation = gestureRecognizer.translation(in: gestureRecognizer.view?.superview)
var progress = translation.x / 200
progress = CGFloat(fminf(fmaxf(Float(progress), 0.0), 1.0))
switch gestureRecognizer.state {
case .began:
interactionInProgress = true
viewController.present(toViewController, animated: true, completion: nil)
case .changed:
shouldCompleteTrantision = progress > 0.5
update(progress)
case .cancelled:
interactionInProgress = false
cancel()
case .ended:
interactionInProgress = false
if shouldCompleteTrantision {
finish()
} else {
cancel()
}
default:
break
}
}
}
-
interactionInProgressBool屬性,表示交互式場(chǎng)景是否在發(fā)生。 -
shouldCompleteTrantisionBool屬性,表示是否應(yīng)該終止過渡動(dòng)畫。用于內(nèi)部管理過渡。 -
viewController和toViewController,獲取源視圖控制器和目標(biāo)視圖控制器的引用,用于管理過渡某狀態(tài)present視圖控制器,達(dá)到交互式控制器與動(dòng)畫控制器相聯(lián)系的作用。 -
prepareGestureRecognizer(in:)為源視圖添加屏幕手勢(shì)的方法,這里的交互式動(dòng)畫為從左往右present出VC,所以為源視圖添加屏幕相應(yīng)在.left的手勢(shì)。 -
handleGsture(_:)為相應(yīng)手勢(shì)變化從而改變過渡動(dòng)畫狀態(tài)的方法。通過聲明局部變量以跟蹤滑動(dòng)進(jìn)度translation,根據(jù)translation在視圖中獲取并計(jì)算過渡進(jìn)度progress。手勢(shì)開始時(shí),您將設(shè)置interactionInProgress為true并觸發(fā)prsent視圖控制器。手勢(shì)移動(dòng)時(shí),調(diào)用update(_:)更新過渡進(jìn)度。這是一種根據(jù)UIPercentDrivenInteractiveTransition您傳入的百分比移動(dòng)過渡的方法。如果取消手勢(shì),則更新interactionInProgress并取消過渡。一旦動(dòng)作已經(jīng)結(jié)束,您使用的過渡進(jìn)度來決定cancel()或finish()。
2. 控制器聯(lián)系
(源)視圖控制器與交互控制器相聯(lián)
在viewController中添加以下屬性:
var presentInteractionController: PresentInteractionController?
并在viewDidLoad()方法中初始化屬性:
presentInteractionController = PresentInteractionController(viewController: self, toViewController: leftViewController)
交互控制器與動(dòng)畫控制器相聯(lián)
在PresentAnimationController中添加以下屬性:
let interactionController: PresentInteractionController?
并添加init方法:
init(interactionController: PresentInteractionController?) {
self.interactionController = interactionController
}
3. 實(shí)現(xiàn)過渡代理協(xié)議方法
動(dòng)畫控制器forPresented方法中修改PresentAnimationController對(duì)象的創(chuàng)建。
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
//展現(xiàn)VC是presented,源VC是presenting
guard let _ = presented as? LeftViewController, let fromVC = presenting as? ViewController else {
return nil
}
return PresentAnimationController(interactionController: fromVC.presentInteractionController)
}
添加以下方法:
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
guard let animator = animator as? PresentAnimationController,
let interactionController = animator.interactionController,
interactionController.interactionInProgress else {
return nil
}
return interactionController
}
這首先檢查所涉及的動(dòng)畫控制器是否為PresentAnimationController。如果是這樣,它將獲取關(guān)聯(lián)的交互控制器的引用,并驗(yàn)證用戶交互正在進(jìn)行中。如果不滿足這些條件中的任何一個(gè),它將返回nil以便轉(zhuǎn)換將繼續(xù)進(jìn)行而不會(huì)發(fā)生交互。否則,它將交互控制器交還給UIKit,以便它可以管理過渡。
dismiss狀態(tài)的交互式控制器的方法也是相近的,最終實(shí)現(xiàn)效果如下:

以上便是自定義過渡動(dòng)畫的全部?jī)?nèi)容,讀者學(xué)習(xí)完后可以學(xué)一些進(jìn)階動(dòng)畫,參考一些優(yōu)秀App的轉(zhuǎn)場(chǎng)動(dòng)畫,進(jìn)而創(chuàng)建自己過渡動(dòng)畫,從而獲得良好的用戶體驗(yàn)。