“動畫”一詞源自拉丁語,意思是“生命的行為”。在您的應用程序中,動畫可以將界面元素平滑地帶入屏幕或焦點,可以吸引用戶的注意力,而它們可以清楚地表明您的應用程序是如何響應用戶操作的。 在本章中,您將返回到您的 Quiz
應用程序,并使用各種動畫技術來實現。
在更新 Quiz 之前,我們可以通過文檔來查看什么能夠動畫化。 要打開文檔,請打開 Xcode
的 Help
菜單,然后選擇 Documentation and API Reference
。 這將在新窗口中打開文檔。
打開文檔后,使用頂部的搜索欄搜索 “UIView”。在搜索結果的 API Reference
下,單擊 UIView
打開類引用,然后向下滾動到標題為 Animations
的部分。 該文檔提供了一些動畫建議(我們將在本書中介紹),并列出了可以動畫化的 UIView 上的屬性(圖8.1)。
圖8.1 UIView Animations 文檔
基本動畫
該文檔始終是了解任何 iOS 技術的良好起點。 有了這個小小的信息,讓我們繼續添加一些動畫到 ‘Quiz’。 您將要使用的第一種類型的動畫是 基本動畫(basic animation)
。 基本動畫在起始值和結束值之間產生動畫(圖8.2)。
圖8.2 基本動畫
您要添加的第一個動畫將動畫化與 ViewController 相關聯的問題標簽的 alpha
值(透明度)。 當用戶進入下一個問題時,您將使用動畫來淡出標簽。 UIView 上有類方法可以讓你完成這個。 最簡單的 UIView 動畫方法是:
class func animate(withDuration duration: TimeInterval, animations: () -> Void)
此類方法有兩個參數:類型為 TimeInterval(它是 Double 的別名)的 duration
和作為閉包的 animations
變量。
閉包
閉包(closure)
是一組離散的功能,可以圍繞您的代碼傳遞。 閉包很像函數和方法。 事實上,函數和方法是閉包的特殊情況。
閉包具有輕量級的語法,允許它們作為參數容易地傳遞給函數和方法。 閉包甚至可以是函數或方法的返回類型。 在本節中,您將使用閉包來指定要發生的動畫。
一個閉包的語法是逗號分隔的括號中的參數列表,后跟一個返回箭頭和返回類型:
(arguments) -> return type
請注意,此語法與函數的語法類似:
func functionName(arguments) -> return type
現在再看一下 animations
參數期望的閉包用法:
class func animate(withDuration duration: TimeInterval, animations: () -> Void)
這個閉包不會引用任何參數,也不會返回任何內容。 (你有時會看到這個返回類型表示為(),這與 Void 相同)
閉包用法非常簡單和熟悉,但是如何在代碼中聲明關閉? 閉包語法采用以下形式:
{ (arguments) -> return type in
??// code
}
閉包表達式寫在 大括號({}) 中。 閉包的參數在開頭大括號之后的括號內列出。 閉包的返回類型來自參數,并使用常規語法。 關鍵字 in
用于將閉包的參數和返回類型與其正文中的語句分開。
打開 Quiz.xcodeproj
。 在 ViewController.swift
中,添加一個新方法來處理動畫,并聲明一個不帶參數的閉包常量,不返回任何東西。
func animateLabelTransitions() {
??let animationClosure = { () -> Void in
??}
}
你使用一個常數,引用了一大堆功能。 然而,目前,這個閉包實際上并沒有做任何事情。 向閉包添加功能,將 questionLabel
的 alpha 設置為 1
.然后,將此閉包作為參數傳遞給 animate(withDuration:animations :)。
func animateLabelTransitions() {
??let animationClosure = { () -> Void in
????self.questionLabel.alpha = 1
??}
??// Animate the alpha
??UIView.animate(withDuration: 0.5, animations: animationClosure)
}
在屏幕上,questionLabel
已經具有1
的 alpha
值,所以如果您構建并運行,您將看不到任何動畫。 為了解決這個問題,重寫 viewWillAppear(_ :),以便每次 ViewController 的視圖進入屏幕時,將 questionLabel
的 alpha
重置為 0
。
override func viewWillAppear(_ animated: Bool) {
??super.viewWillAppear(animated)
??// Set the label's initial alpha
??questionLabel.alpha = 0
}
以上代碼非常好,但您可以使其更簡潔。 更新代碼。
func animateLabelTransitions() {
??let animationClosure = { () -> Void in
????self.questionLabel.alpha = 1
??}
??// Animate the alpha
??UIView.animate(withDuration: 0.5, animations: animationClosure)
??UIView.animate(withDuration: 0.5, animations: {
????self.questionLabel.alpha = 1
??})
}
你有兩個改變:首先,你匿名傳遞閉包(即將它直接傳遞給方法,而不是將其分配給變量或常量)。 其次,您已經刪除了類型信息,因為閉包可以從上下文推斷出這一點。
當用戶點擊 Next Question
按鈕時,調用 animateLabelTransitions() 方法。
@IBAction func showNextQuestion(_ sender: UIButton) {
??currentQuestionIndex += 1
??if currentQuestionIndex == questions.count {
????currentQuestionIndex = 0
??}
??let question: String = questions[currentQuestionIndex]
??questionLabel.text = question
??answerLabel.text = "???"
??animateLabelTransitions()
}
構建并運行應用程序。 當您點擊 Next Question
按鈕時,標簽將淡入視圖。 動畫提供了令人震驚的用戶體驗,而不只是簡單地讓視圖直接出現。
下一個標簽
第一次按下 Next Question
按鈕時,動畫效果很好,但是隨后的按鈕按下時,沒有可見的動畫,因為標簽的 alpha 值為 1.在本節中,您將要添加另一個標簽。 當按下 “下一個問題” 按鈕時,現有標簽將淡出,而新標簽(下一個問題的文本)將淡入。
在 ViewController.swift
的頂部,用兩個標簽替換單個標簽的聲明。
@IBOutlet var questionLabel: UILabel!
@IBOutlet var currentQuestionLabel: UILabel!
@IBOutlet var nextQuestionLabel: UILabel!
@IBOutlet var answerLabel: UILabel!
Xcode
標記四個地方,您需要用其中一個新標簽替換 questionLabel
。 更新 viewDidLoad() 以使用 currentQuestionLabel
。 更新 viewWillAppear(_ :) 和 showNextQuestion(_ :) 以使用 nextQuestionLabel
。
func viewDidLoad() {
??super.viewDidLoad()
??questionLabel.text = questions[currentQuestionIndex]
??currentQuestionLabel.text = questions[currentQuestionIndex]
}
override func viewWillAppear(_ animated: Bool) {
??super.viewWillAppear(animated)
??// Set the label's initial alpha
??questionLabel.alpha = 0
nextQuestionLabel.alpha = 0
}
@IBAction func showNextQuestion(_ sender: UIButton) {
currentQuestionIndex += 1
??if currentQuestionIndex == questions.count {
????currentQuestionIndex = 0
??}
??let question: String = questions[currentQuestionIndex]
??questionLabel.text = question
??nextQuestionLabel.text = question
??answerLabel.text = "???"
??animateLabelTransitions()
}
現在更新 animateLabelTransitions() 以動畫化兩個標簽的透明度。 您將淡出 currentQuestionLabel
并同時淡入 nextQuestionLabel
。
func animateLabelTransitions() {
??// Animate the alpha
??UIView.animate(withDuration: 0.5, animations: {
????self.questionLabel.alpha = 1
????self.currentQuestionLabel.alpha = 0
????self.nextQuestionLabel.alpha = 1
??})
}
打開 Main.storyboard
。 現在,這兩個標簽的代碼已被更新,您需要進行連接。 右鍵單擊 View Controller
以查看連接列表。 請注意,現有的 questionLabel
在其旁邊仍然存在黃色警告標志(圖8.3)。 單擊 x
刪除此連接。
圖8.3 缺少連接
通過從currentQuestionLabel
標簽旁邊的圓圈拖到畫布上的標簽,將 currentQuestionLabel outlet 連接到問題標簽。
現在將一個新的 Label
拖到界面上,并將其放在 currentQuestionLabel
標簽旁。 將 nextQuestionLabel
連接到此新標簽。
您希望此標簽與現有問題標簽處于相同的位置。 正如你可能猜到的那樣,實現這一目標的最好方法是通過約束。 右擊從 nextQuestionLabel
拖動到 currentQuestionLabel
并選擇 Top
。 然后右擊從
nextQuestionLabel
向上拖動到其父視圖,并選擇 Center Horizontally in Container
。
現在,nextQuestionLabel
放錯了。 選擇它,打開 Resolve Auto Layout Issues
菜單,然后選擇 Update Frames
。 標簽現在將出現在另一個的上面。
構建并運行應用程序。 點擊 Next Question
按鈕,您將看到兩個標簽優雅地淡入淡出。
如果再次點擊它,則不會再出現淡入淡出,因為 nextQuestionLabel 已經具有 1
的透明度。要解決此問題,您將交換對兩個標簽的引用。 當動畫完成時,需要將 currentQuestionLabel
設置為屏幕標簽,并將 nextQuestionLabel
設置為屏幕外標簽。 您將使用 動畫的完成者(completion handler)
來完成此操作。
動畫完成
方法 animate(withDuration:animations :) 很快就會結束并返回。 也就是說,它開始動畫,但是并不等待動畫完成。 如果你想知道什么時候動畫完成怎么辦? 例如,您可能希望將動畫鏈接在一起,或者在動畫完成時更新另一個對象。 要知道動畫的完成時間,可以使用閉包中的 completion
參數。 您將利用此機會交換兩個標簽引用。
在 ViewController.swift
中,更新 animateLabelTransitions() 以使用具有最多參數的 UIView 動畫方法,其中包括一個 completion 閉包的動畫方法。
func animateLabelTransitions() {
??// Animate the alpha
??UIView.animate(withDuration: 0.5, animations: {
??self.currentQuestionLabel.alpha = 0
??self.nextQuestionLabel.alpha = 1
??})
??UIView.animate(withDuration: 0.5,
????delay: 0,
????options: [],
????animations: {
????self.currentQuestionLabel.alpha = 0
??????self.nextQuestionLabel.alpha = 1
????},
????completion: { _ in
????swap(&self.currentQuestionLabel,
????&self.nextQuestionLabel)
??})
}
delay 指示系統在觸發動畫之前等待多長時間。 我們將在本章后面討論這些選項。 現在,你將傳遞一個空的數組。
在 completion 閉包中,您需要告訴系統,以前是 currentQuestionLabel
的現在是 nextQuestionLabel
,而以前的 nextQuestionLabel
現在是 currentQuestionLabel
。 要實現這一點,您可以使用 swap(: :) 函數,它接受兩個引用并交換它們。
構建并運行應用程序。 現在你可以在所有的問題之間轉換。
動畫化約束
在本節中,您將擴展您的動畫,使 nextQuestionLabel
從屏幕左側飛入,當用戶按下 Next Question 按鈕時, currentQuestionLabel 會從右側飛出。 在這樣做的時候,你需要學習如何動畫化約束。
首先,您需要參考需要修改的約束。 到目前為止,所有 @IBOutlet 都是視圖對象。 但是,outlet 不僅限于視圖——實際上,您的界面文件中的任何對象都可以有 outlet,包括約束。
在 ViewController.swift
的頂部,為兩個標簽的中心約束聲明兩個 outlet。
@IBOutlet var currentQuestionLabel: UILabel!
@IBOutlet var currentQuestionLabelCenterXConstraint: NSLayoutConstraint!
@IBOutlet var nextQuestionLabel: UILabel!
@IBOutlet var nextQuestionLabelCenterXConstraint: NSLayoutConstraint!
@IBOutlet var answerLabel: UILabel!
現在打開 Main.storyboard
。 您想將這兩個 outlet 連接到各自的約束。 最簡單的方法是使用文檔大綱。 單擊文檔大綱 Constraints
旁邊的三角形,并找到 Current Question Label CenterX Constraint
。 右擊 View Controller
拖動到該約束(圖8.4)并選擇正確的 outlet。 對 Next Question Label CenterX Constraint
進行同樣的操作。
圖8.4連接約束outlet
目前,Next Question
按鈕和答案子視圖的中心 X 約束在 currentQuestionLabel
的中心 X。 當您實現此標簽的動畫以在屏幕外滑動時,其他子視圖將隨之而來。 這不是你想要的。
選中 Next Question
按鈕的 X 值居中到 currentQuestionLabel
的約束并將其刪除它。 然后從右擊 Next Question
按鈕向上拖動到其父視圖,然后選擇 Center Horizontally in Container
。
接下來,您希望兩個問題標簽在一個屏幕寬度中分開。 nextQuestionLabel
的中心將是距視圖左側的屏幕寬度的一半。 currentQuestionLabel
的中心將處于當前位置,居中在當前屏幕。
當動畫被觸發時,兩個標簽將向右移動全屏寬度,將 nextQuestionLabel
放置在屏幕的中心,將 currentQuestionLabel
放在屏幕右側的屏幕寬度的一半(圖8.5)。
圖8.5滑動標簽
為了實現這一點,當加載 ViewController 的視圖時,您需要將 nextQuestionLabel
移動到其屏幕外的位置。
在 ViewController.swift
中,添加一個新方法,并從 viewDidLoad() 中調用它。
func viewDidLoad() {
??super.viewDidLoad()
??currentQuestionLabel.text = questions[currentQuestionIndex]
??updateOffScreenLabel()
}
func updateOffScreenLabel() {
??let screenWidth = view.frame.width
??nextQuestionLabelCenterXConstraint.constant = -screenWidth
}
你想要動畫化標簽從左到右地顯示。 動畫化約束與動畫化其他屬性有所不同。 如果修改動畫塊中的約束的常量,則不會發生動畫。 為什么? 修改約束后,系統需要重新計算層中所有相關視圖的邊框以適應此更改。 任何約束更改都會自動觸發,這將是非常浪費的。 (想象一下,如果您更新了相當多的約束條件,你不會想讓它在每次更改后都重新計算一次這些邊框)。所以您必須要求系統在修改完成后重新計算邊框。 為此,您可以在視圖中調用 layoutIfNeeded() 方法。 這將迫使該視圖根據最新的約束來布局其子視圖。
在 ViewController.swift
中,更新 animateLabelTransitions() 以更改約束常量,然后強制更新視圖的布局。
func animateLabelTransitions() {
??// Animate the alpha
??// and the center X constraints
??let screenWidth = view.frame.width
??self.nextQuestionLabelCenterXConstraint.constant = 0
??self.currentQuestionLabelCenterXConstraint.constant += screenWidth
??UIView.animate(withDuration: 0.5,
????delay: 0,
????options: [],
????animations: {
??????self.currentQuestionLabel.alpha = 0
??????self.nextQuestionLabel.alpha = 1
??????self.view.layoutIfNeeded()
????},
????completion: { _ in
??????swap(&self.currentQuestionLabel,
????????&self.nextQuestionLabel)
??})
}
最后,在完成處理程序中,您需要交換兩個約束 outlet,并將 nextQuestionLabel
重置為屏幕左側。
func animateLabelTransitions() {
??// Animate the alpha
??// and the center X constraints
??let screenWidth = view.frame.width
??self.nextQuestionLabelCenterXConstraint.constant = 0
??self.currentQuestionLabelCenterXConstraint.constant += screenWidth
??UIView.animate(withDuration: 0.5,
????delay: 0,
????options: [],
????animations: {
??????self.currentQuestionLabel.alpha = 0
??????self.nextQuestionLabel.alpha = 1
??????self.view.layoutIfNeeded()
????},
????completion: { _ in
??????swap(&self.currentQuestionLabel,
????????&self.nextQuestionLabel)
??????swap(&self.currentQuestionLabelCenterXConstraint,
????????&self.nextQuestionLabelCenterXConstraint)
??????self.updateOffScreenLabel()
??})
}
構建并運行應用程序。 動畫幾乎完美。 標簽在屏幕上滑動和關閉,alpha 值也適當地動畫化。
有一個小問題要解決,但可能會有點難發現。 要發現它,請從模擬器(Command-T
)的 Debug
菜單中打開 Slow Animations
。 所有標簽的寬度都會被動畫化(要在 answerLabel
上看到這一點,您需要點擊 Show Answer
按鈕)。 這是因為內在內容大小會在文本更改時發生變化。 解決方法是強制視圖在動畫開始之前布局其子視圖。 這將在 alpha 和滑動動畫開始之前更新所有三個標簽的邊框,以適應下一個文本。
更新 animateLabelTransitions() 以強制視圖在動畫開始之前布局其子視圖。
func animateLabelTransitions() {
??// Force any outstanding layout changes to occur
??view.layoutIfNeeded()
??// Animate the alpha
??// and the center X constraints
??let screenWidth = view.frame.width
??self.nextQuestionLabelCenterXConstraint.constant = 0
??self.currentQuestionLabelCenterXConstraint.constant += screenWidth
??UIView.animate(withDuration: 0.5,
????delay: 0,
????options: [],
????animations: {
??????self.currentQuestionLabel.alpha = 0
self.nextQuestionLabel.alpha = 1
??????self.view.layoutIfNeeded()
????},
????completion: { _ in
??????swap(&self.currentQuestionLabel,
????????&self.nextQuestionLabel)
??????swap(&self.currentQuestionLabelCenterXConstraint,
????????&self.nextQuestionLabelCenterXConstraint)
??????self.updateOffScreenLabel()
??})
}
構建并運行應用程序并循環瀏覽一些問題和答案。 動畫小問題現已解決。
定時功能
動畫的加速由其定時功能控制。 默認情況下,動畫使用 ease-in/ease-out
的定時功能。 用駕駛來類比,這意味著司機從休息時間平穩地加速到恒定的速度,然后在最后逐漸減速,休息。
其他定時功能包括 線性(linear)
(從頭到尾的恒定速度),緩和(ease-in)
(加速到恒定速度,然后突然終止)和 緩解(ease-out)
(從全速開始,然后最終減慢)。
在 ViewController.swift
中,更新 AnimateLabelTransitions() 中的動畫以使用線性定時函數。
UIView.animate(withDuration: 0.5,
??delay: 0,
??options: [
.curveLinear
],
??animations: {
????self.currentQuestionLabel.alpha = 0
????self.nextQuestionLabel.alpha = 1
????self.view.layoutIfNeeded()
??},
??completion: { _ in
??swap(&self.currentQuestionLabel, &self.nextQuestionLabel)
??swap(&self.currentQuestionLabelCenterXConstraint, &self.nextQuestionLabelCenterXConstraint)
??self.updateOffScreenLabel()
})
現在,與使用默認的 easy-in/ease-out 動畫曲線相反,動畫將具有線性動畫曲線。 構建并運行應用程序。 差異是微妙的,但如果你注意它也是能發現的。
options 參數接受 UIViewAnimationOptions 參數。 為什么這個參數在方括號內? 除了定時功能之外,動畫還有很多選擇。 因此,您需要一種指定多個選項(數組)的方法。 UIViewAnimationOptions 符合 OptionSet 協議,允許您使用數組對多個值進行分組。
以下是可以傳入options 參數的一些可能的動畫選項。
動畫曲線選項控制動畫的加速。 可能的值是:
UIViewAnimationOptions.curveEaseInOut
UIViewAnimationOptions.curveEaseIn
UIViewAnimationOptions.curveEaseOut
UIViewAnimationOptions.curveLinear
UIViewAnimationOptions.allowUserInteraction
默認情況下,視圖無法與動畫相互影響。 指定此選項將覆蓋默認值。 這可以用于重復動畫,例如脈沖視圖。
UIViewAnimationOptions.repeat
無限期重復動畫; 經常與 UIViewAnimationOptions.autoreverse
選項配對。
UIViewAnimationOptions.autoreverse
向前運行動畫,然后向后,將視圖返回到其初始狀態。
請務必查看 UIView Class Reference
中的 Constants
部分,以查看所有可能的選項。
青銅挑戰:彈簧動畫
iOS內置了一個強大的物理引擎。利用這種動力的一個簡單的方法是使用彈簧動畫。
// UIView
class func animate(withDuration duration: TimeInterval,
??delay: TimeInterval,
??usingSpringWithDamping dampingRatio: CGFloat,
??initialSpringVelocity velocity: CGFloat,
??options: UIViewAnimationOptions,
??animations: () -> Void,
??completion: ((Bool) -> Void)?)
使用這種方法使兩個標簽以彈簧的形式在屏幕上彈入和彈出。 參考 UIView 文檔來了解每個參數。
白銀挑戰:Layout Guides
如果您旋轉為橫向,則 nextQuestionLabel
變得可見。 不要使用硬編碼間距約束的常量,使用 UILayoutGuide 的實例將兩個標簽分開。 該 layout guide 應該具有等于 ViewController 視圖的寬度約束,以確保在不動畫時 nextQuestionLabel
保持在屏幕外。