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

Andy_Ron發表於2019-01-13

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

UIViewPropertyAnimator是從iOS10開始引入,它能夠建立易於互動,可中斷和/或可逆的檢視動畫。

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

UIViewPropertyAnimator可以在同一個類中方便地將許多API包裝在一起,這樣更容易使用。

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

內容預覽:

20-UIViewPropertyAnimator入門

21-深入UIViewPropertyAnimator

22-用UIViewPropertyAnimator進行互動式動畫

23-用UIViewPropertyAnimator自定義檢視控制器轉場

本文的四個章節都是使用同一個專案 LockSearch

20-UIViewPropertyAnimator入門

在iOS10之前,建立基於檢視的動畫的唯一選擇是UIView.animate(withDuration: ...)I,但這組API沒有為開發人員提供暫停或停止已經執行的動畫的處理方式。此外,對於反轉,加速或減慢動畫,開發人員只能使用基於圖層的CAAnimation(核心動畫)。

UIViewPropertyAnimator就是為了解決上述問題而出現的,它是一個允許保持執行動畫的類,允許開發者調整當前執行的動畫,並提供有關動畫當前狀態的詳細資訊。

當然,簡單單一的檢視動畫直接使用UIView.animate(withDuration: ...)就可以了。

基礎動畫

本章的開始專案 LockSearch 。 類似於iOS鎖屏時的螢幕。 初始檢視控制器有搜尋欄,單個視窗小部件和編輯按鈕等:

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

開始專案 已經實現了一些與動畫無關的功能。 例如,如果點選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()
    }
  }
複製程式碼

這將允許使用者通過點選右側按鈕解除搜尋。

執行,效果:

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

點按搜尋欄文字欄位,小部件在模糊效果檢視下消失;點選搜尋欄右側的按鈕時,模糊檢視會淡出。

基礎關鍵幀動畫

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()
}
複製程式碼

效果:

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

提取模糊動畫

把前面的模糊動畫也提取到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)
}
複製程式碼

防止動畫重疊

如何檢查動畫師當前是否正在執行其動畫?

如果在同一個圖示上快速連續點選,會發現抖動動畫沒有結束就重新開始了。

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

解決這個問題,就需要檢測檢視是否有動畫正在執行。

IconCell新增一個屬性,並修改iconJiggle()

  var animator: UIViewPropertyAnimator?

  func iconJiggle() {
    if let animator = animator, animator.isRunning {
      return
    }

    animator = AnimatorFactory.jiggle(view: icon)
  }
複製程式碼

對比可以發現有所不同:

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

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()
}
複製程式碼

執行,效果:

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

請注意模糊不僅僅是淡入或淡出,實際上它會在效果檢視中插入模糊量。

貝塞爾曲線

有時想要對動畫的時間非常具體時,使用這些曲線簡單地“開始減速”或“慢慢結束”是不夠的。

10-動畫組和時間控制 中學習了使用CAMediaTimingFunction控制圖層動畫的時間。

之前沒有了解背後的原理貝塞爾曲線,這邊介紹一下它。這邊的內容也可應用到圖層動畫中。

貝塞爾曲線是什麼?

讓我們從簡單的事情開始 —— 一條線。它非常簡潔,需要在螢幕上畫一條線,只需要定義它的兩個點的座標,開始 (A) 和結束 (B)

image-20181204154619268

現在讓我們來看看曲線。曲線比線條更有趣,因為它們可以在螢幕上繪製任何東西。例如:

image-20181204154744302

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

所以曲線不是隨機的。它們也有一些細節,就像線條一樣,可以幫助我們通過座標定義它們。

您可以通過向線條新增控制點來定義曲線。 讓我們在之前的行中新增一個控制點:

image-20181204154909038

可以想象由連線到線的鉛筆繪製的曲線,其起點沿著線AC移動,其終點沿著線CB移動:

image-20181204154949279

網上找了一個動圖:

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

具有一個控制點的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??。

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

上面貝塞爾的原理說的不夠深刻?‍♀️,現在只需瞭解曲線,通過兩個控制點可以畫曲線。

接下來,向專案中新增自定義計時動畫。

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()
複製程式碼

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

檢視過渡

檢視動畫的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)
複製程式碼

這將改變按鈕標題,具有很好的交叉淡入淡出效果:

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

效果也可以嘗試.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檢視,以防萬一在螢幕上留下之前的檢視。 然後,您可以建立集合檢視圖示的快照,最後將其新增到模糊效果檢視上方的螢幕上。

執行,按壓圖示。發現圖示出現在左上角!?

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

因為尚未設定其位置。 繼續新增:

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有點問題:

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

Main.storyboard中將檢視的背景更改為Clear Color

執行,變成:

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

下面向動畫師新增新屬性,為了可以將任何自定義動畫注入轉場動畫, 使用相同的轉場類來生成略有不同的動畫。

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或其他選項,先有的模糊檢視,然後恢復到第一個檢視控制器:

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

互動檢視控制器轉場

這個部分通過下拉的手勢來時學習實現互動檢視控制器轉場。

首先,讓我們使用強大的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

本文在我的個人部落格中地址:系統學習iOS動畫之五:使用UIViewPropertyAnimator

相關文章