在 iOS 10 中使用 Swift 3 和 UIViewPropertyAnimator 編寫動畫
作者:Jason Newell,原文連結,原文日期:2016-06-28
譯者:冬瓜;校對:numbbbbb;定稿:numbbbbb
這是一個 iOS 10 系列教程,會介紹 iOS 10、Swift 和 XCode 8 的新特性。
UIKit in iOS 10 now has “new object-based, fully interactive and interruptible animation support that lets you retain control of your animations and link them with gesture-based interactions” through a family of new objects and protocols.
iOS 10 的 UIKit 使用一系列新物件和
protocol
來控制使用者互動和中斷動畫,支援用手勢操作。
簡言之,iOS 10 可以讓開發者更加自由寬鬆地控制動畫計時。你可以細粒度控制自己製作的動畫,易於抹除、逆向、暫停和重啟動畫,並重構動畫幀使之平滑流暢。這些功能也可以用於控制器的轉場動畫。
我希望通過此文介紹一些關於新特性的基本用法,並記錄一些在文件中的關鍵點。
構建基礎應用
我們會使用一些 UIViewPropertyAnimator
的新特性。首先需要一些素材。
建立一個 single-view application,然後在 ViewController.swift 中新增如下程式碼:
import UIKit |
這並不複雜。在 viewDidLoad
方法中,建立一個綠色背景的圓形檢視並放置在當前檢視中央。然後新增 UIPanGestureRecongnizer
例項來感知拖動圓形檢視的手勢並呼叫 dragCircle
方法。你應該已經猜到執行效果了:
UIViewPropertyAnimator 介紹
UIViewPropertyAnimator
是修改動畫屬性的核心類,它提供了中斷動畫、修改動畫中間過程的功能。UIViewpropertyAnimator
維護了一個動畫集合,可以無縫連線新動畫和原有動畫。
注意:
UIViewPropertyAnimator
有些拗口,我在下文將使用animator
來代替。
如果兩個或多個動畫需要同時改變相同的屬性,則會遵循“後者優先”原則。有趣的是,這將導致卡頓,因為需要組合新舊動畫。在舊動畫淡出的同時會隱約看見新動畫。
後者優先:
UIViewPropertyAnimator
例項中靠後新增的動畫或者執行時間更晚的動畫會覆蓋之前的效果。
暫停和恢復動畫
我們繼續擴充套件上面的動畫,增加一個動畫效果:原型檢視被拖動時會擴大到原尺寸的兩倍,停止拖動時該檢視會縮小到原尺寸。
首先給 animator 新增一個屬性和一個持續時間。
// ... |
在 viewDidLoad:
中對 animator
初始化:
// ... |
初始化 circleAnimator
時, 我們需要傳入持續時間和動畫曲線。 curve
引數可設定為四種簡單的曲線之一。在本例中使用的是easeInOut
。其他三種是 easeIn
、easeOut
和 linear
。我們使用一個閉包來實現圓形檢視變大動畫。
現在需要一個方法來觸發動畫。修改一下 dragCircle:
。在這段程式碼中,通過拖動檢視來觸發動畫,通過circleAnimator.isReversed
來判斷動畫縮放狀態。
func dragCircle(gesture: UIPanGestureRecognizer) { |
執行程式碼,長按,感受一下新動畫。
注意
動畫結束時,如下圖所示:
圓形檢視不再縮放,停止在放大後的尺寸。
到底發生了什麼?簡單來說,動畫結束後,其引用自動釋放了。
animator 有三種狀態:
- inactive(休眠):初始狀態, 以及動畫完成後的狀態(可以過渡到 active)
- active(啟用):動畫正在執行(可以過渡到
stopped
或者inactive
) - stopped(停止):呼叫
animator
的stopAnimation:
方法(過渡到 inactive)
來看下圖示:
只要過渡到 inactive 狀態,就會導致 animator
清除所有動畫(並執行 animator
的完成回撥函式)
我們已經介紹了 startAnimation
方法,下面介紹另外兩個狀態。
要修復本節的問題,需要修改 circleAnimator
的初始化方法:
expansionAnimator = UIViewPropertyAnimator(duration: expansionDuration, curve: .easeInOut) |
譯者注:這裡原作者寫錯了 duration 引數名稱,expansionDuration 應該改成 animationDuration。
修改 dragCircle
方法:
// ... |
無論使用者開始還是停止拖拽,我們都讓 animator
停止並完成(只要它處於active狀態)。animator
會清除關聯動畫並返回到 inactive狀態。然後,我們新增一個新動畫,讓圓形檢視回到正確狀態。
使用 transform
的好處是,你可以直接把 transform
屬性設定為 CGAffineTransform.identity
來還原檢視,無需記錄初值。
circleAnimator.stopAnimation(true)
相當於這兩行程式碼:
circleAnimator.stopAnimation(false) // 不要結束(保持在 stop 狀態) |
finishAnimationAt:
方法接受一個 UIViewAnimatingPosition
引數。如果我們已經到達 start 和 end,原型檢視的動畫變形狀態將會改變。
動畫時間
我們的程式碼還有一個小問題。每次終止並開始一個新動畫時,新動畫都會持續4.0秒,哪怕當前狀態已經很接近終止狀態。
修改一下程式碼:
// dragCircle: |
我們主動停止動畫,在原動畫末尾加入新動畫並啟動,通過 continueAnimationWithTimingParameters: durationFactor:
來確定第一個動畫的剩餘時間。這樣就解決了剛才的固定時間問題。 continueAnimationWithTimingParameters: durationFactor:
這個方法也能動態修改動畫的持續時間。
譯者注:fractionComplete: 這個屬性在
NSProgress
中也有涉及,不過NSProgress
的屬性為fractionCompleted
。其含義與此處類似,都是使用 0.0-1.0 之間的浮點數來表示一段連續動作的完成比例。
* 當你變化後的動畫(相比於之前的動畫)擁有不同的時間曲線函式,動畫在過渡時會插入到舊時間繼續執行。例如,從一個彈性時間曲線過渡到線性時間曲線,動畫線上性變化之前會有一段彈性時間部分。
時間曲線函式
新的時間曲線函式時間曲線函式要比原來的更加合理。
Swift 3 相容了舊的 UIViewAnimationCurve
(例如在本文示例中使用的 easeInOut
這類靜態曲線函式),還新增了兩個新的時間曲線函式物件:UISpringTimingParameters
和UICubicTimingParameters
。
UISpringTimingParameters
UISpringTimingParameters
的例項需要設定阻尼係數(damping)、質量引數(mass)、剛性係數(stiffness)和初始速度(initial velocity)。這些引數會帶入給定公式,讓動畫更加真實。雖然不需要使用,但是初始化動畫時必須傳入持續時間引數,UISpringTimingParameters
會忽略它。這個引數可以相容舊的時間曲線函式。
下面來看一個例項,使用彈簧動畫把圓形檢視約束在螢幕中心:
ViewController.swift
import UIKit |
拖動圓形檢視,讓動畫執行起來。這裡我們把向量速度(velocity)傳入 initialVelocity:
並用彈性時間曲線函式(Sprint Timing)作為 parameters:
引數,這樣圓形檢視不僅會返回起始點,釋放時還會保持原有動量。
// dragCircle: .ended: |
為了跟蹤動畫路徑,我繪製了一些圓點。原本筆直的路徑稍顯彎曲,因為釋放圓形檢視,中心點會以彈簧效果的形式向中心“拉拽”,到達終點後動量仍舊保留。
UICubicTimingParameters
UICubicTimingParameters
允許通過多個控制點(control point)來定義三階貝塞爾曲線。需要注意的是,在 0.0~1.0 範圍外的點會修正到範圍內。
// 為 y 設定對應的值 |
如果你仍不滿足這些時間曲線函式,可以通過 UITimingCurveProvider
協議來實現更加合適的曲線函式。
動畫抹除
你可以給 animator
的 fractionComplete
傳入一個 0.0-1.0 的浮點數,使其在對應位置暫停。如果傳入 0.5,無論時間曲線函式是什麼,動畫都會停止在一半狀態的位置。需要注意的是,當動畫重新開始時,其銜接位置將會對映到給定的時間曲線函式曲線上,所以fractionComplete = 0.5
並不代表已經運行了一半時間。
我們來做個實驗。首先在 viewDidLoad
尾部初始化 animator
並傳入兩個動畫:
// viewDidLoad: |
這次不呼叫 startAnimation
方法。圓圈會隨著動畫而逐漸變大,檢視背景在 75% 處開始變成藍色。
重寫一下 dragCircle:
方法:
func dragCircle(gesture: UIPanGestureRecognizer) { |
現在拖拽圓形檢視時會更新 animator
的 fractionComplete
屬性,從而達到不同的效果:
這裡我使用的是線性曲線函式,你也可以基於這個例子實現其他函式。這個改變顏色的動畫遵循一個壓縮過的時間曲線。
自定義
animator
的動畫程序需要使用 0.0-1.0 範圍內的浮點數表示,如果超出範圍,則會取該資料臨近的端界值(即 0 或 1)。
擴充套件性
最後我想強調一點:Don’t like something? Change it! (譯者注:“看著不爽?自己動手!”)你可以根據動畫需要來實現各種時間曲線函式。
此外,這會進一步解耦協議和類,很多原始碼中的協議都能做到這一點。這會使開發更加便捷,我也希望能有更多開發者去深入探索。
本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 http://swift.gg。