授權轉載,作者:ZeroJ(Github)
前言
正如標題所示,iOS開發中,自定義轉場的過渡動畫確實是必須要了解的,在iOS7之後實現也是很簡單的。如果會使用它,可以實現很多比較實用的功能。比如:
如果覺得系統的UIAlertController不能滿足需求, 那麼你可以使用自定義轉場過渡動畫的方式來實現彈出自定義的控制器(同時實現比較實用的動畫效果)。
系統默認的present是從下方彈出控制器, 可以通過自定義轉場過渡動畫的方式來自定義切換頁面的動畫
利用手勢實現tabbarController滑動切換頁面
利用手勢實現navigationController全屏返回的功能
......
本篇中首先介紹自定義present/dismiss的轉場動畫的方式 Demo地址
最終效果如下
一、在iOS7以後Apple提供了很方便的接口來實現自定義轉場動畫, 使用起來很是簡單方便,在實現過程中會接觸到三個對象。
Delegate: 一個繼承自NSObject的代理, 並且需要遵守相關的協議, 用來指定動畫中需要的其他兩個對象(下面提到的兩個), 需要遵守相關的協議如下
(UIViewControllerTransitioningDelegate -- 自定義present/dismiss的時候)
UINavigationControllerDelegate --- 自定義navigationController轉場動畫的時候
UITabBarControllerDelegate --- 自定義tabbarController轉場動畫的時候
......
UIViewControllerAnimatedTransitioning: 這個協議中提供了接口, 遵守這個協議的對象實現動畫的具體內容
UIViewControllerInteractiveTransitioning: 這個協議中提供了手勢交互動畫的接口, 不過, 我們大多都是使用它的一個子類UIPercentDrivenInteractiveTransition來更簡單的實現手勢交互動畫
二、了解UIViewControllerTransitioningDelegate
這個代理需要提供兩種類型的對象給系統來實現自定義動畫, 如果沒有提供, 將會使用系統默認的動畫效果
第一種類型對象是遵守UIViewControllerAnimatedTransitioning協議的對象
// 自定義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的對象
// 自定義交互動畫(手勢, 或者重力感應...)需要提供的遵守UIViewControllerInteractiveTransitioning對象 optional public func interactionController(forDismissal animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?
三、了解UIViewControllerAnimatedTransitioning
這個協議是上面提到的代理來獲取到具體的動畫操作的
遵守這個協議的對象來只需要實現兩個必須的方法
// 通過這個方法獲取到動畫執行的時間 public func transitionDuration(_ transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval // 在這個方法中通過獲取到源控制器和目標控制器等來執行動畫 // 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 是實現了
UIViewControllerInteractiveTransitioning這個協議的
我們使用UIPercentDrivenInteractiveTransition可以簡單的
通過調用提供的幾個函數來執行具體的動畫
(會調用UIViewControllerAnimatedTransitioning裡面實現的動畫)
一般可以通過繼承(也可不繼承)它來實現可交互動畫
在子類中通過添加手勢(或者其他方式)到相應的view上面, 在手勢的響應方法
中根據不同的手勢狀態來進行不同的交互動畫的操作, 一般使用到如下三個函數
// 更新動畫進度 public func update(_ percentComplete: CGFloat) // 取消交互動畫 public func cancel() // 完成交互動畫 public func finish()
五、了解UIViewControllerContextTransitioning
在UIViewControllerAnimatedTransitioning協議的
實現具體動畫的函數中
func animateTransition(_ transitionContext:UIViewControllerContextTransitioning)
我們會接觸到UIViewControllerContextTransitioning
這個接口用來提供切換上下文給開發者使用,包含了從哪個VC到哪個VC等
各類信息, 我們可以很方便的獲取到源控制器和目標控制器...很多我們需要的屬性
* 使用viewControllerForKey: 獲取到源控制器和目標控制器
* 使用containerView獲取到當前的containerView, 將要執行動畫的view都在這個containerView上進行
* 使用viewForKey: 獲取到將要添加或者移除的view(一般是控制器的view)
* 使用finalFrameForViewController:獲取到將要添加或者移除的view的最終frame
* 注意 'from' -> 指的的當前正在屏幕上顯示的控制器(present和dismiss的時候是不一樣的)
六、自定義present/dismiss動畫的系統調用過程
首先設置controller的代理transitioningDelegate為我們自定義的, 如果我們的代理裡面沒有提供上面所需要的對象, 那麼將會使用系統默認的
prenting動畫執行過程
UIKit首先會調用代理的
animationControllerForPresentedController:presentingController:sourceController:方法取得自定義的動畫對象
UIKit接著調用代理的 interactionControllerForPresentation: 方法看是否支持交互性動畫, 如果返回nil表示不支持
UIKit接著調用代理的 transitionDuration: 方法獲取動畫執行的時間
如果是不可交互的動畫UIKit會調用代理的animateTransition:方法來執行真正的動畫,
如果是可交互的動畫, UIKit會調用代理的startInteractiveTransition:方法開始動畫
接著是執行動畫的操作, 並且等待代理調用completeTransition:結束動畫(所以我們一定需要在動畫執行完畢後調用這個方法, 告訴系統我們的動畫執行完畢或者中途取消了)
dismiss動畫執行過程和上面只有第一步和第二步調用的代理方法不一樣
例如第一步調用(animationControllerForDismissedController:), 其他是相同的過程
七、下面以自定義present/dismiss動畫過程示例上面提到的各種用法(注意: 使用的swift3.0 xcode8, 如果是使用oc或者swift低版本的朋友請對應轉換相應的語法)
首先新建一個CustomAnimator繼承自NSObject, 並且遵守UIViewControllerAnimatedTransitioning協議, 來處理動畫的實現
class CustomAnimator:NSObject, UIViewControllerAnimatedTransitioning {
然後實現這個協議中必須的兩個方法來實現具體的動畫
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不一定是對應controller.view toView = transitionContext.view(forKey: UITransitionContextToViewKey)! fromView = transitionContext.view(forKey: UITransitionContextFromViewKey)! } else { // Apple文檔中提到不要直接使用這種方法來獲取fromView和toView toView = toVc.view fromView = fromVc.view } // 添加toview到最上面(fromView是當前顯示在屏幕上的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的下面便於執行動畫 // 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(可交互的時候會發生) toView.removeFromSuperview() } // 通知系統動畫是否完成或者取消了 transitionContext.completeTransition(!cancelled) } } }
接著新建一個CustomDelegate繼承自NSObject,並且遵守
UIViewControllerTransitioningDelegate協議, 來實現動畫的代理的工作
class CustomDelegate: NSObject, UIViewControllerTransitioningDelegate
接著實現需要自定義的相應的方法, 並且返回所需的執行對象
class CustomDelegate: NSObject, UIViewControllerTransitioningDelegate { private lazy var customAnimator = CustomAnimator() // 提供present的時候使用到的動畫執行對象 func animationController(forPresentedController presented: UIViewController, presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? { return customAnimator } // 提供dismiss的時候使用到的動畫執行對象 func animationController(forDismissedController dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { return customAnimator } }
到這裡為止就已經實現了自定義的不可交互的轉場動畫, 可以使用了, 效果和我們圖片示例的一樣
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() // 設置代理為我們自定義的 testVc.transitioningDelegate = deletage // 彈出控制器 present(testVc, animated: true, completion: nil) }
然後我們添加可交互的對象, 首先新建 Interactive:繼承自
UIPercentDrivenInteractiveTransition
class Interactive: UIPercentDrivenInteractiveTransition
接著添加手勢, 並且在手勢處理過程中根據不同的手勢狀態執行不同的操作
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) } } // 是否執行交互動畫 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(需要設置animated: true) isInteracting = true // dimiss dismissedVc.dismiss(animated: true, completion: nil) case .changed: // 手勢改變狀態, 計算動畫的進度 if isInteracting {// 開始執行交互動畫的時候才設置為非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裡面增加實現可交互動畫的執行對象和接口
// 注意在present接口裡面設置了 // interactive.dismissedVc = presented private lazy var interactive = Interactive() // 提供dismiss的時候使用到的可交互動畫執行對象 func interactionController(forDismissal animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { // 因為執行自定義動畫會先調用這個方法, 如果返回不為nil, 那麼將不會執行非交互的動畫!! // 所以isInteracting只有在手勢開始的時候才被設置為true // 返回nil便於不是可交互的時候就直接執行不可交互的動畫 return interactive.isInteracting ? interactive : nil }
就是這樣就實現了利用手勢滑動返回的可交互動畫, 現在運行, 將會看到圖片的示例效果, 還是很簡單?!!!!
這裡以自定義present/dismiss為例詳細的介紹了自定義轉場動畫的使用, 那麼到現在, 你是可以很自由的去實現各種需要的自定義動畫(navigationController, tabBarController...), 並且增加各種交互動畫(滑動, 捏合, 甚至設備搖晃...), 希望你會很愉快的使用:Demo地址,歡迎關注, 歡迎star。