翻譯:UIKit Dynamics Tutorial: Getting Started

本文譯自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)

說明一下。現在編譯運行下。你會看到正方形緩慢地下墜,直到觸底,就像下面這樣:

FallingSquare.png

剛添加的代碼里,有幾個動態類:

  • 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屬性。

編譯運行,正方形碰到屏幕底部后回彈了一下,然后靜止不動了。

SquareAtRest.png

這是個令人印象深刻的行為,特別是只寫了那么少的代碼。

處理碰撞

下一步,在下墜的過程中添加一個障礙,正方形會撞到障礙。
將下面的代碼插入到viewDidLoad中正方形的下面:

let barrier = UIView(frame: CGRect(x: 0, y: 300, width: 130, height: 20))
barrier.backgroundColor = UIColor.redColor()
view.addSubview(barrier)

編譯運行,一個紅色的障礙橫插在屏幕中間。但是,障礙并不會阻礙正方形下墜:

BadBarrier.png

這不是我們想要的效果,還有個重要的提示:dynamics只會影響和行為關聯的view。

圖解:

DynamicClasses.png

UIDynamicAnimator會關聯一個Reference View,并由Reference View提供坐標系統。
然后添加一個或多個行為,并和Square關聯起來。
多部分行為可以關聯多個item,那個item又可以關聯多個行為。
上面的流程圖展示了當前的行為和它們關聯的對象。

現在已經可以看見障礙物了,但還沒有和物理引擎關聯起來,就像不存在一樣。

碰撞反應

為了讓正方形和障礙物相互碰撞,替換collision的初始化方法:

collision = UICollisionBehavior(items: [square, barrier])

將互相碰撞的兩個view作為參數傳遞給collision;障礙物才能生效。

編譯運行,兩個view會相互碰撞,就像下面這樣:

GoodBarrier.png

collision behavior給每個view的四周都添加了一個看不見的邊框;讓原本可以相互穿過的view變的更堅硬。

更新之前的圖解,collision behavior現在將兩個view關聯起來了:

DynamicClasses2.png

然而,兩個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,
而用戶雖然看不見邊框,但邊框仍然可以觸發碰撞。
正方形在下墜時,撞上了障礙物,但實際上撞的是看不見的邊框。

編譯運行,就像下面這張圖一樣:

BestBarrier.png

正方形在撞到邊框后,旋轉了一下,然后一直下墜直到觸底。

現在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發生碰撞時將它的背景色設置為黃色,又很快重新回到了灰色。

編譯運行后可以看到這樣的效果:

YellowCollision.png

正方形在碰到邊界是會閃成黃色。

到目前為止,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變的非常有彈性:

PrettyBounce.png

在上面的代碼中只修改了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的初次接觸,創建了第二個正方形,且添加了碰撞和重力行為。
此外,還設置了吸附行為,做出了虛擬彈簧的吸附效果。

編譯運行,當正方形和障礙物初次碰撞后會有一個新的正方形初現,就像下面這樣:

Attachment.png

雖然兩個正方形之間有連接,但看不見連接線或彈簧,因為沒有在屏幕上繪制它。

交互

如你所見,物理系統在工作時動態地添加和刪除行為。在最后一節,將添加另一種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中,讓用戶更加身臨其境。

SandwichFlowDynamics.png

如果你想了解更多關于UIKit Dynamics的知識,可以閱讀iOS 7 By Tutorials

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容