版本記錄
版本號 | 時間 |
---|---|
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:NSObject
和Language: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)前的nil
return語句:
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)定義了triggerButton
和contentTextView
,所以它已經(jīng)接近準(zhǔn)備好了横堡。 您需要做的最后一件事是為mainView
屬性添加一個計算屬性埋市。 在定義contentTextView
之后立即添加以下內(nèi)容:
var mainView: UIView {
return view
}
在這里,您所要做的就是返回視圖控制器的默認(rèn)view
屬性命贴。
該項目包含一個BlackViewController
和WhiteViewController
道宅,它在應(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)制作動畫而不必在完成后將其放回原處章喉,則快照是一種理想的解決方案。
在你的guard
的else
子句中身坐,你在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è)置此動畫的from
和to
值距贷。
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")
您需要再次指定要為maskLayer
的path
設(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)注~~~