本文譯自UIKit Dynamics Tutorial: Getting Started
iOS鼓勵開發者設計app時使用觸摸、手勢及方向旋轉,不同于簡單的圖形,就像現實世界物理驅動一樣。
結果就是用戶與界面有了更深的連接,而不是簡單的擬真。
這聽起來像個艱難的任務,看起來真實比感覺真實更容易一些,然而,現在有了新的工具可以用:UIKit Dynamics和Motion Effects。
UIKit Dynamics是UIKit中的物理引擎,它能夠幫助創建真實的物理行為:重力、吸附(彈簧)、彈性。
定義好想要的物理特征,物理引擎會幫你完成剩下的工作。Motion Effects能夠幫助你創建酷酷的視差效果,就像iOS7的主屏幕一樣。
基本上你可以通過手機的加速計來響應手機方向的變化。
開始
UIKit dynamics有很多樂趣,最好的學習方法就是開始動手。
打開Xcode,選擇File / New / Project … ,然后選iOS Application / Single View Application,
再輸入項目名稱DynamicsDemo。項目就創建好了,打開ViewController.swift,
將下面的代碼添加到viewDidLoad的最后。
let square = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
square.backgroundColor = UIColor.grayColor()
view.addSubview(square)
這段代碼簡單的在界面上創建了一個正方形UIView。
編譯運行一下,一個孤零零的正方形在屏幕上,就像下面這樣:
如果你是在真機上運行的,試著傾斜下你的手機,頭朝下,活著搖一搖,什么都沒發生?
那就對了 — 所有事情都要先計劃。當你添加view到界面上后它會一直在那個地方,直到給它加上了動態效果。
添加重力
還是ViewController.swift,添加下面的屬性到viewDidLoad的上方:
var animator: UIDynamicAnimator!
var gravity: UIGravityBehavior!
這些屬性是implicitly-unwrapped optionals(隱式解析可選)(屬性名后加了感嘆號)。
這些屬性一定是可選的,因為你不能用init方法初始化。
你可以用隱式解析可選是因為初始化后我們知道那些屬性不可能是nil。
這樣可以防止你每次訪問屬性時都加上感嘆號。
將下面的代碼添加到viewDidLoad的最后:
animator = UIDynamicAnimator(referenceView: view)
gravity = UIGravityBehavior(items: [square])
animator.addBehavior(gravity)
說明一下。現在編譯運行下。你會看到正方形緩慢地下墜,直到觸底,就像下面這樣:
剛添加的代碼里,有幾個動態類:
UIDynamicAnimator是UIKit的物理引擎。這個類能夠跟蹤你添加的各種行為,如重力,并提供整體上下文。
當你創建一個animator的實例時,傳遞一個引用view來定義它的坐標系統。UIGravityBehavior是重力行為的模型,可以給一個或多個item施加力度,讓你模擬物理相互作用。
當你創意一個行為的實例時,設置一組item跟行為關聯起來 — 必須是view。
你可以通過這種方式選擇讓哪個item受到行為的影響,在本例子中item受到重力影響。
大部分行為類都有幾個配置參數,舉例,重力行為可以改變它的角度和量級。
試著修改這些屬性讓物體的下降速度加快,或者對角用不同的速度。
注意:在物理世界中,重力表示每秒下降多少米,大約是9.8 m/s2。
利用牛頓第二定律,你可以用下列公式計算一個物體在重力的影響下離底面有多遠:
distance = 0.5 × g × time2
在UIKit Dynamics,公式是相同的,但單位不同。當然不是米,以每秒數千像素為單位。
利用牛頓第二定律,你的view根據你提供的重力正常工作。
你真的需要理論知識嗎,不;你只需要知道g越大下墜速度越快,不需要知道底層算法。
設置邊界
盡管看不見它,即使已經碰到了屏幕最底部正方形依然會不停地下墜。
為了讓正方形留在屏幕上,你需要給它定義邊界。
添加另一個屬性到ViewController.swift:
var collision: UICollisionBehavior!
將下面的代碼添加到viewDidLoad的最后:
collision = UICollisionBehavior(items: [square])
collision.translatesReferenceBoundsIntoBoundary = true
animator.addBehavior(collision)
上面這段代碼創建了一個碰撞檢測行為,
不是直接用坐標描繪邊界,另外還是設置屬性translatesReferenceBoundsIntoBoundary為true。
這是因為UIDynamicAnimator在設置view的邊界時使用了bounds屬性。
編譯運行,正方形碰到屏幕底部后回彈了一下,然后靜止不動了。
這是個令人印象深刻的行為,特別是只寫了那么少的代碼。
處理碰撞
下一步,在下墜的過程中添加一個障礙,正方形會撞到障礙。
將下面的代碼插入到viewDidLoad中正方形的下面:
let barrier = UIView(frame: CGRect(x: 0, y: 300, width: 130, height: 20))
barrier.backgroundColor = UIColor.redColor()
view.addSubview(barrier)
編譯運行,一個紅色的障礙橫插在屏幕中間。但是,障礙并不會阻礙正方形下墜:
這不是我們想要的效果,還有個重要的提示:dynamics只會影響和行為關聯的view。
圖解:
UIDynamicAnimator會關聯一個Reference View,并由Reference View提供坐標系統。
然后添加一個或多個行為,并和Square關聯起來。
多部分行為可以關聯多個item,那個item又可以關聯多個行為。
上面的流程圖展示了當前的行為和它們關聯的對象。
現在已經可以看見障礙物了,但還沒有和物理引擎關聯起來,就像不存在一樣。
碰撞反應
為了讓正方形和障礙物相互碰撞,替換collision的初始化方法:
collision = UICollisionBehavior(items: [square, barrier])
將互相碰撞的兩個view作為參數傳遞給collision;障礙物才能生效。
編譯運行,兩個view會相互碰撞,就像下面這樣:
collision behavior給每個view的四周都添加了一個看不見的邊框;讓原本可以相互穿過的view變的更堅硬。
更新之前的圖解,collision behavior現在將兩個view關聯起來了:
然而,兩個view之間的交互仍然存在一些問題。障礙物應該不靜止不動的,但障礙物被撞后會朝著底部下墜。
更奇怪的是,障礙物在觸底后會回彈,沒有像正方形那樣靜止不動,原因就是gravity behavior沒有給障礙物施加影響。
這就是為什么障礙物在被正方形撞到之前都不會動。
看起來要換個方法解決問題。障礙物是靜止不動的,所以不需要和dynamics engine關聯起來。但要如何檢測碰撞呢?
看不見的邊框碰撞
collision behavior的初始化還原為最初的狀態:
collision = UICollisionBehavior(items: [square])
collision的下面再加一行:
// add a boundary that has the same frame as the barrier
collision.addBoundaryWithIdentifier("barrier", forPath: UIBezierPath(rect: barrier.frame))
上面這段代碼在障礙物相同的frame放置了一個看不見的邊框。
紅色的障礙物仍然可見,但沒有添加到dynamics engine,
而用戶雖然看不見邊框,但邊框仍然可以觸發碰撞。
正方形在下墜時,撞上了障礙物,但實際上撞的是看不見的邊框。
編譯運行,就像下面這張圖一樣:
正方形在撞到邊框后,旋轉了一下,然后一直下墜直到觸底。
現在UIKit Dynamics的功能大致清楚了:只需要少量的代碼就可以完成復雜的物理現象。
還有一些隱藏的功能;下一節將展示更多物理引擎的細節。
碰撞檢測的幕后推手
每個dynamic behavior都有個action屬性,動畫每一幀都會執行action block。
添加下面的代碼到viewDidLoad:
collision.action = {
println("\(NSStringFromCGAffineTransform(square.transform)) \(NSStringFromCGPoint(square.center))")
}
這段代碼打印了正方形的center和transform屬性。
編譯運行,會看到log輸出到console window。
400毫秒以內的log看上去是這個樣子:
[1, 0, 0, 1, 0, 0], {150, 236}
[1, 0, 0, 1, 0, 0], {150, 243}
[1, 0, 0, 1, 0, 0], {150, 250}
dynamics engine在動畫的每一幀都會修改正方形的center。
當正方形撞到了障礙物會開發旋轉,log信息會像下面這樣:
[0.99797821, 0.063557133, -0.063557133, 0.99797821, 0, 0] {152, 247}
[0.99192101, 0.12685727, -0.12685727, 0.99192101, 0, 0] {154, 244}
[0.97873402, 0.20513339, -0.20513339, 0.97873402, 0, 0] {157, 241}
可以看到dynamics engine使用了底層物理模型修改view的transform和frame偏移,從而改變view的位置。
雖然了解這些屬性的精確值沒什么用,但重要的是知道哪些屬性被用到了。
因此,如果寫代碼修改view的frame或transform,這些值會被覆蓋掉。
這意味著,當view在dynamics的控制下時,不可以使用transform屬性。
dynamic behaviors的方法簽名使用術語而不是view。
唯一的條件是實現協議UIDynamicItem,就像下面這樣:
protocol UIDynamicItem : NSObjectProtocol {
var center: CGPoint { get set }
var bounds: CGRect { get }
var transform: CGAffineTransform { get set }
}
UIDynamicItem協議提供了動態讀寫訪問center和transform屬性,從而達到基于內部算法來移動item的效果。
它也有對bounds屬性的訪問權限,可以用來確定item的大小。這使得它能夠在四周創建碰撞邊框以及計算它的質量。
這個協議意味著dynamics與UIView解耦合;的確還有另一個UIKit類不是view確使用了這個協議:UICollectionViewLayoutAttributes。
這使得dynamics可以給collection views添加動畫。
碰撞通知
到目前為止,你已經添加了一些view和behaviors,然后讓dynamics接管他們。
在這一節中將學習如何在他們發生碰撞時收到通知。
還是在ViewController.swift,給類定義添加UICollisionBehaviorDelegate:
class ViewController: UIViewController, UICollisionBehaviorDelegate {
在viewDidLoad中,在collision初始化后設置viewController為委托:
collision.collisionDelegate = self
下一步,添加collision behavior委托方法:
func collisionBehavior(behavior: UICollisionBehavior!, beganContactForItem item: UIDynamicItem!, withBoundaryIdentifier identifier: NSCopying!, atPoint p: CGPoint) {
println("Boundary contact occurred - \(identifier)")
}
在發生碰撞時會執行這個委托方法。只在控制臺輸出log。為了避免控制臺的log太亂,可以選擇刪除collision.action的輸出。
編譯運行,當兩個view即將碰撞時,會看到這樣的log:
Boundary contact occurred - barrier
Boundary contact occurred - barrier
Boundary contact occurred - nil
Boundary contact occurred - nil
Boundary contact occurred - nil
Boundary contact occurred - nil
從這個log信息從可以看出正方形和barrier碰撞了兩次。
(null)identifier是外層reference view的邊框。
這些log信息有很好的易讀性,但如果item在回彈出現一些視覺指示會更友好。
在輸出log代碼的下面,添加這些代碼:
let collidingView = item as UIView
collidingView.backgroundColor = UIColor.yellowColor()
UIView.animateWithDuration(0.3) {
collidingView.backgroundColor = UIColor.grayColor()
}
這段代碼是在item發生碰撞時將它的背景色設置為黃色,又很快重新回到了灰色。
編譯運行后可以看到這樣的效果:
正方形在碰到邊界是會閃成黃色。
到目前為止,UIKit Dynamics會根據view的bounds自動計算物理屬性(比如質量、彈力)。
下一步將學習如何使用UIDynamicItemBehavior來控制物理屬性。
配置屬性
將下面的代碼添加到viewDidLoad方法的最后:
let itemBehaviour = UIDynamicItemBehavior(items: [square])
itemBehaviour.elasticity = 0.6
animator.addBehavior(itemBehaviour)
這段代碼創建了一個item behavior,跟square關聯起來了,然后添加到了animator。
elasticity屬性控制item的反彈力;值等于1時表示完全彈性碰撞;意味著,碰撞后不會減速或停下。
square設置為了0.6,這意味著每反彈一次速度就會下降一點。
編譯運行,會發現square變的非常有彈性:
在上面的代碼中只修改了elasticity;然而,還可以修改其他的屬性:
- elasticity – 確定在碰撞時有多大的彈性
- friction – 確定在沿表面滑動時有多少阻力
- density – 結合size時,會給item一個總質量,質量越大越難加速,質量越大減速越快。
- resistance – 確定在直線移動時會收到多少阻力。跟friction的區別是,這僅適用于滑動。
- angularResistance – 確定在旋轉運動時會收到多少阻力。
- allowsRotation – 這是個有趣的屬性,它并不模擬真實世界的物理特性。當它設置為NO時便不會再旋轉,無論有多少旋轉力度。
動態添加behaviors
目前,
打開ViewController.swift,在viewDidLoad方法的上面添加屬性:
var firstContact = false
在collision delegate的委托方法collisionBehavior(behavior:beganContactForItem:withBoundaryIdentifier:atPoint:)的最后添加:
if (!firstContact) {
firstContact = true
let square = UIView(frame: CGRect(x: 30, y: 0, width: 100, height: 100))
square.backgroundColor = UIColor.grayColor()
view.addSubview(square)
collision.addItem(square)
gravity.addItem(square)
let attach = UIAttachmentBehavior(item: collidingView, attachedToItem:square)
animator.addBehavior(attach)
}
上面的代碼檢查了barrier和square的初次接觸,創建了第二個正方形,且添加了碰撞和重力行為。
此外,還設置了吸附行為,做出了虛擬彈簧的吸附效果。
編譯運行,當正方形和障礙物初次碰撞后會有一個新的正方形初現,就像下面這樣:
雖然兩個正方形之間有連接,但看不見連接線或彈簧,因為沒有在屏幕上繪制它。
交互
如你所見,物理系統在工作時動態地添加和刪除行為。在最后一節,將添加另一種dynamics behaviour,UISnapBehavior,
無論用戶點擊屏幕的什么位置,UISnapBehavior會控制view跳到指定位置,并附帶彈簧動畫。
為了讓屏幕只呈現UISnapBehavior的效果。刪除上一節中添加的代碼:包括firstContact屬性和collisionBehavior()方法中的if判斷。
在viewDidLoad方法的上面添加兩個屬性:
var square: UIView!
var snap: UISnapBehavior!
square變成了屬性,這樣在viewController的任何地方都可以訪問。
下一步將使用snap。
在viewDidLoad中,移除掉squar前面的let關鍵字,它將變成新屬性,而不是局部變量:
square = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
最后,添加touchesEnded方法實現,當用戶點擊屏幕時創建一個新的snap behavior:
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
if (snap != nil) {
animator.removeBehavior(snap)
}
let touch = touches.anyObject() as UITouch
snap = UISnapBehavior(item: square, snapToPoint: touch.locationInView(view))
animator.addBehavior(snap)
}
這段代碼非常直接,先檢查snap behavior有沒有,如果有就刪除。
然后根據用戶點擊的位置創建一個新的snap behavior,然后添加到animator。
編譯運行。在四周點一點,square會快速移動到你點擊的位置!
何去何從
到這里你應該已經對UIKit Dynamics的核心要點有了深刻理解。
這里可以下載本文的最終示例。
UIKit Dynamics給app帶來了強大的物理引擎。你可以將反彈、彈簧和重力添加到你的app中,讓用戶更加身臨其境。
如果你想了解更多關于UIKit Dynamics的知識,可以閱讀iOS 7 By Tutorials。