本文是我學習《iOS Animations by Tutorials》 筆記中的一篇。
文中詳細程式碼都放在我的Github上 andyRon/LearniOSAnimations。
UIViewPropertyAnimator
是從iOS10開始引入,它能夠建立易於互動,可中斷和/或可逆的檢視動畫。
這個類讓某些型別的檢視動畫更容易建立,值得學習。
UIViewPropertyAnimator
可以在同一個類中方便地將許多API包裝在一起,這樣更容易使用。
此外,這個新類不能完全取代了UIView.animate(withDuration...)
API集。
內容預覽:
22-用UIViewPropertyAnimator進行互動式動畫
23-用UIViewPropertyAnimator自定義檢視控制器轉場
本文的四個章節都是使用同一個專案 LockSearch
20-UIViewPropertyAnimator入門
在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)
}
複製程式碼
對比可以發現有所不同:
21-深入UIViewPropertyAnimator
上一章節學習了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
等
22-用UIViewPropertyAnimator進行互動式動畫
前面兩個章節介紹了許多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互動的互動式動畫:
注意:對於本章專案,需要相容3D touch的iOS裝置(沒記錯的話是6S+)。
聽聞?,3D touch這個技術會被在iPhone上取消,好吧,這邊是學習類似3D touch 的動畫,它的未來如何,就不過問了。
3D touch的動畫,可以這樣描述:當我們手指按壓螢幕上的圖示時,動畫互動式開始,背景越來越模糊,從圖示旁漸漸呈現一個選單,這個過程會隨著手指按壓的力度變化而前後變化。
放慢的效果為:
WidgetView.swift
中,WidgetView
通過擴充套件遵守UIPreviewInteractionDelegate
協議。這個協議中就包括了3D touch過程中一些委託方法。
為了讓您開始開發動畫本身,UIPreviewInteractionDelegate
方法已經連線到LockScreenViewController上呼叫相關方法。
WidgetView中的程式碼如下:
- 3D Touch開始時呼叫
LockScreenViewController.startPreview(for:)
。 - 當使用者按下的過程中,可能更硬(或更柔和)時,反覆呼叫
LockScreenViewController.updatePreview(percent:)
。 - 當peek互動成功完成時,呼叫
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)
})
})
}
複製程式碼
第一個關鍵幀執行您之前的相同動畫。 第二個關鍵幀是簡單旋轉,效果:
23-用UIViewPropertyAnimator自定義檢視控制器轉場
在系統學習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提供可中斷的動畫師。
轉場動畫師類現在有兩種不同的行為:
- 如果以非互動方式使用它(當使用者按下編輯按鈕時),UIKit將呼叫
animateTransition(using:)
來設定轉場動畫。 - 如果以互動方式使用它,UIKit將呼叫
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