本文是我學習《iOS Animations by Tutorials》 筆記中的一篇。
文中詳細代碼都放在個人Github上 andyRon/LearniOSAnimations。html
UIViewPropertyAnimator
是從iOS10開始引入,它可以建立易於交互,可中斷和/或可逆的視圖動畫。ios
這個類讓某些類型的視圖動畫更容易建立,值得學習。git
UIViewPropertyAnimator
能夠在同一個類中方便地將許多API包裝在一塊兒,這樣更容易使用。github
此外,這個新類不能徹底取代了UIView.animate(withDuration...)
API集。spring
內容預覽:swift
20-UIViewPropertyAnimator入門設計模式
22-用UIViewPropertyAnimator進行交互式動畫app
23-用UIViewPropertyAnimator自定義視圖控制器轉場框架
本文的四個章節都是使用同一個項目 LockSearch
在iOS10以前,建立基於視圖的動畫的惟一選擇是UIView.animate(withDuration: ...)
I,但這組API沒有爲開發人員提供暫停或中止已經運行的動畫的處理方式。此外,對於反轉,加速或減慢動畫,開發人員只能使用基於圖層的CAAnimation
(核心動畫)。
UIViewPropertyAnimator
就是爲了解決上述問題而出現的,它是一個容許保持運行動畫的類,容許開發者調整當前運行的動畫,並提供有關動畫當前狀態的詳細信息。
固然,簡單單一的視圖動畫直接使用UIView.animate(withDuration: ...)
就能夠了。
本章的開始項目 LockSearch 。 相似於iOS鎖屏時的屏幕。 初始視圖控制器有搜索欄,單個窗口小部件和編輯按鈕等:
開始項目 已經實現了一些與動畫無關的功能。 例如,若是點擊Show More按鈕,窗口小部件將展開並顯示更多項目。 若是點擊編輯,會轉到另外一個視圖控制器,這是一個簡單的TableView。
固然,該項目只是模擬iOS中的鎖定屏幕,用來學習動畫,沒有實際的功能,。
打開LockScreenViewController.swift
並向該視圖控制器添加一個新的viewWillAppear(_:)
方法:
override func viewWillAppear(_ animated: Bool) {
tableView.transform = CGAffineTransform(scaleX: 0.67, y: 0.67)
tableView.alpha = 0
}
複製代碼
爲了建立簡單的縮放和淡入淡出視圖動畫,首先縮小整個表視圖並使其透明。
接下來,在視圖控制器的視圖出如今屏幕上時建立一個動畫師。 將如下內容添加到LockScreenViewController
:
override func viewDidAppear(_ animated: Bool) {
let scale = UIViewPropertyAnimator.init(duration: 0.33, curve: .easeIn) {
}
}
複製代碼
在這裏,您使用UIViewPropertyAnimator
的一個便利構造器:UIViewPropertyAnimator.init(duration:curve:animations:)
。
經過構造器建立動畫實例並設置動畫的總持續時間和時間曲線。 後一個參數的類型爲UIViewAnimationCurve
,這是一個枚舉類型,有四個類型:easeInOut
、easeIn
、easeOut
、linear
。這與UIView.animate(withDuration:...)
中的option
是相似的。
在viewDidAppear(_:)
中添加:
scale.addAnimations {
self.tableView.alpha = 1.0
}
複製代碼
使用addAnimations
添加動畫代碼塊,就像UIView.animate(withDuration...)
的閉包參數animations
。 使用動畫師的不一樣之處在於能夠添加多個動畫塊。
除了可以有條件地構建複雜的動畫外,還能夠添加具備不一樣延遲的動畫。 另外一個版本的addAnimations
,有兩個參數: animation
動畫代碼 delayFactor
動畫開始前的延遲
delayFactor
與UIView.animate(withDuration...)
中delay
不一樣,它介於0.0到1.0,不是絕對時間是相對時間。
在同一個動畫師添加第二個動畫,但有一些延遲。繼續在上面的代碼後添加:
scale.addAnimations({
self.tableView.transform = .identity
}, delayFactor: 0.33)
複製代碼
實際延遲時間是delayFactor
乘以動畫師的剩餘持續時間(remaining duration)。 目前還沒有啓動動畫,所以剩餘持續時間等於總持續時間。 因此在上面的狀況:
delayFactor(0.33) * remainingDuration(=duration 0.33) = delay of 0.11 seconds
複製代碼
爲何第二個參數不是一個簡單的秒數值? 想象動畫師已經在運行了,你決定在中途添加一些新的動畫。 在這種狀況下,剩餘持續時間不會等於總持續時間,由於自啓動動畫以來已通過了一段時間。
在這種狀況下,delayFactor
將容許開發者根據剩餘可用時間設置延遲動畫。 此外,這樣設計也確保了不能將延遲設置爲長於剩餘運行時間。
在viewDidAppear(_:)
中添加:
scale.addCompletion { (_) in
print("ready")
}
複製代碼
addCompletion(_:)
就是動畫完成閉包,固然,它也可屢次調用,來完成多了處理程序。
下面要啓動動畫,在viewWillAppear(_:)
的末尾添加:
scale.startAnimation()
複製代碼
爲了代碼的清晰,能夠把動畫代碼集中放到一個類中。
建立一個名爲AnimatorFactory.swift
的新文件,並將其默認內容替換爲:
import UIKit
class AnimatorFactory {
}
複製代碼
而後添加一個類型方法,其中包含剛剛編寫的動畫代碼,但默認狀況下不運行動畫,而是返回動畫師:
static func scaleUp(view: UIView) -> UIViewPropertyAnimator {
let scale = UIViewPropertyAnimator(duration: 0.33, curve: .easeIn)
scale.addAnimations {
view.alpha = 1.0
}
scale.addAnimations({
view.transform = .identity
}, delayFactor: 0.33)
scale.addCompletion { (_) in
print("ready")
}
return scale
}
複製代碼
該方法將視圖做爲參數,並在該視圖上建立全部動畫,最後它返回準備好的動畫師。
將LockScreenViewController
中的viewDidAppear(_:)
替換爲:
override func viewDidAppear(_ animated: Bool) {
AnimatorFactory.scaleUp(view: tableView).startAnimation()
}
複製代碼
這樣看上去代碼更加簡潔,清晰,把動畫代碼從視圖控制器移出。
這個動畫師工廠🏭類AnimatorFactory
集中處理動畫代碼,這是設計模式中的工廠模式的一個簡單應用。😀
當用戶使用搜索欄時,將淡入模糊圖層(blurView),並在用戶完成搜索時將其淡出。
向LockScreenViewController
類添加一個新方法:
func toggleBlur(_ blurred: Bool) {
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.5, delay: 0.1, options: .curveEaseOut, animations: {
self.blurView.alpha = blurred ? 1 : 0
}, completion: nil)
}
複製代碼
UIViewPropertyAnimator.runningPropertyAnimator(withDuration:...)
與UIView.animate(withDuration:...)
有徹底相同的參數,使用也相同。
雖然看起來這多是一種**「即發即忘」**(「fire-and-forget」 )的API,但請注意它確實會返回一個動畫實例。 所以,您能夠添加更多動畫,更多完成塊,而且一般與當前正在運行的動畫進行交互。
如今讓咱們看看淡入淡出動畫的樣子。 LockScreenViewController已設置爲搜索欄的委託,所以您只需實現所需的方法便可在正確的時間觸發動畫。
以擴展的方式爲LockScreenViewController
遵照搜索欄的代理協議:
extension LockScreenViewController: UISearchBarDelegate {
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
toggleBlur(true)
}
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
toggleBlur(false)
}
}
複製代碼
要爲用戶提供取消搜索的功能,還要添加如下兩種方法:
func searchBarResultsListButtonClicked(_ searchBar: UISearchBar) {
searchBar.resignFirstResponder()
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchText.isEmpty{
searchBar.resignFirstResponder()
}
}
複製代碼
這將容許用戶經過點擊右側按鈕解除搜索。
運行,效果:
點按搜索欄文本字段,小部件在模糊效果視圖下消失;點擊搜索欄右側的按鈕時,模糊視圖會淡出。
UIViewPropertyAnimator
也可使用UIView.addKeyframe
(5-視圖的關鍵幀動畫)。下面建立一個簡單的圖標抖動動畫來展現。
在AnimatorFactory
中添加類型方法:
static func jiggle(view: UIView) -> UIViewPropertyAnimator {
return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.33, delay: 0
, animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.25, animations: {
view.transform = CGAffineTransform(rotationAngle: -.pi/8)
})
UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.75, animations: {
view.transform = CGAffineTransform(rotationAngle: +.pi/8)
})
UIView.addKeyframe(withRelativeStartTime: 0.75, relativeDuration: 1.0, animations: {
view.transform = CGAffineTransform.identity
})
}, completion: { (_) in
})
}
複製代碼
第一個關鍵幀向左旋轉,第二個關鍵幀向右旋轉,最後第三個關鍵幀回到原點 。
要確保圖標保持在其初始位置,在完成閉包中添加:
view.transform = .identity
複製代碼
下面就能夠在想要運行這個動畫的視圖上添加動畫了。
打開IconCell.swift
(該文件位於Widget子文件夾中)。這是自定義單元類,對應於窗口小部件視圖中的每一個圖標。 在IconCell
中添加:
func iconJiggle() {
AnimatorFactory.jiggle(view: icon)
}
複製代碼
如今Xcode抱怨AnimatorFactory.jiggle
方法返回一個結果沒有被使用,這是Xcode善意的提醒😊。
這個問題很容易解決,只須要在jiggle
方法前添加@discardableResult
,讓Xcode知道這個方法的結果我不要了😏。
discardableResult
的官方解釋:Apply this attribute to a function or method declaration to suppress the compiler warning when the function or method that returns a value is called without using its result.
@discardableResult
static func jiggle(view: UIView) -> UIViewPropertyAnimator {
複製代碼
要最終運行動畫,在WidgetView.swift
的collectionView(_:didSelectItemAt:)
中添加:
if let cell = collectionView.cellForItem(at: indexPath) as? IconCell {
cell.iconJiggle()
}
複製代碼
效果:
把前面的模糊動畫也提取到AnimatorFactory
中。
@discardableResult
static func fade(view: UIView, visible: Bool) -> UIViewPropertyAnimator {
return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.5, delay: 0.1, options: .curveEaseOut, animations: {
view.alpha = visible ? 1.0 : 0.0
}, completion: nil)
}
複製代碼
替代LockScreenViewController
中的toggleBlur(_:)
方法:
func toggleBlur(_ blurred: Bool) {
AnimatorFactory.fade(view: blurView, visible: blurred)
}
複製代碼
如何檢查動畫師當前是否正在執行其動畫?
若是在同一個圖標上快速連續點擊,會發現抖動動畫沒有結束就從新開始了。
解決這個問題,就須要檢測視圖是否有動畫正在運行。
爲IconCell
添加一個屬性,並修改iconJiggle()
:
var animator: UIViewPropertyAnimator?
func iconJiggle() {
if let animator = animator, animator.isRunning {
return
}
animator = AnimatorFactory.jiggle(view: icon)
}
複製代碼
對比能夠發現有所不一樣:
上一章節學習了UIViewPropertyAnimator
的基本使用,這一章節學習更多關於UIViewPropertyAnimator
的知識。
本章的開始項目 使用上一章節完成的項目。
前文已經屢次提到:easeInOut
、easeIn
、easeOut
、linear
(能夠理解爲物體運動軌跡的曲線類型)。能夠參考視圖動畫中的動畫緩動 或者圖層動畫中的動畫緩動,這邊就再也不介紹了。
目前,當您激活搜索欄時,您會在窗口小部件頂部的模糊視圖中淡入淡出。 在此示例中,您將刪除該淡入淡出動畫併爲模糊效果自己設置動畫。
以前,激活搜索欄時,就會有一個模糊視圖中淡入淡出效果。這個部分刪除這個效果,修改爲對模糊效果自己設置動畫。什麼意思呢? 看完下面的操做,應該能明白。
向LockScreenViewController
類添加一個新方法:
func blurAnimations(_ blured: Bool) -> () -> Void {
return {
self.blurView.effect = blured ? UIBlurEffect(style: .dark) : nil
self.tableView.transform = blured ? CGAffineTransform(scaleX: 0.75, y: 0.75) : .identity
self.tableView.alpha = blured ? 0.33 : 1.0
}
}
複製代碼
刪除viewDidLoad()
中的兩行代碼:
blurView.effect = UIBlurEffect(style: .dark)
blurView.alpha = 0
複製代碼
替代toggleBlur(_:)
內容爲:
func toggleBlur(_ blurred: Bool) {
UIViewPropertyAnimator(duration: 0.55, curve: .easeOut, animations: blurAnimations(blurred)).startAnimation()
}
複製代碼
運行,效果:
請注意模糊不只僅是淡入或淡出,實際上它會在效果視圖中插入模糊量。
有時想要對動畫的時間很是具體時,使用這些曲線簡單地「開始減速」或「慢慢結束」是不夠的。
在10-動畫組和時間控制 中學習了使用CAMediaTimingFunction
控制圖層動畫的時間。
以前沒有了解背後的原理貝塞爾曲線,這邊介紹一下它。這邊的內容也可應用到圖層動畫中。
貝塞爾曲線是什麼?
讓咱們從簡單的事情開始 —— 一條線。它很是簡潔,須要在屏幕上畫一條線,只須要定義它的兩個點的座標,開始 (A) 和結束 (B):
如今讓咱們來看看曲線。曲線比線條更有趣,由於它們能夠在屏幕上繪製任何東西。例如:
在上面看到的是四條曲線放在一塊兒;它們的兩端在小白方塊的地方相遇。圖中有趣的是小綠圈,它們定義了每條曲線。
因此曲線不是隨機的。它們也有一些細節,就像線條同樣,能夠幫助咱們經過座標定義它們。
您能夠經過向線條添加控制點來定義曲線。 讓咱們在以前的行中添加一個控制點:
能夠想象由鏈接到線的鉛筆繪製的曲線,其起點沿着線AC移動,其終點沿着線CB移動:
網上找了一個動圖:
具備一個控制點的Bézier曲線稱爲 二次曲線。有兩個控制點的Bézier曲線叫作 三次曲線(立方貝塞爾曲線)。 咱們使用的內置曲線就是三次曲線。
核心動畫使用始終以座標(0,0)開始的三次曲線,它表示動畫持續時間的開始。 固然,這些時間曲線的終點始終是(1,1),表示 動畫的持續時間和進度的結束。
讓咱們來看看 ease-in 曲線:
隨着時間的推移(在座標空間中從左向右水平移動),曲線在垂直軸上的進展很是小,而後大約在動畫持續時間的一半時間後,曲線在垂直軸上的進展很是大,最終在(1, 1)處結束。
ease-out 和 ease-in-out曲線分別是:
如今已瞭解Bézier曲線的工做原理,剩下的問題是如何在視覺上設計一些曲線並得到控制點的座標,方即可以將它們用於iOS動畫。
可使用網站:cubic-bezier.com。 這是計算機科學研究員和演講者Lea Verou的很是方便的網站。 它能夠拖動立方Bézier的兩個控制點並查看即時動畫預覽,很是nice😊😊。
上面貝塞爾的原理說的不夠深入🤦♀️,如今只需瞭解曲線,經過兩個控制點能夠畫曲線。
接下來,向項目中添加自定義計時動畫。
把LockScreenViewController
中的toggleBlur()
的現有動畫替換爲:
func toggleBlur(_ blurred: Bool) {
UIViewPropertyAnimator(duration: 0.55, controlPoint1: CGPoint(x: 0.57, y: -0.4), controlPoint2: CGPoint(x: 0.96, y: 0.87), animations: blurAnimations(blurred)).startAnimation()
}
複製代碼
這邊的controlPoint1
和controlPoint2
兩個點,就是咱們自定義三次曲線的控制點。
能夠經過 cubic-bezier.com 網站來選着控制點。
另外一個便利構造器UIViewPropertyAnimator(duration:dampingRatio:animations:)
,用於定義彈簧動畫。
這與UIView.animate(withDuration: delay: usingSpringWithDamping: initialSpringVelocity: options: animations: completion:)
相似,只不過初始速度爲0。
UIViewPropertyAnimator
類還有一個構造器UIViewPropertyAnimator(duration:timingParameters:)
。
參數timingParameters
必須遵照UITimingCurveProvider
協議,有兩個類可供咱們使用:UICubicTimingParameters
和UISpringTimingParameters
。
下面看看這個構造器的使用方式。
添加阻尼和速度的方式以下:
let spring = UISpringTimingParameters(dampingRatio:0.5, initialVelocity: CGVector(dx: 1.0, dy: 0.2))
let animator = UIViewPropertyAnimator(duration: 1.0, timingParameters: spring)
複製代碼
注意初始速度initialVelocity
是矢量類型,這個參數是一個可選參數。
若是想對彈簧動畫更加具體的設置,能夠UISpringTimingParameters
的另外一個構造器init(mass:stiffness:damping:initialVelocity:)
,代碼以下:
let spring = UISpringTimingParameters(mass: 10.0, stiffness: 5.0, damping: 30, initialVelocity: CGVector(dx: 1.0, dy: 0.2))
let animator = UIViewPropertyAnimator(duration: 1.0, timingParameters: spring)
複製代碼
上面這些參數的工做原理,能夠查看以前的文章11-圖層彈簧動畫。
前面的文章系統學習iOS動畫之二:自動佈局動畫 學習了自動佈局動畫。
使用UIViewPropertyAnimator
的佈局約束動畫與使用UIView.animate(withDuration: ...)
建立它們的方式很是類似。 訣竅是更新約束,在動畫塊中調用layoutIfNeeded()
。
在AnimatorFactory
中添加一個新的工廠方法:
@discardableResult
static func animateConstraint(view: UIView, constraint: NSLayoutConstraint, by: CGFloat) -> UIViewPropertyAnimator {
let spring = UISpringTimingParameters(dampingRatio: 0.55)
let animator = UIViewPropertyAnimator(duration: 1.0, timingParameters: spring)
animator.addAnimations {
constraint.constant += by
view.layoutIfNeeded()
}
return animator
}
複製代碼
在LockScreenViewController
中viewWillAppear
裏添加:
dateTopConstraint.constant -= 100
view.layoutIfNeeded()
複製代碼
在viewDidAppear
裏添加:
AnimatorFactory.animateConstraint(view: view, constraint: dateTopConstraint, by: 150).startAnimation()
複製代碼
這讓時間標籤的位置,在應用打開時有一個動畫。
接下來,在添加一個約束動畫。當點擊「Show more」時,窗口小部件會加載內容,並須要更改其高度約束。
從新定義WidgetCell.swift
中的toggleShowMore(_:)
方法:
@IBAction func toggleShowMore(_ sender: UIButton) {
self.showsMore = !self.showsMore
let animations = {
self.widgetHeight.constant = self.showsMore ? 230 : 130
if let tableView = self.tableView {
tableView.beginUpdates()
tableView.endUpdates()
tableView.layoutIfNeeded()
}
}
let spring = UISpringTimingParameters(mass: 30, stiffness: 10, damping: 300, initialVelocity: CGVector(dx: 5, dy: 0))
toggleHeightAnimator = UIViewPropertyAnimator(duration: 0.0, timingParameters: spring)
toggleHeightAnimator?.addAnimations(animations)
toggleHeightAnimator?.startAnimation()
}
複製代碼
在toggleShowMore(_:)
方法的底部,添加如下代碼用來加載窗口小部件中的圖標:
widgetView.expanded = showsMore
widgetView.reload()
複製代碼
在視圖動畫的3-過渡動畫,學習了視圖過渡。如今用UIViewPropertyAnimator
作視圖過渡。
顯示更多按鈕的title,"Show More" 和 "Show Less" 二者相互淡入淡出動畫。
在toggleShowMore(_ :)
的toggleHeightAnimator
定義以前添加這段代碼:
let textTransition = {
UIView.transition(with: sender, duration: 0.25, options: .transitionCrossDissolve, animations: {
sender.setTitle(self.showsMore ? "Show Less" : "Show More", for: .normal)
}, completion: nil)
}
複製代碼
在toggleHeightAnimator
開始以前添加:
toggleHeightAnimator?.addAnimations(textTransition, delayFactor: 0.5)
複製代碼
這將改變按鈕標題,具備很好的交叉淡入淡出效果:
效果也能夠嘗試.transitionFlipFromTop
等
前面兩個章節介紹了許多UIViewPropertyAnimator
的使用,例如基本動畫,自定義計時和彈簧動畫,以及動畫的提取。可是,與之前視圖動畫 「即發即忘」("fire-and-forget")API相比,還沒有研究使UIViewPropertyAnimator
真正有趣的地方。
UIView.animate(withDuration:...)
提供了動畫的設置方法,可是一旦定義動畫結束狀態,那麼動畫就會開始執行,而沒法控制。
可是若是咱們想在動畫運行時與之交互,怎麼辦? 細說,就是動畫不是靜態的,而是由用戶手勢或麥克風輸入驅動的,就像在前面圖層動畫 系統學習iOS動畫之三:圖層動畫 所學的同樣。
使用UIViewPropertyAnimator
建立的動畫是徹底交互式的:能夠啓動,暫停,改變速度,甚至能夠直接調整進度。
因爲UIViewPropertyAnimator
能夠同時驅動預設動畫和交互式動畫,於是在描述動畫師當前的狀態時,就有點複雜了😵。下面就看看如何處理動畫師的狀態。
本章的開始項目 使用上一章節完成的項目。
UIViewPropertyAnimator
能夠檢查動畫是否已啓動(isRunning
),是否已暫停或徹底中止(state
),或動畫是否已顛倒(isReversed
)。
UIViewPropertyAnimator
有三個描述當前狀態的屬性:
isRunning
(只讀):動畫當前是否處於運動狀態。 默認爲false
,在調用startAnimation()
時變爲true
,若是暫停或中止動畫,或者動畫天然完成,它將再次變爲false
。
isReversed
:默認爲false
,由於咱們老是向前開始動畫,即動畫從開始狀態播放到結束狀態。 若是更改成true
,則動畫將顛倒,即從介紹狀態到開始狀態。
state
(只讀):
state
默認爲inactive
,這一般意味着剛剛建立了動畫師,而且尚未調用任何方法。請注意,這與將isRunning
設置爲false
不一樣,isRunning
實際上只關注正在進行的動畫,而當state
處於inactive
時,這實際上意味着動畫師尚未作任何事情。
state
變成 active
的狀況有:
startAnimation()
來啓動動畫pauseAnimation()
fractionComplete
屬性以將動畫「倒回」到某個位置動畫天然完成後,state
切換回.inactive
。
若是在動畫師上調用stopAnimation()
,它會將其state
屬性設置爲.stopped
。在這種狀態下,你惟一能作的就是徹底放棄動畫師或者調用finishAnimation(at:)
來完成動畫並讓動畫師回到.inactive
。
正如你可能想到的那樣,UIViewPropertyAnimator
只能按特定順序在狀態之間切換。 它不能直接從inactive
到stopped
,也不能從stopped
直接轉爲active
。
若是設置了pausesOnCompletion
,一旦動畫師完成了動畫的運行而不是自動中止,而是暫停。 這將使咱們有機會在暫停狀態下繼續使用它。
狀態流程圖:
可能有點繞,以後的使用中,若是有疑問,能夠再回到這個部分查看。
從這個部分開始,將學習建立相似於3D touch交互的交互式動畫:
注意:對於本章項目,須要兼容3D touch的iOS設備(沒記錯的話是6S+)。
聽聞👂,3D touch這個技術會被在iPhone上取消,好吧,這邊是學習相似3D touch 的動畫,它的將來如何,就不過問了。
3D touch的動畫,能夠這樣描述:當咱們手指按壓屏幕上的圖標時,動畫交互式開始,背景愈來愈模糊,從圖標旁漸漸呈現一個菜單,這個過程會隨着手指按壓的力度變化而先後變化。
放慢的效果爲:
WidgetView.swift
中,WidgetView
經過擴展遵照UIPreviewInteractionDelegate
協議。這個協議中就包括了3D touch過程當中一些委託方法。
爲了讓您開始開發動畫自己,UIPreviewInteractionDelegate
方法已經鏈接到LockScreenViewController上調用相關方法。 WidgetView中的代碼以下:
LockScreenViewController.startPreview(for:)
。LockScreenViewController.updatePreview(percent:)
。LockScreenViewController.finishPreview()
。LockScreenViewController.cancelPreview()
。在LockScreenViewController
中添加這三個屬性,您須要這些屬性來建立窺視交互:
var startFrame: CGRect?
var previewView: UIView?
var previewAnimator: UIViewPropertyAnimator?
複製代碼
startFrame
來跟蹤動畫的開始位置。
previewView
圖標的快照視圖,動畫期間暫時使用它。 previewAnimator
將成爲驅動預覽動畫的交互式動畫師。
再添加一個屬性以保持模糊效果以顯示圖標框:
let previewEffectView = IconEffectView(blur: .extraLight)
複製代碼
IconEffectView
是自定義的UIVisualEffectView
的子類,它包含單個標籤的簡單模糊視圖,使用它來模擬從按下的圖標彈出的菜單:
在LockScreenViewController
遵照WidgetsOwnerProtocol
協議的擴展中,實現startPreview(for:)
方法:
func startPreview(for forView: UIView) {
previewView?.removeFromSuperview()
previewView = forView.snapshotView(afterScreenUpdates: false)
view.insertSubview(previewView!, aboveSubview: blurView)
}
複製代碼
WidgetsOwnerProtocol
協議是一個自定義協議。
只要用戶開始按下圖標,WidgetView
就會調用startPreview(for:)
。 參數for
是用戶開始手勢的集合單元格圖像。
首先刪除任何現有的previewView
視圖,以防萬一在屏幕上留下以前的視圖。 而後,您能夠建立集合視圖圖標的快照,最後將其添加到模糊效果視圖上方的屏幕上。
運行,按壓圖標。發現圖標出如今左上角!😰
由於還沒有設置其位置。 繼續添加:
previewView?.frame = forView.convert(forView.bounds, to: view)
startFrame = previewView?.frame
addEffectView(below: previewView!)
複製代碼
如今圖標副本位置正確了,徹底覆蓋在原有圖標上。 startFrame
用來存儲起始frame
,以供以後使用。
函數addEffectView(below:)
添加圖標快照下方的模糊框。代碼爲:
func addEffectView(below forView: UIView) {
previewEffectView.removeFromSuperview()
previewEffectView.frame = forView.frame
forView.superview?.insertSubview(previewEffectView, belowSubview: forView)
}
複製代碼
下面建立動畫自己,在AnimatorFactory
中添加類方法:
static func grow(view: UIVisualEffectView, blurView: UIVisualEffectView) -> UIViewPropertyAnimator {
view.contentView.alpha = 0
view.transform = .identity
let animator = UIViewPropertyAnimator(duration: 0.5, curve: .easeIn)
return animator
}
複製代碼
兩個參數,view
是動畫視圖,blurView
是動畫的模糊背景。
在返回動畫師以前,爲動畫師添加動畫和完成閉包:
animator.addAnimations {
blurView.effect = UIBlurEffect(style: .dark)
view.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
}
animator.addCompletion { (_) in
blurView.effect = UIBlurEffect(style: .dark)
}
複製代碼
動畫代碼爲blurView
建立了模糊過渡,爲view
建立一個普通的轉換。
以後,在LockScreenViewController.swift
的startPreview()
中完成調用:
previewAnimator = AnimatorFactory.grow(view: previewEffectView, blurView: blurView)
複製代碼
如今運行,尚未效果,還須要實現updatePreview(percent:)
方法:
func updatePreview(percent: CGFloat) {
previewAnimator?.fractionComplete = max(0.01, min(0.99, percent))
}
複製代碼
當WidgetView
被按壓時,上面個方法會被重複調用。fractionComplete
在0.01和0.99範圍內,由於我不但願在動畫才這段結束,我另外指定的方法完成或取消動畫。
運行,效果(放慢):
你會(驚喜!)須要更多的動畫師。 打開AnimatorFactory.swift並添加一個動畫師,它能夠解除你的「成長」動畫師所作的一切。 您須要此動畫師的一種狀況是用戶取消手勢。 當您須要清理UI時,另外一個是成功交互的最後階段。
在AnimatorFactory
中添加方法:
static func reset(frame: CGRect, view: UIVisualEffectView, blurView: UIVisualEffectView) -> UIViewPropertyAnimator {
return UIViewPropertyAnimator(duration: 0.5, dampingRatio: 0.7, animations: {
view.transform = .identity
view.frame = frame
view.contentView.alpha = 0
blurView.effect = nil
})
}
複製代碼
此方法的三個參數分別是原始動畫的起始幀,動畫視圖和背景模糊視圖。 動畫塊將重置交互開始以前狀態中的全部屬性。
在LockScreenViewController.swift
中,實現WidgetsOwnerProtocol
協議的另外一個方法:
func cancelPreview() {
if let previewAnimator = previewAnimator {
previewAnimator.isReversed = true
previewAnimator.startAnimation()
}
}
複製代碼
cancelPreview()
是WidgetView
被按壓後,忽然擡起手指時調用的方法,取消正在進行的手勢。
到目前爲止,你尚未開始你的動畫師。 您一直在重複設置fractionComplete
,這會以交互方式驅動動畫。 可是,一旦用戶取消交互,您就沒法繼續以交互方式驅動動畫,由於您沒有更多輸入。 相反,經過將isReversed
設置爲true
並調用startAnimation()
,能夠將動畫播放到其初始狀態。 如今這是UIView.animate(withDuration: ...)
沒法作到的事情!
再試一次互動。按下動畫的一半,而後開始測試cancelPreview()
。
當您擡起手指時動畫會正確播放,但最終黑暗模糊會忽然從新出現。
這個問題植根於你的成長動畫師的代碼。切換回AnimatorFactory.swift並查看grow中的代碼(view:UIVisualEffectView,blurView:UIVisualEffectView) - 更具體地說,這部分:
animator.addCompletion { (_) in
blurView.effect = UIBlurEffect(style: .dark)
}
複製代碼
動畫能夠向前或向後播放,須要在完成閉包中處理。
addCompletion()
的閉包的參數用_
省略掉了,它實際上是一個枚舉類型UIViewAnimatingPosition
,表示動畫當前進行的狀況。它的值可有三個,能夠是.start
,.end
或.current
。
將完成閉包替代爲:
animator.addCompletion { (position) in
switch position {
case .start:
blurView.effect = nil
case .end:
blurView.effect = UIBlurEffect(style: .dark)
default:
break
}
}
複製代碼
若是動畫被返回,則刪除模糊效果。 若是成功完成,則明確將效果調整爲暗模糊效果。
如今有一個新問題。 若是取消對某個圖標上的按壓,則沒法再按下它! 這是由於圖標快照仍然位於原始圖標上方,擋住按壓手勢操做。 要解決該問題,值須要在重置動畫完成後當即刪除快照。
在LockScreenViewController.swift
的cancelPreview()
中繼續添加:
previewAnimator.addCompletion { (position) in
switch position {
case .start:
self.previewView?.removeFromSuperview()
self.previewEffectView.removeFromSuperview()
default:
break
}
}
複製代碼
注意:,addCompletion(_:)
能夠調用屢次,不會被下一個替代。
讓咱們再添加一個動畫師來顯示圖標菜單。 切換到AnimatorFactory.swift並添加到它:
static func complete(view: UIVisualEffectView) -> UIViewPropertyAnimator {
return UIViewPropertyAnimator(duration: 0.3, dampingRatio: 0.7, animations: {
view.contentView.alpha = 1
view.transform = .identity
view.frame = CGRect(x: view.frame.minX - view.frame.minX/2.5,
y: view.frame.maxY - 140,
width: view.frame.width + 120,
height: 60)
})
}
複製代碼
這一次你建立了一個簡單的彈簧動畫師。 對於動畫師,您能夠執行如下操做:
菜單的位置根據用戶按下的圖標而變化。
您將水平位置設置爲 view.frame.minX - view.frame.minX/2.5
,若是圖標位於屏幕左側,則顯示右側菜單,若是圖標位於左側,則顯示左側菜單在屏幕的右側。請參閱如下差別:
動畫師準備好了,因此打開LockScreenViewController.swift並在WidgetsOwnerProtocol擴展中添加最後一個必需的方法:
func finishPreview() {
previewAnimator?.stopAnimation(false)
previewAnimator?.finishAnimation(at: .end)
previewAnimator = nil
}
複製代碼
當您感受到觸覺反饋時,用戶按下3D觸摸手勢時會調用finishPreview()。
stopAnimation(_:)
是中止當前在屏幕上運行的動畫。參數爲false
,動畫師狀態爲stopped
;參數爲true
,動畫師狀態爲inactive
並清除全部動畫,並且不調用完成閉包。
一旦你將動畫師置於中止狀態,你就有了一些選擇。你在finishPreview()中追求的是告訴動畫師完成它的最終狀態。所以,您調用finishAnimation(at:.end);這將使用計劃動畫的目標值更新全部視圖並調用您的完成。
此手勢再也不須要previewAnimator,所以您能夠將其刪除。
您可使用如下方法之一調用finishAnimation(at :):
start
:將動畫重置爲初始狀態。 current
:從動畫的當前進度更新視圖的屬性並完成。
調用finishAnimation(at:)
後,您的動畫師處於inactive
。
回到Widgets項目。因爲你擺脫了預覽動畫師,你能夠運行完整的動畫師來顯示菜單。將如下內容附加到finishPreview()的末尾:
AnimatorFactory.complete(view: previewEffectView).startAnimation()
複製代碼
運行,按壓圖標:
目前,菜單彈出,模糊視圖顯示後,尚未回到原來視圖的操做,下面添加這個操做。
在finishPreview()
中添加如下代碼,以準備交互式模糊:
blurView.effect = UIBlurEffect(style: .dark)
blurView.isUserInteractionEnabled = true
blurView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(dismissMenu)))
複製代碼
先確保將模糊效果設置爲.dark
,而後模糊視圖自己上啓用用戶交互,並未模糊視圖添加點擊手勢操做,容許用戶點擊圖標周圍的任何位置用來關閉菜單。
dismissMenu()
代碼爲:
@objc func dismissMenu() {
let reset = AnimatorFactory.reset(frame: startFrame!, view: previewEffectView, blurView: blurView)
reset.addCompletion { (_) in
self.previewEffectView.removeFromSuperview()
self.previewView?.removeFromSuperview()
self.blurView.isUserInteractionEnabled = false
}
reset.startAnimation()
}
複製代碼
在20-UIViewPropertyAnimator入門學習了 用UIViewPropertyAnimator
製做關鍵幀動畫,如今再給關鍵幀動畫添加交互式操做。
爲了嘗試一下,你將爲成長動畫添加一個額外的元素 - 在用戶按下圖標時以交互方式擦洗的元素。
刪除AnimatorFactory
的grow()
方法中的代碼:
animator.addAnimations {
blurView.effect = UIBlurEffect(style: .dark)
view.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
}
複製代碼
替換爲:
animator.addAnimations {
UIView.animateKeyframes(withDuration: 0.5, delay: 0.0, animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1.0, animations: {
blurView.effect = UIBlurEffect(style: .dark)
view.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
})
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5, animations: {
view.transform = view.transform.rotated(by: -.pi/8)
})
})
}
複製代碼
第一個關鍵幀運行您以前的相同動畫。 第二個關鍵幀是簡單旋轉,效果:
在系統學習iOS動畫之四:視圖控制器的轉場動畫中,學習瞭如何建立自定義視圖控制器轉場。這個章節學習使用UIViewPropertyAnimator
來自定義視圖控制器轉場。
本章的開始項目 使用上一章節完成的項目。
如今,點擊**」Edit「**按鈕時,體驗很是糟糕😰。
首先建立一個新文件PresentTransition.swift
,從名字也能看出這個類是用來轉場的。 將其默認內容替換爲:
import UIKit
class PresentTransition: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.75
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
}
}
複製代碼
UIViewControllerAnimatedTransitioning
協議已經在系統學習iOS動畫之四:視圖控制器的轉場動畫中學過。
我將建立一個轉場動畫:原視圖逐漸模糊圖,新視圖慢慢移動出來。
在PresentTransition
中添加一個新方法:
func transitionAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
let duration = transitionDuration(using: transitionContext)
let container = transitionContext.containerView
let to = transitionContext.view(forKey: .to)!
container.addSubview(to)
}
複製代碼
在上面的代碼中,爲視圖控制器轉場作了一些必要的準備工做。 首先獲取動畫持續時間,而後獲取目標視圖控制器的視圖,最後將此視圖添加到過渡容器中。
接下來,能夠設置動畫並運行它。 將下面代碼添加到上面的方法transitionAnimator(using:)
中:
to.transform = CGAffineTransform(scaleX: 1.33, y: 1.33).concatenating(CGAffineTransform(translationX: 0.0, y: 200))
to.alpha = 0
複製代碼
這會向上伸展,而後向下移動目標視圖控制器的視圖,最後將其淡出。
在to.alpha = 0
以後添加動畫師來運行轉換:
let animator = UIViewPropertyAnimator(duration: duration, curve: .easeOut)
animator.addAnimations({
to.transform = CGAffineTransform(translationX: 0.0, y: 100)
}, delayFactor: 0.15)
animator.addAnimations({
to.alpha = 1.0
}, delayFactor: 0.5)
複製代碼
動畫師中有兩個動畫:將目標視圖控制器的視圖移動到最終位置和淡入。
最後添加完成閉包:
animator.addCompletion { (_) in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
return animator
複製代碼
在animateTransition(using:)
中調用上面的方法transitionAnimator(using:)
:
transitionAnimator(using: transitionContext).startAnimation()
複製代碼
在LockScreenViewController
中定義常量屬性:
let presentTransition = PresentTransition()
複製代碼
讓LockScreenViewController
遵照UIViewControllerTransitioningDelegate
協議:
// MARK: - UIViewControllerTransitioningDelegate
extension LockScreenViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return presentTransition
}
}
複製代碼
UIViewControllerTransitioningDelegate
協議在 系統學習iOS動畫之四:視圖控制器的轉場動畫 中學習過。
animationController(forPresented:presents:source:)
方法是告訴UIKit,我想自定義視圖控制器轉場。
在LockScreenViewController
中,找到點擊Edit按鈕的ActionpresentSettings(_:)
,添加代碼:
settingsController = storyboard?.instantiateViewController(withIdentifier: "SettingsViewController") as! SettingsViewController
settingsController.transitioningDelegate = self
present(settingsController, animated: true, completion: nil)
複製代碼
運行,點擊Edit按鈕,SettingsViewController
有點問題:
在Main.storyboard
中將視圖的背景更改成Clear Color。
運行,變成:
下面向動畫師添加新屬性,爲了能夠將任何自定義動畫注入轉場動畫, 使用相同的轉場類來生成略有不一樣的動畫。
在PresentTransition
中添加兩個新屬性:
var auxAnimations: (() -> Void)?
var auxAnimationsCancel: (() -> Void)?
複製代碼
在transitionAnimator(using:)
方法中動畫師返回以前添加:
if let auxAnimations = auxAnimations {
animator.addAnimations(auxAnimations)
}
複製代碼
這樣能夠根據具體狀況在轉換中添加自定義動畫。 例如,要爲當前轉場添加模糊動畫。
打開LockScreenViewController
並在presentSettings()
的開始處插入:
presentTransition.auxAnimations = blurAnimations(true)
複製代碼
再試一次過渡,看看這一行如何改變它:
模糊動畫重複使用了。
另外,當用戶解除控制器時,還須要隱藏模糊視圖。
在presentSettings(_:)
中的present(_:animated:completion:)
前添加:
settingsController.didDismiss = { [unowned self] in
self.toggleBlur(false)
}
複製代碼
如今,運行,點擊SettingsViewController
視圖中的Cancel或其餘選項,先有的模糊視圖,而後恢復到第一個視圖控制器:
這個部分經過下拉的手勢來時學習實現交互視圖控制器轉場。
首先,讓咱們使用強大的UIPercentDrivenInteractionTransition
類來啓用視圖控制器轉場的交互性。
打開PresentTransition.swift
把下面:
class PresentTransition: NSObject, UIViewControllerAnimatedTransitioning 複製代碼
替換爲:
class PresentTransition: UIPercentDrivenInteractiveTransition, UIViewControllerAnimatedTransitioning {
複製代碼
UIPercentDrivenInteractiveTransition
是一個定義基於「百分比」的轉場方法的類,例若有三個方法:
update(_:)
回退轉場。cancel()
取消視圖控制器轉場。finish()
播放轉場直到完成。以前學習的19-交互式導航控制器轉場中也提到相關內容。
UIPercentDrivenInteractiveTransition
的一些屬性:
timingCurve
:若是以交互方式驅動轉場,而且是播放轉場時直到結束,就能夠經過設置此屬性爲動畫提供自定義時序曲線。
wantsInteractiveStart
:默認是true
,是否使用交互式轉場。
pause()
:調用此方法暫停非交互式轉場並切換到交互模式。
向PresentTransition
添加一個新方法:
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
return transitionAnimator(using: transitionContext)
}
複製代碼
這是UIViewControllerAnimatedTransitioning
協議的一個方法。 它容許咱們UIKit提供可中斷的動畫師。
轉場動畫師類如今有兩種不一樣的行爲:
animateTransition(using:)
來設置轉場動畫。interruptibleAnimator(using:)
,獲取動畫師,並使用它來推進這種轉場。切換到LockScreenViewController.swift
, 在UIViewControllerTransitioningDelegate
擴展中添新方法:
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return presentTransition
}
複製代碼
接下來,在LockScreenViewController
中添加兩個新屬性,用來跟蹤用戶的手勢:
var isDragging = false
var isPresentingSettings = false
複製代碼
當用戶向下拉時,將isDragging
標誌設置爲true
,當拉得足夠遠,也將將isPresentingSettings
設置爲true
。
實現UISearchBarDelegate
的一個方法:
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
isDragging = true
}
複製代碼
這可能看起來有點多餘,由於UITableView
已經有一個屬性來跟蹤它當前是否被拖動,但如今要本身作一些自定義跟蹤。
接下來繼續實現UISearchBarDelegate
協議的另外一個方法,用來跟蹤用戶的進度:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard isDragging else { return }
if !isPresentingSettings && scrollView.contentOffset.y < -30 {
isPresentingSettings = true
presentTransition.wantsInteractiveStart = true
presentSettings()
return
}
}
複製代碼
接下來,須要添加代碼以交互方式更新。 將如下內容追加到上面方法的末尾:
if isPresentingSettings {
let progess = max(0.0, min(1.0, ((-scrollView.contentOffset.y) - 30) / 90.0))
presentTransition.update(progess)
}
複製代碼
根據拉出TableView的距離計算0.0到1.0範圍內的進度,並在轉場動畫師上調用update(_:)
以將動畫定位到當前進度。 運行,當向下拖動時,將看到表格視圖逐漸模糊。
還須要注意完成取消轉場,實現UISearchBarDelegate
協議的另外一個方法:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let progress = max(0.0, min(1.0, ((-scrollView.contentOffset.y) - 30) / 90.0))
if progress > 0.5 {
presentTransition.finish()
} else {
presentTransition.cancel()
}
isPresentingSettings = false
isDragging = false
}
複製代碼
這段代碼看起來與19-交互式導航控制器轉場中類似。若是用戶下拉已經超過距離的一半,則認爲轉場成功;若是用戶未下拉超過一半,則取消轉場。
把transitionAnimator(using:)
方法中的addCompletion
代碼塊替換爲:
animator.addCompletion { (position) in
switch position {
case .end:
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
default:
transitionContext.completeTransition(false)
}
}
複製代碼
運行,上下拉動,可能會出現下面這種像素化問題狀況(iOS10可能會出現,iOS11以後應該修復了):
使用以前在PresentTransition
中添加的auxAnimationsCancel
屬性。 在transitionAnimator(using:)
中找到animator.addCompletion
的調用,並在default:
添加:
self.auxAnimationsCancel?()
複製代碼
到LockScreenViewController
的presentSettings(_:)
方法。在設置auxAnimations
屬性後,添加:
presentTransition.auxAnimationsCancel = blurAnimations(false)
複製代碼
運行,像素化問題應該已經消失。
可是還有另外一個問題。點擊Edit按鈕的非交互式轉場沒反應了!😱
只要用戶點擊Edit按鈕,就須要更改代碼以將視圖控制器轉場設置爲非交互式。
到LockScreenViewController
的tableView(_:cellForRowAt:)
,在self.presentSettings()
以前插入:
self.presentTransition.wantsInteractiveStart = false
複製代碼
運行,效果:
接下來,要考慮轉場期間在非交互模式和交互模式之間切換。
在這一部分,將實現點擊Edit按鈕後開始執行顯示設置控制器的動畫,但若是用戶在動畫期間再次點擊屏幕,則暫停轉場。
切換到PresentTranstion.swift
。須要稍微改變更畫師,不只要分別處理交互式和非交互式模式,還要同時處理相同的過渡。 在PresentTranstion
中再添加兩個屬性:
var context: UIViewControllerContextTransitioning?
var animator: UIViewPropertyAnimator?
複製代碼
使用這兩個屬性來跟蹤動畫的上下文以及動畫師。 在transitionAnimator(using:)
方法的return animator
前插入:
self.animator = animator
self.context = transitionContext
複製代碼
每次爲轉場建立新的動畫師時,也會存儲對它的引用。
轉場完成後釋放這些資源也很重要。 繼續添加:
animator.addCompletion { [unowned self] _ in
self.animator = nil
self.context = nil
}
複製代碼
在PresentTranstion
中再添加一個方法:
func interruptTransition() {
guard let context = context else { return }
context.pauseInteractiveTransition()
pause()
}
複製代碼
在transitionAnimator(using:)
方法的return animator
前插入:
animator.isUserInteractionEnabled = true
複製代碼
確保轉場動畫是交互式的,這樣用戶能夠在暫停後繼續與屏幕進行交互。
容許用戶向上或向下滾動以分別完成或取消轉場。 爲此,在LockScreenViewController
中添加一個新屬性:
var touchesStartPointY: CGFloat?
複製代碼
若是用戶在轉場期間觸摸屏幕,能夠將其暫停並存儲第一次觸摸的位置:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard presentTransition.wantsInteractiveStart == false, presentTransition.animator != nil else {
return
}
touchesStartPointY = touches.first!.location(in: view).y
presentTransition.interruptTransition()
}
複製代碼
跟蹤用戶觸摸並查看用戶是向上仍是向下平移,添加:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let startY = touchesStartPointY else { return }
let currentPoint = touches.first!.location(in: view).y
if currentPoint < startY - 40 {
touchesStartPointY = nil
presentTransition.animator?.addCompletion({ (_) in
self.blurView.effect = nil
})
presentTransition.cancel()
} else if currentPoint > startY + 40 {
touchesStartPointY = nil
presentTransition.finish()
}
}
複製代碼
運行,點擊Edit按鈕後,當即點擊屏幕,這個時候轉場會暫停,此時向下滑動會完成轉場,向上滑動會取消轉場,效果以下:
本文在個人我的博客中地址:系統學習iOS動畫之五:使用UIViewPropertyAnimator