動畫示例(八) —— UIViewController間轉(zhuǎn)場動畫的實現(xiàn) (一)

版本記錄

版本號 時間
V1.0 2018.08.24

前言

如果你細(xì)看了我前面寫的有關(guān)動畫的部分佛析,就知道前面介紹了CoreAnimation物遇、序列幀以及LOTAnimation等很多動畫方式狈醉,接下來幾篇我們就以動畫示例為線索喝峦,進(jìn)行動畫的講解饵撑。相關(guān)代碼已經(jīng)上傳至GitHub - 刀客傳奇剑梳。感興趣的可以看我寫的前面幾篇。
1. 動畫示例(一) —— 一種外擴(kuò)的簡單動畫
2. 動畫示例(二) —— 一種抖動的簡單動畫
3. 動畫示例(三) —— 仿頭條一種LOTAnimation動畫
4. 動畫示例(四) —— QuartzCore之CAEmitterLayer下雪??動畫
5. 動畫示例(五) —— QuartzCore之CAEmitterLayer煙花動畫
6. 動畫示例(六) —— QuartzCore之CAEmitterLayer滑潘、CAReplicatorLayer和CAGradientLayer簡單動畫
7. 動畫示例(七) —— 基于CAShapeLayer圖像加載過程的簡單動畫(一)

開始

本文寫作環(huán)境:Swift 4, iOS 11, Xcode 9

iOS支持視圖控制器之間的自定義轉(zhuǎn)換垢乙,在本文中,您將實現(xiàn)應(yīng)用程序的UIViewController轉(zhuǎn)場動畫语卤。

首先看一個效果追逮。

不久前,發(fā)布了一款名為Ping的應(yīng)用程序粹舵,該應(yīng)用程序允許用戶接收有關(guān)他們感興趣的主題的通知钮孵。

除了不可預(yù)測的推薦之外,關(guān)于Ping的一個突出的事情是主屏幕和菜單之間的圓形轉(zhuǎn)場過渡眼滤,如上面的動畫所示巴席。

當(dāng)然,當(dāng)你看到一些很酷的東西時诅需,你想看看你是否能弄清楚它們是如何做到的漾唉。 在本文中,您將學(xué)習(xí)如何使用UIViewController transition animation在Swift中實現(xiàn)這個很酷的動畫堰塌。 在此過程中赵刑,您將學(xué)習(xí)如何使用形狀圖層(shape layer),蒙版场刑,UIViewControllerAnimatedTransitioning協(xié)議般此,UIPercentDrivenInteractiveTransition類等。

在Ping中摇邦,當(dāng)您從一個視圖控制器轉(zhuǎn)到另一個視圖控制器時恤煞,會發(fā)生UIViewController轉(zhuǎn)場動畫屎勘。

在iOS中施籍,您可以通過將兩個視圖控制器放在UINavigationController中來實現(xiàn)視圖控制器之間的自定義轉(zhuǎn)場,并實現(xiàn)iOS的UIViewControllerAnimatedTransitioning協(xié)議來設(shè)置轉(zhuǎn)換動畫概漱。

在開始之前要記住的一件事是丑慎,您可以使用您想要的任何方法實現(xiàn)這些動畫,無論是pop,UIView竿裂,UIKit Dynamics還是較低級別的Core Animation API玉吁。

在本文中,您將專注于標(biāo)準(zhǔn)的UIView和Core Animation API腻异。
既然您知道編碼操作發(fā)生在哪里进副,那么就該考慮如何實際實現(xiàn)轉(zhuǎn)場過渡。

僅僅看一下悔常,對動畫實現(xiàn)的一個很好的猜測如下:

  • 1)有一個圓圈來自右上角的按鈕影斑,它充當(dāng)了出現(xiàn)的視圖的視口。
  • 2)您要離開的視圖控制器的文本會增長机打,并在屏幕左側(cè)動畫矫户。
  • 3)您正在移動的視圖控制器的文本從右側(cè)開始增長和淡入,在擴(kuò)展圈的可見空間內(nèi)残邀。

現(xiàn)在您已經(jīng)模糊地了解了您的目標(biāo)皆辽,現(xiàn)在是時候開始了。

下載下來工程芥挣,如果您Build并運(yùn)行驱闷,您將看到一個已創(chuàng)建的應(yīng)用程序,繼續(xù)這樣做空免,然后點擊圓形按鈕幾次遗嗽。

正如你所看到的,你手上有一個無聊的舊版默認(rèn)push和pop動畫鼓蜒,下面我們做的就是優(yōu)化這個轉(zhuǎn)場效果痹换。


Navigation Controller Delegates - 導(dǎo)航VC代理

UINavigationController實例具有delegate屬性匈棘,該屬性可以是實現(xiàn)UINavigationControllerDelegate協(xié)議的任何對象斧抱。

此協(xié)議中的其他四種方法與對顯示的視圖控制器作出反應(yīng)并指定支持哪些方向有關(guān),但有兩種方法允許您指定負(fù)責(zé)實現(xiàn)自定義轉(zhuǎn)場的對象脂倦。

在您轉(zhuǎn)場離開之前畅厢,您將需要創(chuàng)建一個可以為您的應(yīng)用程序成為此委托的新類冯痢。

啟動應(yīng)用程序打開并選擇Pong后,按?+ N開始添加新文件框杜。 從選項中選擇Cocoa Touch Class浦楣,然后單擊Next。 將新類命名為TransitionCoordinator咪辱,并確保將其設(shè)置為Subclass:NSObjectLanguage:Swift振劳。 點擊Next,然后點擊Create油狂。

該類需要遵守UINavigationControllerDelegate協(xié)議历恐。 將類定義行更改為:

class TransitionCoordinator: NSObject, UINavigationControllerDelegate {

到目前為止一切順利寸癌,接下來你將實現(xiàn)你唯一關(guān)心的代理方法。 將以下方法添加到TransitionCoordinator:

func navigationController(_ navigationController: UINavigationController,
                          animationControllerFor operation: UINavigationControllerOperation,
                          from fromVC: UIViewController,
                          to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
  return nil
}

所有這個方法需要做的是查看它正在移動的視圖控制器弱贼,以及它移動到的哪個視圖控制器蒸苇,并返回該對的適當(dāng)動畫對象。

目前吮旅,你只是返回nil溪烤,這是默認(rèn)情況下一直在發(fā)生的事情。 當(dāng)導(dǎo)航控制器要求動畫控制器進(jìn)行某個轉(zhuǎn)場并收到nil時庇勃,它會使用您之前看到的push和pop轉(zhuǎn)場氛什。

你將回到這個類,稍微返回一個合適的動畫控制器對象匪凉。

AppDelegate.swift中枪眉,在窗口屬性聲明的下方添加以下內(nèi)容:

let transitionCoordinator = TransitionCoordinator()

這初始化了TransitionCoordinator并保留了對它的強(qiáng)引用。

現(xiàn)在找到您隱藏導(dǎo)航欄的行:

nav.isNavigationBarHidden = true

在此行之后再层,將TransitionCoordinator指定為導(dǎo)航控制器的代理贸铜,其中包含以下內(nèi)容:

nav.delegate = transitionCoordinator

Build并運(yùn)行以確認(rèn)它運(yùn)行。 因為代理正在返回一個nil動畫聂受,所以你不會看到任何新的事情發(fā)生蒿秦。


The UIViewControllerAnimatedTransitioning Protocol - UIViewControllerAnimatedTransitioning協(xié)議

TransitionCoordinator將返回的animation object只是符合UIViewControllerAnimatedTransitioning的東西。

符合此協(xié)議的對象工作簡單蛋济。他們只需要實現(xiàn)兩種方法棍鳖。第一種方法是返回轉(zhuǎn)換所需的時間(以秒為單位)。第二個是一個方法碗旅,它接受一個上下文(context)對象渡处,它具有實際執(zhí)行動畫所需的所有信息。

一個非常常見的模式是創(chuàng)建動畫對象祟辟,并在pushing and popping之間的轉(zhuǎn)換看起來不同時為其分配UINavigationControllerOperation參數(shù)医瘫。

在這種情況下,您實際上并不需要這樣做旧困;無論你是push還是pop醇份,轉(zhuǎn)場都是一樣的,所以如果你通用的方法進(jìn)行寫吼具,無論你方向如何僚纷,它都會起作用。

既然你知道自己需要什么拗盒,那就是寫新類的時候了怖竭。再次按?+ N,再創(chuàng)建一個Cocoa Touch類锣咒,這次將其命名為CircularTransition侵状。

您需要對新類執(zhí)行的第一件事是使其符合UIViewControllerAnimatedTransitioning協(xié)議赞弥。為此毅整,只需在NSObject繼承聲明之后添加它趣兄,如下所示:

class CircularTransition: NSObject, UIViewControllerAnimatedTransitioning {

按照慣例,您會立即被告知您的類不符合此協(xié)議悼嫉。下面就優(yōu)先解決這個問題艇潭。

首先,添加指定動畫持續(xù)時間的方法戏蔑。

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) 
  -> TimeInterval {
  return 0.5
}

這是UIKit在導(dǎo)航控制器代理提供之后調(diào)用轉(zhuǎn)場對象的第一種方法蹋凝。 在這里,您設(shè)置了這種轉(zhuǎn)場需要大約半秒才能完成总棵。

接下來鳍寂,添加您將在稍后返回的實際動畫方法的空定義。

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
  //make some magic happen
  
}

在這里情龄,您將收到一個轉(zhuǎn)換上下文對象迄汛,該對象將包含編寫動畫代碼所需的所有信息。

返回到TransitionCoordinator.swift并用更有用的東西替換當(dāng)前的nilreturn語句:

return CircularTransition()

在這里骤视,你告訴導(dǎo)航控制器你厭倦了它一直試圖使用的那種無聊的push和pop轉(zhuǎn)場鞍爱,并且你有更好的想法。 在內(nèi)部专酗,UIKit將采用此UIViewControllerAnimatedTransitioning對象睹逃,并使用它來驅(qū)動此導(dǎo)航控制器從現(xiàn)在開始發(fā)生的所有轉(zhuǎn)場的動畫。

這對你來說非常棒祷肯,但請記住沉填,還需要做很多工作。 所以回到CircularTransition.swift并為自己的實際工作做好準(zhǔn)備吧佑笋!


The CircleTransitionable Protocol - CircleTransitionable協(xié)議

如果您曾經(jīng)嘗試過寫其中一個轉(zhuǎn)換拜轨,那么您可能已經(jīng)發(fā)現(xiàn)編寫內(nèi)部視圖控制器狀態(tài)的代碼非常容易。但是允青,您將準(zhǔn)確定義視圖控制器需要預(yù)先提供的內(nèi)容橄碾,并允許任何希望以此方式設(shè)置動畫的視圖控制器提供對這些視圖的獲取和訪問。

在類定義之前颠锉,在CircularTransition.swift的頂部添加以下協(xié)議定義:

protocol CircleTransitionable {
  var triggerButton: UIButton { get }
  var contentTextView: UITextView { get }
  var mainView: UIView { get }
}

此協(xié)議定義了每個視圖控制器所需的信息法牲,以便成功動畫。

  • 1)triggerButton將是用戶點擊的按鈕琼掠。
  • 2)contentTextView將是在屏幕上或屏幕外設(shè)置動畫的文本視圖拒垃。
  • 3)mainView將是在屏幕上或屏幕外動畫的主視圖。

接下來瓷蛙,轉(zhuǎn)到ColoredViewController.swift并通過使用以下內(nèi)容替換定義使其符合您的新協(xié)議悼瓮。

class ColoredViewController: UIViewController, CircleTransitionable {

幸運(yùn)的是戈毒,這個視圖控制器已經(jīng)定義了triggerButtoncontentTextView,所以它已經(jīng)接近準(zhǔn)備好了横堡。 您需要做的最后一件事是為mainView屬性添加一個計算屬性埋市。 在定義contentTextView之后立即添加以下內(nèi)容:

var mainView: UIView {
  return view
}

在這里,您所要做的就是返回視圖控制器的默認(rèn)view屬性命贴。

該項目包含一個BlackViewControllerWhiteViewController道宅,它在應(yīng)用程序中顯示兩個視圖。 兩者都是ColoredViewController的子類胸蛛,所以你正式設(shè)置兩個類都可以轉(zhuǎn)場污茵。 恭喜!


Animating the Old Text Away - 動畫移走舊的文本

接下來要真正的開始做一些動畫????????了葬项。

回到CircularTransition.swift泞当,將guard語句添加到animateTransition(transitionContext:)

guard let fromVC = transitionContext.viewController(forKey: .from) as? CircleTransitionable,
  let toVC = transitionContext.viewController(forKey: .to) as? CircleTransitionable,
  let snapshot = fromVC.mainView.snapshotView(afterScreenUpdates: false) else {
    transitionContext.completeTransition(false)
    return
}

transitionContext允許您獲取對正在轉(zhuǎn)換的視圖控制器的引用民珍。 您將它們轉(zhuǎn)換為CircleTransitionable襟士,以便稍后可以訪問它們的主視圖和文本視圖。

snapshotView(afterScreenUpdates :)返回fromVC的快照位圖穷缤。

快照視圖(Snapshot view)是一種非常有用的方法敌蜂,可以快速獲取動畫視圖的一次性副本。 您無法為各個子視圖設(shè)置動畫津肛,但如果您只需要為完整的層次結(jié)構(gòu)制作動畫而不必在完成后將其放回原處章喉,則快照是一種理想的解決方案。

在你的guardelse子句中身坐,你在transitionContext上調(diào)用completeTransition()秸脱。 您傳遞false以告訴UIKit您沒有完成轉(zhuǎn)換,并且它不應(yīng)該移動到下一個視圖控制器部蛇。

guard之后摊唇,抓取對上下文提供的容器視圖的引用。

let containerView = transitionContext.containerView

此視圖就像是用于在到達(dá)最終目的地的路上添加和刪除視圖的暫存器涯鲁。

當(dāng)您完成動畫制作后巷查,您將在containerView中完成以下操作:

  • 1)從容器中刪除了fromVC的視圖。
  • 2)將toVC的視圖添加到目的地抹腿,并配置應(yīng)該顯示的子視圖岛请。

animateTransition(transitionContext :)的底部添加以下內(nèi)容:

containerView.addSubview(snapshot)

要在屏幕外設(shè)置舊文本的動畫而不會弄亂實際文本視圖的frame,您將為快照snapshot設(shè)置動畫警绩。

接下來崇败,刪除您要來的實際視圖,因為您將不再需要它。

fromVC.mainView.removeFromSuperview()

最后后室,在animateTransition(transitionContext:)方法下面添加下面代碼:

func animateOldTextOffscreen(fromView: UIView) {
  // 1
  UIView.animate(withDuration: 0.25, 
                 delay: 0.0, 
                 options: [.curveEaseIn], 
                 animations: {
    // 2
    fromView.center = CGPoint(x: fromView.center.x - 1300,
                              y: fromView.center.y + 1500)
    // 3
    fromView.transform = CGAffineTransform(scaleX: 5.0, y: 5.0)
  }, completion: nil)
}

這個方法非常簡單:

  • 1)您可以定義一個動畫缩膝,該動畫將需要0.25秒才能完成并進(jìn)入其動畫曲線。
  • 2)您可以將視圖的中心向下設(shè)置為動畫岸霹,也可以設(shè)置在屏幕左側(cè)疾层。
  • 3)視圖被擴(kuò)大了5倍,因此文本似乎隨著您稍后將要制作的圓圈一起增長松申。

這會導(dǎo)致文本同時增長和移出屏幕云芦。 神奇的數(shù)字可能看起來有點隨意俯逾,但它們來自于調(diào)試的結(jié)果贸桶。

將以下內(nèi)容添加到animateTransition(transitionContext :)的底部:

animateOldTextOffscreen(fromView: snapshot)

你將snapshot傳遞給新方法并動畫移出屏幕,Build并運(yùn)行看一下效果桌肴。

好的皇筛,它仍然不是那么令人印象深刻,還需要完善和修改坠七。

注意:CircularTransition.swift中仍有一個警告水醋。 別擔(dān)心,你很快就會解決它彪置!


Fixing the Background - 修復(fù)背景

一個令人討厭的事情是拄踪,由于整個視圖都是動畫,所以你會看到背后的黑色背景拳魁。

這個黑色背景是containerView惶桐,你真正想要的是它看起來只是文本動畫,而不是整個背景潘懊。 要解決此問題姚糊,您需要添加一個不會設(shè)置動畫的新背景視圖。

CircularTransition.swift中授舟,轉(zhuǎn)到animateTransition(using:)救恨。 獲取對containerView的引用之后,在將snapshotView添加為子視圖之前释树,請?zhí)砑右韵麓a:

let backgroundView = UIView()
backgroundView.frame = toVC.mainView.frame
backgroundView.backgroundColor = fromVC.mainView.backgroundColor

在這里肠槽,您要創(chuàng)建backgroundView,將其frame設(shè)置為全屏奢啥,并將其背景顏色設(shè)置為與backgroundView的顏色相匹配秸仙。

然后,添加新背景作為containerView的子視圖扫尺。

containerView.addSubview(backgroundView)

Build并查看效果

這個就好多了筋栋。


The Circular Mask Animation - 圓形遮罩動畫

現(xiàn)在你已經(jīng)完成了第一個塊,接下來你需要做的是實際的圓形轉(zhuǎn)場正驻,新的視圖控制器從按鈕的位置開始動畫弊攘。

首先將以下方法添加到CircularTransition

func animate(toView: UIView, fromTriggerButton triggerButton: UIButton) {
  
}

這將完成圓形轉(zhuǎn)場 - 您很快就會實現(xiàn)它抢腐!

animateTransition(using:)中,在animateOldTextOffscreen(fromView:snapshot)之后添加以下內(nèi)容:

containerView.addSubview(toVC.mainView)
animate(toView: toVC.mainView, fromTriggerButton: fromVC.triggerButton)

這會將您的最終視圖添加到containerView中襟交,并在您實現(xiàn)動畫后對其進(jìn)行動畫處理迈倍!

現(xiàn)在你有了圓形轉(zhuǎn)場的骨架。 然而捣域,使這個動畫工作的真正關(guān)鍵是理解方便的CAShapeLayer類以及layer masking的概念啼染。

1. CAShapeLayer

CAShapeLayers是一個特殊的CALayer類,它不是總是呈現(xiàn)為正方形焕梅,而是可以通過首先定義貝塞爾曲線路徑然后將該路徑指定給圖層的path屬性來定義其形狀迹鹅。

在這種情況下,您將定義兩個貝塞爾曲線路徑并在它們之間設(shè)置動畫贞言。

將以下邏輯添加到先前添加的方法animate(toView:triggerButton :)

// 1
let rect = CGRect(x: triggerButton.frame.origin.x,
                  y: triggerButton.frame.origin.y,
                  width: triggerButton.frame.width,
                  height: triggerButton.frame.width)
// 2
let circleMaskPathInitial = UIBezierPath(ovalIn: rect)

這會創(chuàng)建一個bezier路徑斜棚,從triggerButton的位置開始,在內(nèi)容中定義一個小的圓形窗口该窗。

你創(chuàng)建了一個:

  • 1)rect類似于按鈕的frame弟蚀,但寬度和高度相等。
  • 2)bezier路徑橢圓形從rect酗失,最后是圓形义钉。

接下來,創(chuàng)建一個表示動畫結(jié)束狀態(tài)的圓圈规肴。 由于您只能看到圓圈內(nèi)的內(nèi)容捶闸,因此您不希望在動畫結(jié)束時仍能看到圓圈的任何邊緣。 在剛剛添加的代碼下面添加以下內(nèi)容:

// 1
let fullHeight = toView.bounds.height
let extremePoint = CGPoint(x: triggerButton.center.x,
                           y: triggerButton.center.y - fullHeight)
// 2
let radius = sqrt((extremePoint.x*extremePoint.x) +
                  (extremePoint.y*extremePoint.y))
// 3
let circleMaskPathFinal = UIBezierPath(ovalIn: triggerButton.frame.insetBy(dx: -radius,
                                                                           dy: -radius))

這是這樣做的:

  • 1)定義一個點奏纪,即整個屏幕高于屏幕頂部的高度鉴嗤。
  • 2)使用畢達(dá)哥拉斯定理計算新圓的半徑:a2+b2=c2。
  • 3)通過獲取圓的當(dāng)前frame并在兩個方向上“插入”負(fù)值來創(chuàng)建新的貝塞爾曲線路徑序调,從而將其推出以完全超出屏幕在兩個方向上的界限醉锅。

現(xiàn)在您已經(jīng)設(shè)置了更好的路徑,現(xiàn)在是時候?qū)⑺鼈冇糜诠ぷ髁恕?仍然在animate(toView:triggerButton:)中发绢,添加:

let maskLayer = CAShapeLayer()
maskLayer.path = circleMaskPathFinal.cgPath
toView.layer.mask = maskLayer

這將創(chuàng)建一個CAShapeLayer圖層并將其路徑設(shè)置為圓形貝塞爾曲線路徑硬耍。 然后將maskLayer用作目標(biāo)視圖的遮罩。

但等一下边酒,masks究竟是如何工作的经柴?

2. CALayer Masking - CALayer遮罩

通常,alpha值為1的蒙版顯示下面的圖層內(nèi)容墩朦,而alpha值為0則隱藏下面的內(nèi)容坯认。 中間的任何內(nèi)容都會部分顯示圖層的內(nèi)容。 這是一個解釋這個的圖表:

基本上,你可以想到你可以看到的任何形狀都是被剪掉的形狀牛哺,這樣你就可以看到下面的東西陋气。 其他一切最終都會被隱藏起來。 使用這些貝塞爾曲線路徑時引润,圓圈內(nèi)的像素的alpha值為1.0巩趁,而圓圈邊界以外的部分則清晰,因此無法在這些點處看到蒙版視圖淳附。

現(xiàn)在你已經(jīng)完成了所有這些設(shè)置议慰,剩下要做的唯一事情就是在兩個圓形蒙版之間實際制作動畫。 棘手的是奴曙,到目前為止别凹,你只完成了UIView動畫,但那些不適用于CALayers缆毁。

3. Animations with Core Animation - 使用Core Animation進(jìn)行動畫

在這種情況下番川,您已經(jīng)達(dá)到了UIView動畫抽象無法再幫助您的程度到涂,您需要降低一個級別脊框。

這肯定遲早會發(fā)生,但不要擔(dān)心践啄,API非常簡單浇雹。 這也很好理解,因為無論如何UIView動畫真的只是引擎蓋下的CATransactions屿讽。

與UIView動畫的基于閉包的API相比昭灵,Core Animation動畫使用基于對象的方法。 這也是一個抽象伐谈,分解為引擎蓋下的CATransaction烂完,這對于你所做的任何與視圖相關(guān)的事情實際上都是正確的。

仍然在animate(toView:triggerButton:)诵棵,創(chuàng)建一個將執(zhí)行動畫的CABasicAnimation對象抠蚣。

let maskLayerAnimation = CABasicAnimation(keyPath: "path")

在這里,您創(chuàng)建一個動畫對象并告訴它將要設(shè)置動畫的屬性是path屬性履澳。 這意味著您將為渲染的形狀設(shè)置動畫嘶窄。

接下來,設(shè)置此動畫的fromto值距贷。

maskLayerAnimation.fromValue = circleMaskPathInitial.cgPath
maskLayerAnimation.toValue = circleMaskPathFinal.cgPath

在這里柄冲,您使用先前創(chuàng)建的兩個貝塞爾曲線路徑來定義圖層應(yīng)在其間進(jìn)行動畫處理的兩個狀態(tài)。

配置動畫最后要做的就是告訴對象運(yùn)行多長時間忠蝗。 添加以下行來執(zhí)行此操作:

maskLayerAnimation.duration = 0.15

在這種情況下现横,動畫將運(yùn)行0.15秒。

CAAnimations不使用像UIView動畫這樣的完成塊,而是使用帶回調(diào)的委托來表示完成戒祠。 雖然您在技術(shù)上不需要這個代理晦攒,但您將實現(xiàn)委托以更好地理解它。

首先添加以下行:

maskLayerAnimation.delegate = self

此類現(xiàn)在是動畫對象的委托得哆。

轉(zhuǎn)到文件的底部并添加此類擴(kuò)展以遵循CAAnimationDelegate協(xié)議脯颜。

extension CircularTransition: CAAnimationDelegate {
  func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {

  }
}

完成此動畫后,您可以正式調(diào)用整個動畫贩据。 在此回調(diào)中栋操,您希望在動畫開頭接收的上下文context對象上調(diào)用completeTransition()

不幸的是饱亮,這突出了一個令人討厭的事情矾芙,即必須使用此委托回調(diào)。 要訪問上下文對象近上,您必須在主動畫方法的開頭保存對它的引用剔宪。

首先,轉(zhuǎn)到CircularTransition的頂部并添加:

weak var context: UIViewControllerContextTransitioning?

然后壹无,轉(zhuǎn)到animateTransition(transitionContext :)中的guard語句之后的行葱绒,并保存?zhèn)魅氲纳舷挛囊怨┤蘸笫褂谩?/p>

context = transitionContext

現(xiàn)在,回到擴(kuò)展中的animationDidStop(anim:finished :)并添加以下行:

context?.completeTransition(true)

您現(xiàn)在在動畫成功完成時通知系統(tǒng)斗锭。

現(xiàn)在您已經(jīng)設(shè)置了動畫對象地淀,只需將其添加到maskLayer即可。 在animate(toView:triggerButton:)后面加上下面代碼岖是。

maskLayer.add(maskLayerAnimation, forKey: "path")

您需要再次指定要為maskLayerpath設(shè)置動畫帮毁。 將動畫添加到圖層后,它將自動啟動豺撑。

Build并運(yùn)行以查看幾乎完全完成的轉(zhuǎn)換烈疚!


The Finishing Touches - 結(jié)束點擊

為了完整起見,您將為轉(zhuǎn)場添加一個小動畫聪轿。 您還可以從右側(cè)獲得目標(biāo)視圖的文本淡入爷肝,而不是僅顯示目標(biāo)視圖控制器的圓圈。

與上一個動畫相比屹电,這一個是輕而易舉的阶剑。 轉(zhuǎn)到CircularTransition類定義的底部并添加以下方法:

func animateToTextView(toTextView: UIView, fromTriggerButton: UIButton) {
  
}

在新方法中添加如下代碼:

let originalCenter = toTextView.center
toTextView.alpha = 0.0
toTextView.center = fromTriggerButton.center
toTextView.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)

在這里,您要設(shè)置toTextView的起始狀態(tài)危号。 您將其alpha設(shè)置為0牧愁,使用觸發(fā)按鈕居中,并將其縮放到正常大小的1/10外莲。

接下來猪半,添加以下UIView動畫兔朦。

UIView.animate(withDuration: 0.25, delay: 0.1, options: [.curveEaseOut], animations: {
  toTextView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
  toTextView.center = originalCenter
  toTextView.alpha = 1.0
}, completion: nil)

在這里,您只需撤消您剛才所做的一切磨确,即將文本視圖設(shè)置回中心沽甥,并將其快速淡入淡出。

最后乏奥,將以下調(diào)用添加到animateTransition(transitionContext :)的底部摆舟。

animateToTextView(toTextView: toVC.contentTextView, fromTriggerButton: fromVC.triggerButton)

您將使用toVC文本視圖和fromVC按鈕提供animateToTextView。 現(xiàn)在邓了,它將完成文本視圖動畫以及其他轉(zhuǎn)場動畫恨诱。

最后一次Build并運(yùn)行,以獲得與原始Ping應(yīng)用程序非常相似的轉(zhuǎn)場效果骗炉!


源碼

1. TransitionCoordinator.swift
import UIKit

class TransitionCoordinator: NSObject, UINavigationControllerDelegate {
    func navigationController(_ navigationController: UINavigationController,
                              animationControllerFor operation: UINavigationControllerOperation,
                              from fromVC: UIViewController,
                              to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return CircularTransition()
    }

}
2. CircularTransition.swift
import UIKit

protocol CircleTransitionable {
  var triggerButton: UIButton { get }
  var contentTextView: UITextView { get }
  var mainView: UIView { get }
}

class CircularTransition: NSObject, UIViewControllerAnimatedTransitioning {
  weak var context: UIViewControllerContextTransitioning?

  //make this zero for now and see if it matters when it comes time to make it interactive
  func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
    return 0.0
  }
  func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    guard let fromVC = transitionContext.viewController(forKey: .from) as? CircleTransitionable,
      let toVC = transitionContext.viewController(forKey: .to) as? CircleTransitionable,
      let snapshot = fromVC.mainView.snapshotView(afterScreenUpdates: false) else {
        transitionContext.completeTransition(false)
        return
    }
    
    context = transitionContext

    let containerView = transitionContext.containerView
    
    //Background View With Correct Color
    let backgroundView = UIView()
    backgroundView.frame = toVC.mainView.frame
    backgroundView.backgroundColor = fromVC.mainView.backgroundColor
    containerView.addSubview(backgroundView)
    
    //Animate old view offscreen
    containerView.addSubview(snapshot)
    fromVC.mainView.removeFromSuperview()
    animateOldTextOffscreen(fromView: snapshot)
    
    //Growing Circular Mask
    containerView.addSubview(toVC.mainView)
    animate(toView: toVC.mainView, fromTriggerButton: fromVC.triggerButton)
    
    //Animate Text in with a Fade
    animateToTextView(toTextView: toVC.contentTextView, fromTriggerButton: fromVC.triggerButton)
  }
  
  func animateOldTextOffscreen(fromView: UIView) {
    UIView.animate(withDuration: 0.25, delay: 0.0, options: [.curveEaseIn], animations: {
      fromView.center = CGPoint(x: fromView.center.x - 1000, y: fromView.center.y + 1500)
      fromView.transform = CGAffineTransform(scaleX: 5.0, y: 5.0)
    }, completion: nil)
  }
  func animate(toView: UIView, fromTriggerButton triggerButton: UIButton) {
    //Starting Path
    let rect = CGRect(x: triggerButton.frame.origin.x,
                      y: triggerButton.frame.origin.y,
                      width: triggerButton.frame.width,
                      height: triggerButton.frame.width)
    let circleMaskPathInitial = UIBezierPath(ovalIn: rect)
    
    //Destination Path
    let fullHeight = toView.bounds.height
    let extremePoint = CGPoint(x: triggerButton.center.x,
                               y: triggerButton.center.y - fullHeight)
    let radius = sqrt((extremePoint.x*extremePoint.x) +
      (extremePoint.y*extremePoint.y))
    let circleMaskPathFinal = UIBezierPath(ovalIn: triggerButton.frame.insetBy(dx: -radius,
                                                                               dy: -radius))
    
    //Actual mask layer
    let maskLayer = CAShapeLayer()
    maskLayer.path = circleMaskPathFinal.cgPath
    toView.layer.mask = maskLayer
    
    //Mask Animation
    let maskLayerAnimation = CABasicAnimation(keyPath: "path")
    maskLayerAnimation.fromValue = circleMaskPathInitial.cgPath
    maskLayerAnimation.toValue = circleMaskPathFinal.cgPath
    maskLayerAnimation.delegate = self
    maskLayer.add(maskLayerAnimation, forKey: "path")
  }
  
  func animateToTextView(toTextView: UIView, fromTriggerButton: UIButton) {
    //Start toView offscreen a little and animate it to normal
    let originalCenter = toTextView.center
    toTextView.alpha = 0.0
    toTextView.center = fromTriggerButton.center
    toTextView.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
    
    UIView.animate(withDuration: 0.25, delay: 0.1, options: [.curveEaseOut], animations: {
      toTextView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
      toTextView.center = originalCenter
      toTextView.alpha = 1.0
    }, completion: nil)
  }
}

extension CircularTransition: CAAnimationDelegate {
  func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
    context?.completeTransition(true)
  }
}
3. AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  
  var window: UIWindow?
  let transitionCoordinator = TransitionCoordinator()
  
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    let whiteVC = WhiteViewController()
    
    window = UIWindow(frame: UIScreen.main.bounds)
    let nav = UINavigationController(rootViewController: whiteVC)
    nav.isNavigationBarHidden = true

    //Add TransitionCoordinator as navigation controller's delegate
    nav.delegate = transitionCoordinator
    
    window?.rootViewController = nav
    window?.makeKeyAndVisible()
    
    return true
  }
}
4. ColoredViewController.swift
import UIKit

enum Color {
  case white
  case black
}

class ColoredViewController: UIViewController, CircleTransitionable {    
  var mainView: UIView {
    return view
  }

  let triggerButton = UIButton()
  let contentTextView = UITextView()

  let color: Color
  
  init(color: Color) {
    self.color = color
    
    super.init(nibName: nil, bundle: nil)
  }
  override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = color(for: color)
    contentTextView.isUserInteractionEnabled = false
    
    triggerButton.addTarget(self, action: #selector(buttonWasTapped), for: .touchUpInside)
    
    self.view.addSubview(contentTextView)
    self.view.addSubview(triggerButton)
  }
  
  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    view.setNeedsLayout()
  }
  
  func color(for color: Color) -> UIColor {
    switch color {
    case .white:
      return .white
    case .black:
      return .black
    }
  }
  
  @objc func buttonWasTapped() {
    assertionFailure("This method should be implemented in subclasses")
  }
  
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}
5. WhiteViewController.swift
import UIKit

class WhiteViewController: ColoredViewController {
  init() {
    super.init(color: .white)
  }
  override func buttonWasTapped() {
    let vc = BlackViewController()
    navigationController?.pushViewController(vc, animated: true)
  }
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}
6. WhiteViewControllerLayout.swift
import UIKit

extension WhiteViewController {
  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    triggerButton.backgroundColor = .black
    contentTextView.backgroundColor = .clear
    
    let titleAttributes = [NSAttributedStringKey.foregroundColor: UIColor.black,
                           NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 18.0)]
    let storyAttributes = [NSAttributedStringKey.foregroundColor: UIColor.lightGray,
                           NSAttributedStringKey.font: UIFont.systemFont(ofSize: 16.0)]
    
    let mutableAttrString = NSMutableAttributedString(string: "Beep boop, you've unlocked a new\n something or other that I call \"Tech Talk\"\n\n", attributes:titleAttributes)
    mutableAttrString.append(NSAttributedString(string: "Hi, I'm Pong. Tap the black dot,\n choose stuff you care about and close\n me when you're done. You're going to\n like me.\n\nTouch me.", attributes:storyAttributes))
    
    contentTextView.attributedText = mutableAttrString
  }
  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    
    let buttonWidthHeight: CGFloat = 25.0
    let padding: CGFloat = 16.0
    let statusBarPadding: CGFloat = 20.0
    
    let constrainedSize = CGSize(width: view.bounds.width - 32.0 - buttonWidthHeight, height: view.bounds.height)
    let titleSize = contentTextView.sizeThatFits(constrainedSize)
    
    contentTextView.bounds = CGRect(x: 0, y: 0, width: titleSize.width, height: titleSize.height)
    
    contentTextView.center = CGPoint(x: 16 + contentTextView.bounds.width/2.0, y: 60 + contentTextView.bounds.height/2.0)
    
    triggerButton.layer.cornerRadius = buttonWidthHeight/2.0
    triggerButton.frame = CGRect(x: view.bounds.width - buttonWidthHeight - padding, y: padding + statusBarPadding, width: buttonWidthHeight, height: buttonWidthHeight)
  }
}
7. BlackViewController.swift
import UIKit

class BlackViewController: ColoredViewController {
  init() {
    super.init(color: .black)
  }
  override func buttonWasTapped() {
    navigationController?.popViewController(animated: true)
  }
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}
8. BlackViewControllerLayout.swift
import UIKit

extension BlackViewController {
  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    triggerButton.backgroundColor = .white
    contentTextView.backgroundColor = .clear
    
    let titleAttributes = [NSAttributedStringKey.foregroundColor: UIColor.white,
                           NSAttributedStringKey.font: UIFont.systemFont(ofSize: 18.0)]
    
    let string = """
                        Apps worth downloading\n\n
                        Best of Hacker News\n\n
                        Curiosities\n\n
                        Daily Fortune Cookies\n\n
                        Fitspiration\n\n
                        Is it Friday yet?\n\n
                        Movies worth mocking\n\n
                    """
    let mutableAttrString = NSMutableAttributedString(string: string, attributes:titleAttributes)
    
    contentTextView.attributedText = mutableAttrString
  }
  
  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    
    triggerButton.backgroundColor = .white
    contentTextView.backgroundColor = .black
    
    let buttonWidthHeight: CGFloat = 25.0
    let padding: CGFloat = 16.0
    let statusBarPadding: CGFloat = 20.0
    
    let constrainedSize = CGSize(width: view.bounds.width - 32.0 - buttonWidthHeight, height: view.bounds.height)
    let titleSize = contentTextView.sizeThatFits(constrainedSize)
    
    contentTextView.bounds = CGRect(x: 0, y: 0, width: titleSize.width, height: titleSize.height)
    contentTextView.center = CGPoint(x: 16 + contentTextView.bounds.width/2.0, y: 60 + contentTextView.bounds.height/2.0)
    
    triggerButton.layer.cornerRadius = buttonWidthHeight/2.0
    triggerButton.frame = CGRect(x: view.bounds.width - buttonWidthHeight - padding, y: padding + statusBarPadding, width: buttonWidthHeight, height: buttonWidthHeight)
  }
  
  override var preferredStatusBarStyle: UIStatusBarStyle {
    return .lightContent
  }
}

后記

本篇主要講述了UIViewController間轉(zhuǎn)場動畫的實現(xiàn)照宝,感興趣的給個贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市句葵,隨后出現(xiàn)的幾起案子厕鹃,更是在濱河造成了極大的恐慌,老刑警劉巖乍丈,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剂碴,死亡現(xiàn)場離奇詭異,居然都是意外死亡诗赌,警方通過查閱死者的電腦和手機(jī)汗茄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來铭若,“玉大人,你說我怎么就攤上這事递览〉鹜溃” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵绞铃,是天一觀的道長镜雨。 經(jīng)常有香客問我,道長儿捧,這世上最難降的妖魔是什么荚坞? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮菲盾,結(jié)果婚禮上颓影,老公的妹妹穿的比我還像新娘。我一直安慰自己懒鉴,他們只是感情好诡挂,可當(dāng)我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布碎浇。 她就那樣靜靜地躺著,像睡著了一般璃俗。 火紅的嫁衣襯著肌膚如雪奴璃。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天城豁,我揣著相機(jī)與錄音苟穆,去河邊找鬼。 笑死唱星,一個胖子當(dāng)著我的面吹牛鞭缭,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播魏颓,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼岭辣,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了甸饱?” 一聲冷哼從身側(cè)響起沦童,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎叹话,沒想到半個月后偷遗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡驼壶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年氏豌,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片热凹。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡泵喘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出般妙,到底是詐尸還是另有隱情纪铺,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布碟渺,位于F島的核電站鲜锚,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏苫拍。R本人自食惡果不足惜芜繁,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望绒极。 院中可真熱鬧骏令,春花似錦、人聲如沸集峦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至摘昌,卻和暖如春速妖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背聪黎。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工罕容, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人稿饰。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓锦秒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親喉镰。 傳聞我的和親對象是個殘疾皇子旅择,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內(nèi)容