前言
正如標(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)
最終效果如下
一` 在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)用過程
- 首先設(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