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

Andy_Ron發表於2018-12-27

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

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

iOS中最容易識別的動畫之一是將新檢視控制器推入導航堆疊的動畫,當我們想讓APP有自己的特色,自定義轉場動畫是非常好的方式。

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

預覽:

17-檢視控制器轉場和螢幕旋轉轉場 瞭解如何通過自定義動畫轉場呈現檢視控制器 - 作為獎勵,您將建立動畫轉場以處理裝置方向更改。

18-導航控制器轉場

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

17-檢視控制器轉場和螢幕旋轉轉場

無論是呈現 照相機檢視控制器、地址簿還是自定義的模態螢幕,每次都呼叫相同的UIKit方法:present(_:animated:completion:)。 此方法將當前螢幕“放棄”,然後跳到另一個檢視控制器。

下圖呈現一個“New Contact”檢視控制器向上滑動以覆蓋當前檢視(聯絡人列表),這是預設的動畫方式:

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

在本章中,學習建立自己的自定義演示控制器動畫,以替換預設的動畫,並使本章的專案更加生動。

開始專案

本章開始專案是一個新專案,叫BeginnerCook

這個開始專案可以簡單概括 如下,ViewController中包括一個背景圖UIImageView,一個標題UILabel,一個文字檢視UITextView,下面是一個可以左右移動的UIScrollView。這個UIScrollView裡會用程式碼加入一些香草(herb)圖片,點選圖片會跳轉到另個展示詳情的檢視控制器HerbDetailsViewController,這個轉場是標準的從下到上的垂直覆蓋轉場動畫。

開始專案預覽一下:

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

自定義轉場的原理

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可以自由地結束檢視控制器轉場。

目前的效果就是:

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

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轉場動畫已經完成。

現在的效果:

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

動畫從左上角開始; 這是因為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,並在動畫期間隱藏初始影象。

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

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

解除轉場

剩下要做的就是解除詳細頁檢視的動畫。

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動畫之四:檢視控制器的轉場動畫

螢幕旋轉轉場

裝置方向更改視為從檢視控制器到其自身的轉場過程。

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 +向左箭頭):

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

將螢幕旋轉為橫向模式時,可以清楚地看到背景變深。

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

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

平滑轉場動畫

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

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)。

上面兩個修改後的效果:

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

18-導航控制器轉場

UINavigationController是iOS中為數不多的內建應用導航解決方案之一。 將一個新的檢視控制器推入或彈出導航堆疊,這個過程自帶一個時尚的動畫。

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

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

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

開始專案

本章開始專案是一個新專案,叫LogoReveal

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

點選預設螢幕任意地方(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()中還沒有編寫任何程式碼。

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

新增自定義顯示動畫

自定義轉場動畫的計劃相對簡單。 您只需在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)
複製程式碼

效果:

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

優化細節

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

animateTransition()中新增:

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

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

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

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

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)
       }
}
複製程式碼

最終效果會是:

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

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
}
複製程式碼

執行效果:

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

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

處理提前終止

處理上面的問題。

handlePan()的switch語句中新增case

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

在使用者手指離開螢幕之前,如果平移得足夠遠,就表示轉場完成,呈現新的檢視控制器;相反,就滾回原來的檢視控制器。

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

重寫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動畫之四:檢視控制器的轉場動畫

本文在我的個人部落格中地址:系統學習iOS動畫之四:檢視控制器的轉場動畫

相關文章