系統學習iOS動畫之四:視圖控制器的轉場動畫

本文是我學習《iOS Animations by Tutorials》 筆記中的一篇。 文中詳細代碼都放在個人Github上 andyRon/LearniOSAnimationshtml

以前學習了視圖動畫、圖層動畫、自動佈局動畫等。這個部分讓視野更大一點,學習整個視圖控制器的動畫,視圖控制器的轉場動畫(View Controller Transition Animations)ios

iOS中最容易識別的動畫之一是將新視圖控制器推入導航堆棧的動畫,當咱們想讓APP有本身的特點,自定義轉場動畫是很是好的方式。git

在本文,將學習如何使用動畫建立本身的自定義視圖控制器轉換。github

預覽:swift

17-視圖控制器轉場和屏幕旋轉轉場 瞭解如何經過自定義動畫轉場呈現視圖控制器 - 做爲獎勵,您將建立動畫轉場以處理設備方向更改。安全

18-導航控制器轉場bash

19-交互式導航控制器轉場閉包

17-視圖控制器轉場和屏幕旋轉轉場

不管是呈現 照相機視圖控制器、地址簿仍是自定義的模態屏幕,每次都調用相同的UIKit方法:present(_:animated:completion:)。 此方法將當前屏幕「放棄」,而後跳到另外一個視圖控制器。ide

下圖呈現一個「New Contact」視圖控制器向上滑動以覆蓋當前視圖(聯繫人列表),這是默認的動畫方式:函數

在本章中,學習建立本身的自定義演示控制器動畫,以替換默認的動畫,並使本章的項目更加生動。

開始項目

本章開始項目是一個新項目,叫BeginnerCook

這個開始項目能夠簡單歸納 以下,ViewController中包括一個背景圖UIImageView,一個標題UILabel,一個文本視圖UITextView,下面是一個能夠左右移動的UIScrollView。這個UIScrollView裏會用代碼加入一些香草(herb)圖片,點擊圖片會跳轉到另個展現詳情的視圖控制器HerbDetailsViewController,這個轉場是標準的從下到上的垂直覆蓋轉場動畫。

開始項目預覽一下:

自定義轉場的原理

UIKit實現自定義轉場動畫是經過代理模式完成的。所以首先須要讓ViewController遵照UIViewControllerTransitioningDelegate協議。

每次呈現新的視圖控制器時,UIKit都會詢問其代理是否要使用自定義轉場。如下是自定義轉場動畫的第一步:

image-20181202172719920

須要實現animationController(forPresented:presenting:source:)方法,這個方法若是返回nil,則進行默認的轉場動畫,若是返回時遵照UIViewControllerAnimatedTransitioning協議的對象,則將這個對象做爲自定義轉場的Animator(能夠翻譯爲動畫師)。

在UIKit使用自定義Animator以前,還須要一些步驟:

image-20181202172932834

transitionDuration(using:)返回動畫持續時間。

animateTransition(using:)方法時實際動畫代碼所在的地方。在這個方法中能夠訪問屏幕上的當前視圖控制器以及將要顯示的新視圖控制器,能夠本身根據須要淡化,縮放,旋轉等操做現有視圖和新視圖。

下面開始實現自定義轉場!💪

實現轉場代理

新建一個NSObject子類PopAnimator(就是以前提到的Animator),並遵照協議 UIViewControllerAnimatedTransitioning 。並在這個動畫類中添加兩個函數的存根:

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
    return 0
}
    
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
}
複製代碼

ViewController遵照UIViewControllerTransitioningDelegate協議:

extension ViewController: UIViewControllerTransitioningDelegate {
    
}
複製代碼

didTapImageView(_:)中的present(herbDetails, animated: true, completion: nil)前添加:

herbDetails.transitioningDelegate = self
複製代碼

如今,每次在屏幕上顯示詳情頁的視圖控制器時,UIKit都會向ViewController詢問動畫對象。 可是,目前仍然沒有實現任何UIViewControllerTransitioningDelegate中的相關方法,所以UIKit仍將使用默認轉換。

ViewController中建立動畫屬性:

let transition = PopAnimator()
複製代碼

實現呈現時動畫的協議方法:

func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {  
    return transition
}
複製代碼

實現解除(dismiss)時動畫的協議方法:

func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    return transition
}
複製代碼

如今點擊香草🌿圖片時,沒有反應,這是由於,把默認的轉場動畫修改爲了自定義,但自定義動畫目前是空的。

建立轉場動畫師

PopAnimator添加:

let duration = 1.0
var presenting = true
var originFrame = CGRect.zero
複製代碼

duration 是動畫持續時間。

presenting 是用判斷當前是呈現仍是解除

originFrame用來存儲用戶點擊的圖像的原始 frame —— 呈現動畫就是須要它從原始frame到全屏圖像frame,對應的解除動畫正好相反。

用如下內容替換transitionDuration()中的代碼:

return duration
複製代碼

設置轉場動畫的上下文

是時候爲animateTransition(using:)添加代碼了。 此方法有一個類型爲UIViewControllerContextTransitioning的參數,經過該參數能夠訪問轉場的相關參數和視圖控制器。

在開始寫動畫代碼以前,瞭解動畫上下文其實是什麼很重要。

當兩個視圖控制器之間的轉場開始時,現有視圖將添加到轉場容器視圖(transition container view)中,而且新視圖控制器的視圖已建立但還沒有可見,以下所示:

image-20181202105011119

所以,如今的任務是將新視圖添加到animateTransition()中的轉場容器中,以特定動畫將其顯示,若有須要也是特定動畫的方式解除舊視圖。

默認狀況下,轉場動畫完成後,舊視圖將從轉場容器中刪除。

image-20181202105026911

下面👇先實現簡單的轉場動畫。

淡出轉場

得到動畫將在其中進行的容器視圖,而後您將獲取新視圖並將其存儲在toView中,在animateTransition()中添加:

let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!
複製代碼

view(forKey:)viewController(forKey:)兩個方法很是相似,分別得到轉場動畫對應的視圖和視圖控制器。

繼續在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能夠自由地結束視圖控制器轉場。

目前的效果就是:

pop轉場

上面的fade效果不是最終想要的,把animateTransition()中的代碼替換爲:

let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!
let herbView = presenting ? toView : transitionContext.view(forKey: .from)!
複製代碼

containerView是動畫將存在的地方,而toView是要呈現的新視圖。 若是是呈現presentingtrue),herbViewtoView,不然將從上下文中獲取。 對於呈現解除herbView將始終是表現動畫的視圖。 當呈詳細頁的控制器視圖時,它將逐漸佔用整個屏幕。 當被解除時,它將縮小到圖像的原始幀。

在上面代碼後添加:

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
複製代碼

initialFramefinalFrame分別是初始和最終動畫的framexScaleFactoryScaleFactor分別是x軸和y軸上視圖變化的比例因子(scale factor)

繼續在上面代碼後添加:

let scaleTransform = CGAffineTransform(scaleX: xScaleFactor, y: yScaleFactor)
        
if presenting {
    herbView.transform = scaleTransform
    herbView.center = CGPoint(x: initialFrame.midX, y: initialFrame.midY)
    herbView.clipsToBounds = true
}
複製代碼

當須要呈現新視圖時,設置transform,而且定位(設置center

繼續在上面代碼後添加:

containerView.addSubview(toView)
containerView.bringSubview(toFront: herbView)
UIView.animate(withDuration: duration, delay: 0.0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.0, options: [], animations: {
    herbView.transform = self.presenting ? CGAffineTransform.identity : scaleTransform
    herbView.center = CGPoint(x: finalFrame.midX, y: finalFrame.midY)
}) { (_) in
    transitionContext.completeTransition(true)
}
複製代碼

首先將toView添加到容器中,並確保herbView位於頂部,由於這是動畫的惟一視圖。

而後,實現動畫,在這裏使用彈簧動畫。在動畫表達式中,能夠更改herbViewtransform和位置。在呈現時,將從底部的小尺寸變爲全屏;在解除時,將全屏縮小變爲原始圖像大小。

最後,您調用了completeTransition()告訴UIKit轉場動畫已經完成。

如今的效果:

動畫從左上角開始; 這是由於originFrame的默認值的原點是*(0,0)* 。

ViewController.swiftanimationController(forPresented:presenting:source:) 返回代碼前添加:

transition.originFrame = selectedImage!.superview!.convert(selectedImage!.frame, to: nil)
transition.presenting = true
selectedImage!.isHidden = true
複製代碼

這會將轉場動畫的originFrame設置爲selectedImageframe,並在動畫期間隱藏初始圖像。

目前的效果是初始小視圖轉場到全屏了,沒有問題,可是解除詳情頁時就有問題,詳情頁忽然就消失了:

解除轉場

剩下要作的就是解除詳細頁視圖的動畫。

ViewController.swiftanimationController(forDismissed:)中添加:

transition.presenting = false
return transition
複製代碼

上面的表明 transition對象也做爲解除轉場動畫使用。

轉場動畫看起來很棒,但解除詳細頁面後,原始的小尺寸的圖片消失了。下面就解決這個問題。

在類PopAnimator中添加一個閉包屬性,做爲解除動畫完成後處理:

var dismissCompletion: (()->Void)?
複製代碼

animateTransition(using:)transitionContext.completeTransition(true)以前添加(也就是通知UIKit轉場動畫結束以前,若是是解除動畫,就進行一些處理):

if !self.presenting {
    self.dismissCompletion?()
}
複製代碼

ViewController實現具體閉包內容,在viewDidLoad()中添加:

transition.dismissCompletion = {
    self.selectedImage!.isHidden = false
}
複製代碼

那麼,目前效果:

屏幕旋轉轉場

設備方向更改視爲從視圖控制器到其自身的轉場過程。

iOS 8中引入的viewWillTransition(to:with:)方法,用來提供了一種簡單直接的方法來處理設備方向的變化。 不須要構建單獨的縱向或橫向佈局,而只須要對視圖控制器視圖的大小進行更改。

ViewController中添加:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)

    coordinator.animate(alongsideTransition: { context in
                                              self.bgImage.alpha = (size.width > size.height) ? 0.25 : 0.55
                                             }, completion: nil)
}
複製代碼

第一個參數(size)指視圖控制器變換後的大小。 第二個參數(coordinator)是轉場協調對象,它能夠訪問許多轉場的屬性。

animate(alongsideTransition:completion:)容許指定本身的自定義動畫,與UIKit在更改方向時默認執行的旋轉動畫一塊兒執行。當設備橫向時,減小背景圖像的透明度,讓文本看上去更清晰,更容易閱讀。

運行,旋轉設備(模擬器中按Cmd +向左箭頭):

將屏幕旋轉爲橫向模式時,能夠清楚地看到背景變深。

如今上面的動畫看上去已經很不錯,但若是仔細觀看,會發現還有兩個問題,解除動畫時,全屏視圖到小視圖完成以前看到詳細視圖的文本;全屏視圖是直角,直到動畫要完成的最後一個才從直角忽然變到圓角。

平滑轉場動畫

糾正了細節視圖的文本在被解除時消失的問題。

animateTransition(using:)中的動畫(UIView.animate(...))開始前添加:

let herbController = transitionContext.viewController(forKey: presenting ? .to : .from) as! HerbDetailsViewController

if presenting {
    herbController.containerView.alpha = 0.0
}
複製代碼

animateTransition(using:)中的動畫閉包中添加:

herbController.containerView.alpha = self.presenting ? 1.0 : 0.0
複製代碼

圓角動畫

最後,爲詳情頁視圖的圖層角半徑設置動畫,使其與主視圖控制器中草本圖像的圓角相匹配。

animateTransition(using:)中的動畫閉包中添加:

herbView.layer.cornerRadius = self.presenting ? 0.0 : 20.0/xScaleFactor
複製代碼

爲了更方便的查看動畫,能夠把持續時間增大或用模擬器中滿動畫(Command + T)。

上面兩個修改後的效果:

18-導航控制器轉場

UINavigationController是iOS中爲數很少的內置應用導航解決方案之一。 將一個新的視圖控制器推入或彈出導航堆棧,這個過程自帶一個時尚的動畫。

上圖顯示了iOS如何將新視圖控制器推送到設置應用中的導航堆棧:新視圖從右側滑入以覆蓋舊視圖,新標題淡入,而舊標題淡出。

本章的自定義導航控制器轉場與前一章中構建自定義視圖控制器轉場的方式相似。

開始項目

本章開始項目是一個新項目,叫LogoReveal

點擊默認屏幕任意地方(MasterViewController),跳轉展現vacation packing list頁面(DetailViewController),RW Logo是經過UIBezierPath繪製的CAShapeLayer圖層。

自定義導航控制器轉場的原理

自定義導航控制器轉場的原理相似上一章節的自定義轉場的原理,一樣也能夠用兩個圖歸納:

image-20181203214052969

image-20181203214104467

導航控制器代理

首先須要新建一個Animator,新建一個NSObject子類RevealAnimator的類文件,並讓它遵照UIViewControllerAnimatedTransitioning協議:

class RevealAnimator: NSObject, UIViewControllerAnimatedTransitioning {

}
複製代碼

RevealAnimator中添加兩個屬性,而且實現UIViewControllerAnimatedTransitioning協議的兩個方法:

let animationDuration = 2.0
    var operation: UINavigationControllerOperation = .push
    
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return animationDuration
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) 
    {
        
    }
複製代碼

operationUINavigationControllerOperation類型的屬性,用於表示是在推送仍是彈出控制器。

用擴展的方式讓MasterViewController遵照UINavigationControllerDelegate協議:

extension MasterViewController: UINavigationControllerDelegate {
    
}
複製代碼

在調用任何segues或將某些內容推送到堆棧以前,須要在視圖控制器生命週期的早期設置導航控制器的代理。在MasterViewControllerviewDidLoad()中添加:

navigationController?.delegate = self
複製代碼

MasterViewController中建立Animator屬性:

let transition = RevealAnimator()
複製代碼

實現協議UINavigationControllerDelegate的方法navigationController():

func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    transition.operation = operation
    return transition
}
複製代碼

這是一個方法名稱很是長,參數有:

navigationController:當對象是多個導航控制器的委託時,這用來區分導航控制器,這不是太常見。

operation:這是一個枚舉UINavigationControllerOperation,能夠是.push.pop

fromVC:這是當前在屏幕上可見的視圖控制器,它一般是導航堆棧中的最後一個視圖控制器。

toVC:這是將轉場到的視圖控制器。

若是須要不一樣視圖控制器有不一樣轉場動畫,則能夠選擇返回不一樣的Animator。爲了簡化此項目,在推送或彈出轉場時,都返回RevealAnimator對象。

運行,點擊,導航欄有一個兩秒轉場,但其餘就沒有反應了,這是由於animateTransition()中尚未編寫任何代碼。

添加自定義顯示動畫

自定義轉場動畫的計劃相對簡單。 您只需在DetailViewController上爲蒙版設置動畫,使其看起來像RW徽標的透明部分,顯示底層視圖控制器的內容。 你將不得不處理圖層和一些動畫任務,可是到目前爲止你尚未完成任務。 對於像你這樣的動畫專業人士來講,建立轉場動畫將是一件輕鬆的事!

RevealAnimator中建立一個存儲動畫上下文的屬性:

weak var storedContext: UIViewControllerContextTransitioning?
複製代碼

再在animateTransition()中添加:

storedContext = transitionContext

let fromVC = transitionContext.viewController(forKey: .from) as! MasterViewController
let toVC = transitionContext.viewController(forKey: .to) as! DetailViewController

transitionContext.containerView.addSubview(toVC.view)
toVC.view.frame = transitionContext.finalFrame(for: toVC)
複製代碼

先獲取fromVC並將其轉換爲MasterViewController;而後,獲取toVC並轉換爲DetailViewController。 最後,只需將toVC.view添加到轉場容器視圖中,並將其frame設置爲transitionContext中的最終frame,這是詳情頁面在主屏幕上的最終位置。

將如下內容添加到animateTransition()中:

let animation = CABasicAnimation(keyPath: "transform")
animation.fromValue = NSValue(caTransform3D: CATransform3DIdentity)
animation.toValue = NSValue(caTransform3D:      CATransform3DConcat(CATransform3DMakeTranslation(0.0, -10.0, 0.0), CATransform3DMakeScale(150.0, 150.0, 1.0)))
複製代碼

這個動畫將logo的大小增長了150倍,並同時向上移動了一點。 爲何? logo的形狀不均勻,我但願後面的視圖控制器經過RW形狀的「孔」顯示。 將其向上移動意味着縮放圖像的底部將更快地覆蓋屏幕。

若是使用像圓形或橢圓形這種對稱的logo,就不會有這種問題。

如今將如下面代碼添加到animateTransition()以稍微優化動畫:

animation.duration = animationDuration
animation.delegate = self
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
複製代碼

這些都是前面章節的知識。

RevealAnimator目前還不是動畫代理,記得要讓RevealAnimator遵照CAAnimationDelegate 協議。

animateTransition()中添加圖層:

let maskLayer: CAShapeLayer = RWLogoLayer.logoLayer()
maskLayer.position = fromVC.logo.position
toVC.view.layer.mask = maskLayer
maskLayer.add(animation, forKey: nil)
複製代碼

效果:

優化細節

細看上面的效果,會發現動畫運行時,原來的logo還在那裏,下面解決這個問題。

animateTransition()中添加:

fromVC.logo.add(animation, forKey: nil)
複製代碼

運行後,沒有有原始的logo了:

還有一個稍微複雜一點的問題:在第一次推送轉場後,導航再也不工做了?

RevealAnimator中實現CAAnimationDelegateanimationDidStop(_:finished:)方法:

func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
    if let context = storedContext {
        context.completeTransition(!context.transitionWasCancelled)
        // reset logo
    }
    storedContext = nil
}
複製代碼

在方法結束時,只需將轉場上下文設置爲nil。

因爲顯示動畫在完成後不會自動刪除,所以須要本身處理。

使用如下內容替換位於animationDidStop()中的// reset logo

let fromVC = context.viewController(forKey: .from) as! MasterViewController

fromVC.logo.removeAllAnimations()
複製代碼

只須要在推送轉場期間屏蔽視圖控制器的內容,一旦視圖控制器完成轉場,就能夠安全地移除屏蔽。

接着上面的代碼t添加:

let toVC = context.viewController(forKey: .to) as! DetailViewController
toVC.view.layer.mask = nil
複製代碼

運行報錯:

image-20181203170902426

這是由於,上面的代碼只適用於推送,但不適用於彈出。

animateTransition()中除了第一行storedContext = transitionContext的代碼,都包含在if語句中:

if operation == .push {
    ...
}
複製代碼

淡入新視圖控制器

轉場時,給詳情頁面添加淡入的動畫。

animateTransition(using:)if operation == .push {語句中添加:

let fadeIn = CABasicAnimation(keyPath: "opacity")
fadeIn.fromValue = 0.0
fadeIn.toValue = 1.0
fadeIn.duration = animationDuration
toVC.view.layer.add(fadeIn, forKey: nil)
複製代碼

彈出轉場

前面都是推送轉場,如今添加是彈出轉場。

給在animateTransition(using:)if語句添加一個else

else {
    let fromView = transitionContext.view(forKey: .from)!
    let toView = transitionContext.view(forKey: .to)!

    transitionContext.containerView.insertSubview(toView, belowSubview: fromView)

    UIView.animate(withDuration: animationDuration, delay: 0.0, options: .curveEaseIn, animations: {
        fromView.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
    }) { (_) in
        transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
       }
}
複製代碼

最終效果會是:

19-交互式導航控制器轉場

您不只能夠爲轉換建立自定義動畫 - 還可使其交互並響應用戶的操做。一般,您經過平移手勢驅動此操做,這是您將在本章中採用的方法。 當您完成後,您的用戶將可以經過在屏幕上滑動手指來來回穿過顯示轉場。那會有多酷? 是的,我覺得你會感興趣!繼續閱讀,瞭解它是如何完成的!

關於手勢處理,可看個人一篇簡單的小結 iOS tutorial 13:手勢處理

本章開始項目使用上一章節完成的項目。

建立交互式轉場

當導航控制器向其代理詢問動畫控制器(就是以前提到Animator)時,可能會發生兩件事。返回nil,在這種狀況下,導航控制器會運行標準轉場動畫; 若是返回一個動畫控制器,那麼導航控制器除了會向其代理詢問轉場動畫控制器,也會詢問交互控制器,以下所示:

image-20181216165544709

交互控制器根據用戶的操做移動轉場,而不是簡單地從開始到結束動畫更改。 交互控制器不必定須要是與動畫控制器分開的類;實際上,當兩個控制器在同一個類中時,執行某些任務會更容易一些。 您只須要確保所述類遵照UIViewControllerAnimatedTransitioningUIViewControllerInteractiveTransitioning兩個協議。

UIViewControllerInteractiveTransitioning只有一個必需實現的方法 startInteractiveTransition(_:) ,它將轉換上下文做爲參數。 而後,交互控制器會按期調用updateInteractiveTransition(_ :)來移動轉換。 首先,您須要更改處理用戶輸入的方式。

處理平移手勢

把點擊手勢修改爲平移手勢。平移手勢可觀察到轉場的開始、過程和結束的狀態。

先把底部的標籤的文本修改爲 Slide to start

接下來,在MasterViewController.swiftviewDidAppear(_:)中刪除如下代碼:

let tap = UITapGestureRecognizer(target: self, action: #selector(didTap))
view.addGestureRecognizer(tap)
複製代碼

替代爲平移手勢識別代碼:

let pan = UIPanGestureRecognizer(target: self, action: #selector(didPan(_:)))
view.addGestureRecognizer(pan)
複製代碼

當用戶在屏幕上滑動是,會被識別而後調用didPan(_:)方法。

MasterViewController中添加空didPan(_:)

使用交互式動畫師類

爲了處理上面的轉場,須要使用內置的交互式動畫師類:UIPercentDrivenInteractiveTransition。 此類遵照UIViewControllerInteractiveTransitioning協議,並能夠將轉場的進度表示爲完成百分比。

打開RevealAnimator.swift,並更新文件頂部的類定義,以下所示:

class RevealAnimator: UIPercentDrivenInteractiveTransition, UIViewControllerAnimatedTransitioning, CAAnimationDelegate {
    
複製代碼

請注意,UIPercentDrivenInteractiveTransition是一個類,而不是其餘協議,因此須要處於第一位置。

添加一個屬性,來表示是否已交互方式驅動轉場動畫:

var interactive = false
複製代碼

添加方法到RevealAnimator中:

func handlePan(_ recognizer: UIPanGestureRecognizer) {

}
複製代碼

當用戶在屏幕上平移時,識別器將被傳遞給RevealAnimator中的handlePan(_:)處理,來更新當前的轉場進度。

MasterViewController.swift中添加委託方法:

func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
    if !transition.interactive {
        return nil
    }
    return transition
}
複製代碼

當但願轉場爲交互式時,只需返回交互式控制器,不然返回nil

如今,須要將平移手勢識別器鏈接到交互控制器。 在didPan(_:)中添加:

switch recognizer.state {
    case .began:
        transition.interactive = true
        performSegue(withIdentifier: "details", sender: nil)
    default:
        transition.handlePan(recognizer)
}
複製代碼

當平移手勢開始時,確保交互設置爲true,而後經過 segue 鏈接到下一個視圖控制器。 執行segue將啓動轉場,這時動畫控制器和交互控制器的委託方法將返回轉場動畫。

若是手勢已經開始,只需將操做交給交互控制器,以下圖所示:

image-20181203233104113

計算轉場動畫的進度

平移手勢處理程序中最重要的一點是要弄清楚轉場的進度。

打開RevealAnimator.swift,並將如下代碼添加到handlePan中:

let translation = recognizer.translation(in: recognizer.view!.superview!)
var progress: CGFloat = abs(translation.x / 200.0)
progress = min(max(progress, 0.01), 0.99)
複製代碼

經過平移手勢識別器計算轉場的經度。從邏輯上講,用戶離開初始位置越遠,轉場的進度就越大。

200.0是一個合理的任意數字,來表示轉場完成所須要的距離。

下面更新轉場動畫的進度,將如下代碼添加到handlePan()中:

switch recognizer.state {
    case .changed:
        update(progress)
    default:
        break
}
複製代碼

update() 是來自UIPercentDrivenInteractiveTransition的方法,它設置轉場動畫的當前進度。

當用戶在屏幕上平移時,手勢識別器會重複調用MasterViewControllerdidPan(),從而不停的調用RevealAnimator中 的handlePan()來更新轉場進度。

RevealAnimator中添加屬性:

private var pausedTime: CFTimeInterval = 0
複製代碼

如今,經過將如下代碼添加到animateTransition(using:)來控制圖層:

if interactive {
    let transitionLayer = transitionContext.containerView.layer
    pausedTime = transitionLayer.convertTime(CACurrentMediaTime(), from: nil)
    transitionLayer.speed = 0
    transitionLayer.timeOffset = pausedTime
}
複製代碼

這裏作的是阻止圖層運行本身的動畫。 這將凍結全部子圖層動畫。

重寫update(_:),以將圖層與動畫一塊兒移動:

override func update(_ percentComplete: CGFloat) {
    super.update(percentComplete)

    let animationProgress = TimeInterval(animationDuration) * TimeInterval(percentComplete)
    storedContext?.containerView.layer.timeOffset = pausedTime + animationProgress
}
複製代碼

運行效果:

這邊出現問題,就是手指離開屏幕後,動畫當即中止,再次滑動時也沒有反應。

處理提早終止

處理上面的問題。

handlePan()的switch語句中添加case

case .cancelled, .ended:
    if progress < 0.5 {
        cancel()
    } else {
        finish()
    }
複製代碼

在用戶手指離開屏幕以前,若是平移得足夠遠,就表示轉場完成,呈現新的視圖控制器;相反,就滾回原來的視圖控制器。

重寫cancel()finish()方法:

override func cancel() {
    restart(forFinishing: false)
    super.cancel()
}

override func finish() {
    restart(forFinishing: true)
    super.finish()
}

private func restart(forFinishing: Bool) {
    let transitionLayer = storedContext?.containerView.layer
    transitionLayer?.beginTime = CACurrentMediaTime()
    transitionLayer?.speed = forFinishing ? 1 : -1
}
複製代碼

.cancelled,.endedcase中添加:

interactive = false
複製代碼

本章最後的效果:

本文在個人我的博客中地址:系統學習iOS動畫之四:視圖控制器的轉場動畫

相關文章
相關標籤/搜索