系統學習iOS動畫之五:使用UIViewPropertyAnimator

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

UIViewPropertyAnimator是從iOS10開始引入,它可以建立易於交互,可中斷和/或可逆的視圖動畫。ios

這個類讓某些類型的視圖動畫更容易建立,值得學習。git

UIViewPropertyAnimator能夠在同一個類中方便地將許多API包裝在一塊兒,這樣更容易使用。github

此外,這個新類不能徹底取代了UIView.animate(withDuration...)API集。spring

內容預覽:swift

20-UIViewPropertyAnimator入門設計模式

21-深刻UIViewPropertyAnimator閉包

22-用UIViewPropertyAnimator進行交互式動畫app

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,這是一個枚舉類型,有四個類型:easeInOuteaseIneaseOutlinear。這與UIView.animate(withDuration:...)中的option是相似的。

添加動畫

viewDidAppear(_:)中添加:

scale.addAnimations {
    self.tableView.alpha = 1.0
}
複製代碼

使用addAnimations添加動畫代碼塊,就像UIView.animate(withDuration...)的閉包參數animations。 使用動畫師的不一樣之處在於能夠添加多個動畫塊。

除了可以有條件地構建複雜的動畫外,還能夠添加具備不一樣延遲的動畫。 另外一個版本的addAnimations,有兩個參數: animation 動畫代碼 delayFactor 動畫開始前的延遲

delayFactorUIView.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
複製代碼

爲何第二個參數不是一個簡單的秒數值? 想象動畫師已經在運行了,你決定在中途添加一些新的動畫。 在這種狀況下,剩餘持續時間不會等於總持續時間,由於自啓動動畫以來已通過了一段時間。

image-20181204120317868

在這種狀況下,delayFactor將容許開發者根據剩餘可用時間設置延遲動畫。 此外,這樣設計也確保了不能將延遲設置爲長於剩餘運行時間。

image-20181204120335113

添加完成閉包

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善意的提醒😊。

image-20181204124154609

這個問題很容易解決,只須要在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.swiftcollectionView(_: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的知識。

本章的開始項目 使用上一章節完成的項目。

自定義動畫計時

前文已經屢次提到:easeInOuteaseIneaseOutlinear(能夠理解爲物體運動軌跡的曲線類型)。能夠參考視圖動畫中的動畫緩動 或者圖層動畫中的動畫緩動,這邊就再也不介紹了。

內置時間曲線

目前,當您激活搜索欄時,您會在窗口小部件頂部的模糊視圖中淡入淡出。 在此示例中,您將刪除該淡入淡出動畫併爲模糊效果自己設置動畫。

以前,激活搜索欄時,就會有一個模糊視圖中淡入淡出效果。這個部分刪除這個效果,修改爲對模糊效果自己設置動畫。什麼意思呢? 看完下面的操做,應該能明白。

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)

image-20181204154619268

如今讓咱們來看看曲線。曲線比線條更有趣,由於它們能夠在屏幕上繪製任何東西。例如:

image-20181204154744302

在上面看到的是四條曲線放在一塊兒;它們的兩端在小白方塊的地方相遇。圖中有趣的是小綠圈,它們定義了每條曲線。

因此曲線不是隨機的。它們也有一些細節,就像線條同樣,能夠幫助咱們經過座標定義它們。

您能夠經過向線條添加控制點來定義曲線。 讓咱們在以前的行中添加一個控制點:

image-20181204154909038

能夠想象由鏈接到線的鉛筆繪製的曲線,其起點沿着線AC移動,其終點沿着線CB移動:

image-20181204154949279

網上找了一個動圖:

具備一個控制點的Bézier曲線稱爲 二次曲線。有兩個控制點的Bézier曲線叫作 三次曲線(立方貝塞爾曲線)。 咱們使用的內置曲線就是三次曲線。

核心動畫使用始終以座標(0,0)開始的三次曲線,它表示動畫持續時間的開始。 固然,這些時間曲線的終點始終是(1,1),表示 動畫的持續時間和進度的結束。

讓咱們來看看 ease-in 曲線:

image-20181204155458179

隨着時間的推移(在座標空間中從左向右水平移動),曲線在垂直軸上的進展很是小,而後大約在動畫持續時間的一半時間後,曲線在垂直軸上的進展很是大,最終在(1, 1)處結束。

ease-outease-in-out曲線分別是:

image-20181204155513973

如今已瞭解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()
}
複製代碼

這邊的controlPoint1controlPoint2兩個點,就是咱們自定義三次曲線的控制點。

能夠經過 cubic-bezier.com 網站來選着控制點。

彈簧動畫

另外一個便利構造器UIViewPropertyAnimator(duration:dampingRatio:animations:),用於定義彈簧動畫。

這與UIView.animate(withDuration: delay: usingSpringWithDamping: initialSpringVelocity: options: animations: completion:)相似,只不過初始速度爲0。

自定義時間曲線

UIViewPropertyAnimator類還有一個構造器UIViewPropertyAnimator(duration:timingParameters:)

參數timingParameters必須遵照UITimingCurveProvider協議,有兩個類可供咱們使用:UICubicTimingParametersUISpringTimingParameters

下面看看這個構造器的使用方式。

阻尼和速度

添加阻尼和速度的方式以下:

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

LockScreenViewControllerviewWillAppear裏添加:

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有三個描述當前狀態的屬性:

image-20181204183027143

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只能按特定順序在狀態之間切換。 它不能直接從inactivestopped,也不能從stopped直接轉爲active

若是設置了pausesOnCompletion,一旦動畫師完成了動畫的運行而不是自動中止,而是暫停。 這將使咱們有機會在暫停狀態下繼續使用它。

狀態流程圖:

image-20181204183357332

可能有點繞,以後的使用中,若是有疑問,能夠再回到這個部分查看。

交互式3D touch動畫

從這個部分開始,將學習建立相似於3D touch交互的交互式動畫:

image-20190101223452030

注意:對於本章項目,須要兼容3D touch的iOS設備(沒記錯的話是6S+)。

聽聞👂,3D touch這個技術會被在iPhone上取消,好吧,這邊是學習相似3D touch 的動畫,它的將來如何,就不過問了。

3D touch的動畫,能夠這樣描述:當咱們手指按壓屏幕上的圖標時,動畫交互式開始,背景愈來愈模糊,從圖標旁漸漸呈現一個菜單,這個過程會隨着手指按壓的力度變化而先後變化。

放慢的效果爲:

ScreenRecording_01-04-2019 11-13-10.2019-01-04 11_24_37

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的子類,它包含單個標籤的簡單模糊視圖,使用它來模擬從按下的圖標彈出的菜單:

image-20181219112216359

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.swiftstartPreview()中完成調用:

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範圍內,由於我不但願在動畫才這段結束,我另外指定的方法完成或取消動畫。

運行,效果(放慢):

ScreenRecording_01-04-2019 18-29-21.2019-01-04 18_40_04

你會(驚喜!)須要更多的動畫師。 打開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.swiftcancelPreview()中繼續添加:

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,若是圖標位於屏幕左側,則顯示右側菜單,若是圖標位於左側,則顯示左側菜單在屏幕的右側。請參閱如下差別:

image-20190102115412511

動畫師準備好了,因此打開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()
複製代碼

運行,按壓圖標:

image-20190102115643202

關閉模糊視圖

目前,菜單彈出,模糊視圖顯示後,尚未回到原來視圖的操做,下面添加這個操做。

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製做關鍵幀動畫,如今再給關鍵幀動畫添加交互式操做。

爲了嘗試一下,你將爲成長動畫添加一個額外的元素 - 在用戶按下圖標時以交互方式擦洗的元素。

刪除AnimatorFactorygrow()方法中的代碼:

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

    })
}
複製代碼

第一個關鍵幀運行您以前的相同動畫。 第二個關鍵幀是簡單旋轉,效果:

ScreenRecording_01-04-2019 23-29-27.2019-01-04 23_32_31

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)
複製代碼

再試一次過渡,看看這一行如何改變它:

image-20190111123452428

模糊動畫重複使用了。

另外,當用戶解除控制器時,還須要隱藏模糊視圖。

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提供可中斷的動畫師。

轉場動畫師類如今有兩種不一樣的行爲:

  1. 若是以非交互方式使用它(當用戶按下編輯按鈕時),UIKit將調用animateTransition(using:)來設置轉場動畫。
  2. 若是以交互方式使用它,UIKit將調用interruptibleAnimator(using:),獲取動畫師,並使用它來推進這種轉場。

image-20190111124837379

切換到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(_:)以將動畫定位到當前進度。 運行,當向下拖動時,將看到表格視圖逐漸模糊。

2019-01-11 13-16-13.2019-01-11 13_22_39

還須要注意完成取消轉場,實現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以後應該修復了):

image-20190111133132818

使用以前在PresentTransition中添加的auxAnimationsCancel屬性。 在transitionAnimator(using:)中找到animator.addCompletion的調用,並在default:添加:

self.auxAnimationsCancel?()
複製代碼

LockScreenViewControllerpresentSettings(_:)方法。在設置auxAnimations屬性後,添加:

presentTransition.auxAnimationsCancel = blurAnimations(false)
複製代碼

運行,像素化問題應該已經消失。

可是還有另外一個問題。點擊Edit按鈕的非交互式轉場沒反應了!😱

只要用戶點擊Edit按鈕,就須要更改代碼以將視圖控制器轉場設置爲非交互式。

LockScreenViewControllertableView(_:cellForRowAt:),在self.presentSettings()以前插入:

self.presentTransition.wantsInteractiveStart = false
複製代碼

運行,效果:

2019-01-11 13-40-54.2019-01-11 13_41_51

可中斷的轉場動畫

接下來,要考慮轉場期間在非交互模式和交互模式之間切換。

在這一部分,將實現點擊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

相關文章
相關標籤/搜索