UIKit 力學教程

你可能已經注意到 iOS 7 中似乎有一些自相矛盾的地方,蘋果在建議放棄真實世界的隱喻和擬物化同時,又鼓勵創造體驗真實的用戶界面。

在實踐中這意味着什麼呢?iOS 7 的設計目標是鼓勵創造能像真實的物理對象一樣響應觸摸、手勢和方向變化的數字界面,而不是像素的簡單堆砌。最終,區別於形式上的擬物化,讓用戶與界面產生更爲深刻的聯繫。

這個任務聽起來很艱鉅,因爲做一個看起來很真實的數字界面,要比做一個體驗真實的界面簡單得多。值得慶幸的是,你有一些很讚的新工具可以幫助你完成這個任務:UIKit 力學(Dynamics)和動態效果(Motion Effects)。

譯者注:關於 UIKit Dynamics 的中譯名,我與許多開發者有過討論,有動力、動力模型、動態等譯法。但我認爲譯爲力學更爲貼切,希望文中出現的力學知識能讓你認同我的看法。

  • UIKit 力學是一個集成到 UIKit 的完整的物理引擎。它使你可以通過添加諸如重力、吸附(彈簧)和作用力等行爲(behavior),創造體驗真實的界面。你只需定義你的界面元素需要遵從的物理特性,剩下的事交給力學引擎處理即可。
  • 動態效果讓你能夠創造相當酷的視差效果,例如 iOS 7 主屏的動態背景。簡單來說,你可以利用手機的加速計提供的數據,開發能夠響應手機方向變化的界面。

同時使用動態效果和力學效果,是讓數字界面和體驗變得栩栩如生的利器。當你的用戶看到你的應用以一種自然的、動態的形式響應他們的操作時,就和你的應用產生了更深層次的聯繫。

開始吧

UIKit 力學非常有意思,最好的學習方法就是從一些小的例子開始。

打開 Xcode,選擇 File / New / Project … 然後選擇 iOSApplicationSingle View Application 並且命名新的工程爲 DynamicsPlayground。創建完工程後,打開 ViewController.m 並且添加下面的代碼到 viewDidLoad 的末尾:

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. UIView* square = [[UIView alloc] initWithFrame:  
  2.                                 CGRectMake(100100100100)];  
  3. square.backgroundColor = [UIColor grayColor];  
  4. [self.view addSubview:square];  

以上代碼簡單地添加了一個方塊 UIView 到界面上。

編譯運行,你可以看到如下圖所示方塊:

LonelySquare

如果你在真機上運行 App,嘗試轉動手機,上下顛倒,或者搖動它。什麼都沒發生?那就對了,理應如此。因爲當你向界面中添加一個視圖的時候,你希望他保持穩定的 frame,直到你添加一些力學行爲到界面中。

添加重力

繼續編輯 ViewController.m,添加以下實例變量:

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. UIDynamicAnimator* _animator;  
  2. UIGravityBehavior* _gravity;  

在 viewDidLoad 末尾添加以下代碼:

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. _animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];  
  2. _gravity = [[UIGravityBehavior alloc] initWithItems:@[square]];  
  3. [_animator addBehavior:_gravity];  

我稍後再解釋這些代碼,現在,你只需編譯運行你的程序。你應該會看到方塊漸漸地加速下墜,直到落到屏幕之外,如下圖所示:

FallingSquare

在剛添加的代碼中,出現了一些力學相關的類:

  • UIDynamicAnimator 是 UIKit 物理引擎。這個類會記錄你添加到引擎中的各種行爲(比如重力),並且提供全局上下文。當你創建動畫實例時,需要傳入一個參考視圖用於定義座標系。
  • UIGravityBehavior 把重力的行爲抽象成模型,並且對一個或多個元素施加作用力,讓你可以建立物理交互模型。當你創建一個行爲實例的時候,你需要把它關聯到一組元素上,一般是一組視圖。這樣你就能選擇受該行爲影響的元素,在這個例子中就是指受重力影響的元素。

大部分行爲有一些可配置屬性。比如重力行爲允許你改變它的角度和量級。嘗試修改這些屬性使你的物體向上、側向或斜向以不同的加速度移動。

注意:關於單位:在物理學中,重力(g)單位是米每平方秒,約爲 9.8 m/s2。根據牛頓第二定律,你可以用下面公式計算在重力作用下,物體移動的距離:

距離 = 0.5 × g × 時間^2

在 UIKit 力學中,公式依然適用,但單位有所不同。單位中的米每平方秒要用像素每平方秒代替。基於重力參數,應用牛頓第二定律你任然可以計算出視圖在任意時間的位置。

你真的需要了解這些麼?未必,你只需要知道更大的 g 意味着掉落得更快,但是瞭解背後的數學原理有利無弊。

設置邊界

雖然你看不到,但是當方塊消失在屏幕邊緣的時候,它其實還在繼續下落。爲了使它保持在屏幕範圍之內,你需要定義一個邊界。

在 ViewController.m 裏添加另一個實例變量:

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. UICollisionBehavior* _collision;  

在 viewDidLoad 末尾加入以下代碼:

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. _collision = [[UICollisionBehavior alloc]  
  2.                                       initWithItems:@[square]];  
  3. _collision.translatesReferenceBoundsIntoBoundary = YES;  
  4. [_animator addBehavior:_collision];  

上面的代碼創建了一個碰撞行爲,定義了一個或多個邊界,以決定相關元素之間如何互相影響。

上面的代碼沒有顯式地添加邊界座標,而是設置 translatesReferenceBoundsIntoBoundary 屬性爲 YES。這意味着用提供給 UIDynamicAnimator 的視圖的 bounds 作爲邊界。

編譯並運行,你會看到方塊在碰到屏幕底部之後,輕輕彈起,並最終靜止,如下圖所示:

SquareAtRest

這是一個很讚的行爲,特別是看到如此少的代碼量。

處理碰撞

接下來你要添加一個固定的障礙物,他會跟下落的方塊碰撞並相互影響。在 viewDidLoad 中添加方塊的代碼之後加入以下代碼:

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. UIView* barrier = [[UIView alloc] initWithFrame:CGRectMake(030013020)];  
  2. barrier.backgroundColor = [UIColor redColor];  
  3. [self.view addSubview:barrier];  

編譯並運行應用,你可以看到一個紅色的“障礙物”橫跨在屏幕中間。但是你會發現他沒有起到任何作用,方塊直接穿過了障礙物:

BadBarrier

這顯然不是你想要的,這也說明了很重要的一點:力學隻影響關聯到行爲上的視圖。

下面是一個簡單的示意圖:

DynamicClasses

關聯 UIDynamicAnimator 到提供座標系的參考視圖,然後添加一個或多個行爲來對關聯的物體施加作用力。大部分行爲可以與多個物體關聯,每個物體可以與多個行爲關聯。上圖展示了當前應用中的行爲以及他們的關係。

當前代碼裏的行爲都沒有涉及到障礙物,因此在力學引擎中,這個障礙物並不存在。

使物體響應碰撞

爲了讓方塊與障礙物碰撞,找到初始化碰撞行爲的代碼並用下面的代碼替代:

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. _collision = [[UICollisionBehavior alloc] initWithItems:@[square, barrier]];  

碰撞實例需要知道它所影響的每一個視圖,因此添加障礙物到列表中使得碰撞對障礙物也有作用。

編譯並運行應用,兩個物體碰撞並相互影響,如下圖所示:

GoodBarrier

碰撞行爲在每個關聯的物體周圍形成一個邊界,使得這些物體從可以互相穿越的物體變成實體無法穿越。

更新一下前面的示意圖,現在碰撞行爲與兩個視圖都關聯起來了:

DynamicClasses2

但是現在還有一些有出入的地方。我們希望障礙物是不可移動的,但是當前設置下,當兩個物體碰撞的時候,障礙物被撞開並且旋轉着落向屏幕底部。

更奇怪的是,障礙物從底部彈起後似乎沒有趨於靜止的意思。這是因爲重力沒有對障礙物產生影響,這也解釋了爲什麼在方塊撞到障礙物之前它沒有移動。

你需要另一種解決問題的思路。既然障礙物是不可移動的,那麼力學引擎就沒有必要知道它的存在。但是如何檢測碰撞呢?

不可見的邊界和碰撞

把碰撞行爲的初始化代碼改回原先的樣子,使他只知道方塊的存在:

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. _collision = [[UICollisionBehavior alloc] initWithItems:@[square]];  

然後,添加如下邊界:

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. // add a boundary that coincides with the top edge  
  2. CGPoint rightEdge = CGPointMake(barrier.frame.origin.x +  
  3.                                 barrier.frame.size.width, barrier.frame.origin.y);  
  4. [_collision addBoundaryWithIdentifier:@"barrier"  
  5.                             fromPoint:barrier.frame.origin  
  6.                               toPoint:rightEdge];  

上述代碼添加了一個不可見的邊界,它正是障礙物的上邊界。紅色障礙物對用戶依然是可見的,但是力學引擎不知道它的存在;相反,添加的邊界對於力學引擎是可見的,對於用戶是不可見的。當方塊下落的時候,看起來與障礙物發生了碰撞,其實它碰到了不可移動的邊界。

編譯並運行應用,你看到的效果如下圖所示:

BestBarrier

方塊現在從障礙物的邊界彈起,旋轉,然後繼續落到屏幕底部直到靜止。

到現在爲止,UIKit 力學的強大之處可見一斑:你只需要幾行簡單的代碼就可以實現相當複雜的效果。在這背後有許多複雜的邏輯,在下個章節會涉及力學引擎與應用中物體交互的具體方式。

碰撞的背後

每一個力學行爲都有一個 action 屬性,你可以定義一個 block 使其在動畫的每一步被執行。添加下面的代碼到viewDidLoad

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. _collision.action =  ^{  
  2.     NSLog(@"%@, %@",   
  3.           NSStringFromCGAffineTransform(square.transform),   
  4.           NSStringFromCGPoint(square.center));  
  5. };  

上面的代碼記錄了下落的方塊的中點位置核 transform 屬性。編譯並運行應用,你可以在 Xcode 的控制檯中看到調試信息。

在前 400 毫秒左右你會看到類似這樣的信息:

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. 2013-07-26 08:21:58.698 DynamicsPlayground[17719:a0b] [10010, 0], {150236}  
  2. 2013-07-26 08:21:58.715 DynamicsPlayground[17719:a0b] [10010, 0], {150243}  
  3. 2013-07-26 08:21:58.732 DynamicsPlayground[17719:a0b] [10010, 0], {150250}  

可以看到力學引擎在動畫過程中不斷改變方塊的中點位置,或者說它的 frame。

當方塊撞到障礙物的時候,它開始旋轉,這時候的調試信息類似這樣:

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. 2013-07-26 08:21:59.182 DynamicsPlayground[17719:a0b] [0.106792340.99428135, -0.994281350.106792340, 0], {198325}  
  2. 2013-07-26 08:21:59.198 DynamicsPlayground[17719:a0b] [0.0513737020.99867952, -0.998679520.0513737020, 0], {199331}  
  3. 2013-07-26 08:21:59.215 DynamicsPlayground[17719:a0b] [-0.00400367710.99999201, -0.99999201, -0.00400367710, 0], {201338}  

你可以看到力學引擎根據物理模型計算並同時改變 transform 屬性和 frame 屬性來定位視圖。

雖然這些屬性的具體取值沒什麼意思,但是很重要的一點是他們每時每刻都在改變。因此如果你嘗試用代碼改變物體的 frame 或者 transform 屬性,這些值會被覆蓋。這意味着當你的物體受力學引擎控制的時候,你不能通過 transform 來縮放你的物體。

力學行爲的方法名裏用的是 items 而不是 views,這是因爲想要使用力學行爲的對象只需實現 UIDynamicItem 協議即可,定義如下:

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. @protocol UIDynamicItem   
  2.    
  3. @property (nonatomicreadwrite) CGPoint center;  
  4. @property (nonatomicreadonly) CGRect bounds;  
  5. @property (nonatomicreadwrite) CGAffineTransform transform;  
  6.    
  7. @end  

UIDynamicItem 協議爲力學引擎提供了讀寫 center 和 transform 屬性的權限,使它可以根據內部的計算結果移動物體。同時提供了 bounds 的讀權限,用以確定物體的大小,這不但在計算物體邊界的時候被用到,同時在物體受力時用於計算物體的質量。

這個協議說明力學引擎與 UIView並不耦合,其實 UIKit 中還有一個類也實現了這個協議 –UICollectionViewLayoutAttributes。所以可以通過力學引擎對 collection views 實現動畫效果。

碰撞通知

到現在,你添加了一些視圖和行爲,然後讓力學引擎接手剩下的任務。在接下來的內容中你會看到如何接收物體碰撞時的通知。

打開 ViewController.m 並且實現 UICollisionBehaviorDelegate 協議:

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. @interface ViewController ()   
  2.    
  3. @end  

還是在 viewDidLoad 中,在初始化碰撞行爲後,設置當前 view controller 爲其代理(delegate),代碼如下:

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. _collision.collisionDelegate = self;  

然後,添加一個碰撞行爲的代理方法:

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. - (void)collisionBehavior:(UICollisionBehavior *)behavior beganContactForItem:(id)item   
  2.             withBoundaryIdentifier:(id)identifier atPoint:(CGPoint)p {  
  3.     NSLog(@"Boundary contact occurred - %@", identifier);  
  4. }  

當碰撞發生的時候,這個代理方法會被調用並且在控制檯打印出調試信息。爲了避免控制檯的信息太亂,你可以刪除之前在 _collision.action 裏添加的調試信息。

編譯運行,物體相互碰撞,你會在控制檯看到如下信息:

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. 2013-07-26 08:44:37.473 DynamicsPlayground[18104:a0b] Boundary contact occurred - barrier  
  2. 2013-07-26 08:44:37.689 DynamicsPlayground[18104:a0b] Boundary contact occurred - barrier  
  3. 2013-07-26 08:44:38.256 DynamicsPlayground[18104:a0b] Boundary contact occurred - (null)  
  4. 2013-07-26 08:44:38.372 DynamicsPlayground[18104:a0b] Boundary contact occurred - (null)  
  5. 2013-07-26 08:44:38.455 DynamicsPlayground[18104:a0b] Boundary contact occurred - (null)  
  6. 2013-07-26 08:44:38.489 DynamicsPlayground[18104:a0b] Boundary contact occurred - (null)  
  7. 2013-07-26 08:44:38.540 DynamicsPlayground[18104:a0b] Boundary contact occurred - (null)  

從調試信息中可以看到方塊碰了兩次障礙物,也就是之前添加的不可見的邊界。(null) 則是指參考視圖的邊界。

這些調試信息讀起來很有意思(認真的),但是如果能在碰撞時觸發一些視覺反饋,那就更有意思了。

在輸出調試信息的代碼之後添加如下代碼:

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. UIView* view = (UIView*)item;  
  2. view.backgroundColor = [UIColor yellowColor];  
  3. [UIView animateWithDuration:0.3 animations:^{  
  4.     view.backgroundColor = [UIColor grayColor];  
  5. }];  

上述代碼會改變碰撞的物體的背景色爲黃色,然後漸變回灰色。

編譯運行,看一下實際效果:

YellowCollision

每次方塊與邊界發生碰撞的時候,它都會閃現黃色。

UIKit 力學會根據物體的 bounds 計算並自動設置它們的物理屬性(如質量或彈性係數)。接下來你會看到如何使用 UIDynamicItemBehavior 類控制這些物理屬性。

設置物體屬性

在 viewDidLoad 的末尾,添加下面的代碼:

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. UIDynamicItemBehavior* itemBehaviour = [[UIDynamicItemBehavior alloc] initWithItems:@[square]];  
  2. itemBehaviour.elasticity = 0.6;  
  3. [_animator addBehavior:itemBehaviour];  

上面的代碼創建了一個物體行爲(item behavior),把它關聯到方塊,然後添加該行爲到動畫實例(animator)。彈性係數屬性(elasticity)控制物體的彈性,取 1.0 表示完全彈性碰撞,也就是說碰撞中沒有能量或速度的損失。你剛剛設置了方塊的彈性係數爲 0.6,意味着方塊在每次彈起的時候速度都會減慢。

編譯運行應用,你會發現現在的方塊比之前更有彈性,如下:

PrettyBounce

注: 如果你想知道我是如何生成如上圖片來展現方塊的歷史位置,其實相當簡單!我給行爲的 action 屬性添加了一個簡單的 block,每執行五次,用當前方塊的中點位置和 transform 屬性,添加一個新的方塊到當前視圖。

在上面的代碼中,你只改變了物體的彈性係數,然後物體的行爲類還有很多其他可以調整的屬性。有下列屬性:

  • elasticity(彈性係數) – 決定了碰撞的彈性程度,比如碰撞時物體的彈性。
  • friction(摩擦係數) – 決定了沿接觸面滑動時的摩擦力大小。
  • density(密度) – 跟 size 結合使用,來計算物體的總質量。質量越大,物體加速或減速就越困難。
  • resistance(阻力) – 決定線性移動的阻力大小,這根摩擦係數不同,摩擦係數只作用於滑動運動。
  • angularResistance(轉動阻力) – 決定轉動運動的阻力大小。
  • allowsRotation(允許旋轉) – 這個屬性很有意思,它在真實的物理世界沒有對應的模型。設置這個屬性爲 NO 物體就完全不會轉動,無力受到多大的轉動力。

動態添加行爲

現在的情況下,你的應用設置系統的所有行爲,然後由力學引擎處理系統的物理行爲,直至所有物體靜止。在下一步中,你會看到如何動態添加或刪除行爲。

打開 ViewController.m 並添加如下實例變量:

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. BOOL _firstContact;  

添加下面的代碼到碰撞代理方法collisionBehavior:beganContactForItem:withBoundaryIdentifier:atPoint: 的末尾:

[objc] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. if (!_firstContact)  
  2. {  
  3.     _firstContact = YES;  
  4.    
  5.     UIView* square = [[UIView alloc] initWithFrame:CGRectMake(300100100)];  
  6.     square.backgroundColor = [UIColor grayColor];  
  7.     [self.view addSubview:square];  
  8.    
  9.     [_collision addItem:square];  
  10.     [_gravity addItem:square];  
  11.    
  12.     UIAttachmentBehavior* attach = [[UIAttachmentBehavior alloc] initWithItem:view  
  13.                                                                attachedToItem:square];  
  14.     [_animator addBehavior:attach];  
  15. }  

上面的代碼檢測到方塊和障礙物的第一次接觸時,創建第二個方塊並添加到碰撞和重力行爲中。此外,設置了一個吸附行爲,實現兩個物體之間加入虛擬的彈簧的效果。

編譯運行應用,當原有的方塊撞到障礙物時,你應該會看到一個新的方塊出現,如下:

Attachment

雖然兩個方塊看起來被連接到一起,但是因爲沒有在屏幕上畫線條或是彈簧,你並不會看到視覺上的聯繫。

接下來做什麼?

現在你應該比較瞭解 UIKit 力學的核心概念了。

如果你有興趣學習更多關於 UIKit 力學的內容,可以關注我們的新書 iOS 7 教程集。書中結合你在本文學到的內容,有更深入的內容,展示瞭如何在現實場景中利用 UIKit 力學:

SandwichFlowDynamics

用戶可以上拉一個食譜來預覽它,當用戶鬆開食譜的時候,它會落回菜單中,或是停靠在屏幕頂部。最終的成品是一個有真實物理體驗的應用。

我希望你喜歡這個 UIKit 力學教程 – 我們覺得這很酷,並且期待看到你在應用中付諸有創意的交互。如果你有任何問題或評論,請加入下面的論壇討論!

你在這個教程中創建的 Dynamics Playground 工程的完整代碼可以在 github 上找到,文中每一步編譯運行都對應一次提交。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章