你可能已經注意到 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
的末尾:
- UIView* square = [[UIView alloc] initWithFrame:
- CGRectMake(100, 100, 100, 100)];
- square.backgroundColor = [UIColor grayColor];
- [self.view addSubview:square];
以上代碼簡單地添加了一個方塊 UIView
到界面上。
編譯運行,你可以看到如下圖所示方塊:
如果你在真機上運行 App,嘗試轉動手機,上下顛倒,或者搖動它。什麼都沒發生?那就對了,理應如此。因爲當你向界面中添加一個視圖的時候,你希望他保持穩定的 frame,直到你添加一些力學行爲到界面中。
添加重力
繼續編輯 ViewController.m,添加以下實例變量:
- UIDynamicAnimator* _animator;
- UIGravityBehavior* _gravity;
在 viewDidLoad
末尾添加以下代碼:
- _animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
- _gravity = [[UIGravityBehavior alloc] initWithItems:@[square]];
- [_animator addBehavior:_gravity];
我稍後再解釋這些代碼,現在,你只需編譯運行你的程序。你應該會看到方塊漸漸地加速下墜,直到落到屏幕之外,如下圖所示:
在剛添加的代碼中,出現了一些力學相關的類:
-
UIDynamicAnimator
是 UIKit 物理引擎。這個類會記錄你添加到引擎中的各種行爲(比如重力),並且提供全局上下文。當你創建動畫實例時,需要傳入一個參考視圖用於定義座標系。 -
UIGravityBehavior
把重力的行爲抽象成模型,並且對一個或多個元素施加作用力,讓你可以建立物理交互模型。當你創建一個行爲實例的時候,你需要把它關聯到一組元素上,一般是一組視圖。這樣你就能選擇受該行爲影響的元素,在這個例子中就是指受重力影響的元素。
大部分行爲有一些可配置屬性。比如重力行爲允許你改變它的角度和量級。嘗試修改這些屬性使你的物體向上、側向或斜向以不同的加速度移動。
注意:關於單位:在物理學中,重力(g)單位是米每平方秒,約爲 9.8 m/s2。根據牛頓第二定律,你可以用下面公式計算在重力作用下,物體移動的距離:
距離 = 0.5 × g × 時間^2
在 UIKit 力學中,公式依然適用,但單位有所不同。單位中的米每平方秒要用像素每平方秒代替。基於重力參數,應用牛頓第二定律你任然可以計算出視圖在任意時間的位置。
你真的需要了解這些麼?未必,你只需要知道更大的 g 意味着掉落得更快,但是瞭解背後的數學原理有利無弊。
設置邊界
雖然你看不到,但是當方塊消失在屏幕邊緣的時候,它其實還在繼續下落。爲了使它保持在屏幕範圍之內,你需要定義一個邊界。
在 ViewController.m 裏添加另一個實例變量:
- UICollisionBehavior* _collision;
在 viewDidLoad
末尾加入以下代碼:
- _collision = [[UICollisionBehavior alloc]
- initWithItems:@[square]];
- _collision.translatesReferenceBoundsIntoBoundary = YES;
- [_animator addBehavior:_collision];
上面的代碼創建了一個碰撞行爲,定義了一個或多個邊界,以決定相關元素之間如何互相影響。
上面的代碼沒有顯式地添加邊界座標,而是設置 translatesReferenceBoundsIntoBoundary
屬性爲
YES。這意味着用提供給 UIDynamicAnimator
的視圖的 bounds 作爲邊界。
編譯並運行,你會看到方塊在碰到屏幕底部之後,輕輕彈起,並最終靜止,如下圖所示:
這是一個很讚的行爲,特別是看到如此少的代碼量。
處理碰撞
接下來你要添加一個固定的障礙物,他會跟下落的方塊碰撞並相互影響。在 viewDidLoad
中添加方塊的代碼之後加入以下代碼:
- UIView* barrier = [[UIView alloc] initWithFrame:CGRectMake(0, 300, 130, 20)];
- barrier.backgroundColor = [UIColor redColor];
- [self.view addSubview:barrier];
編譯並運行應用,你可以看到一個紅色的“障礙物”橫跨在屏幕中間。但是你會發現他沒有起到任何作用,方塊直接穿過了障礙物:
這顯然不是你想要的,這也說明了很重要的一點:力學隻影響關聯到行爲上的視圖。
下面是一個簡單的示意圖:
關聯 UIDynamicAnimator
到提供座標系的參考視圖,然後添加一個或多個行爲來對關聯的物體施加作用力。大部分行爲可以與多個物體關聯,每個物體可以與多個行爲關聯。上圖展示了當前應用中的行爲以及他們的關係。
當前代碼裏的行爲都沒有涉及到障礙物,因此在力學引擎中,這個障礙物並不存在。
使物體響應碰撞
爲了讓方塊與障礙物碰撞,找到初始化碰撞行爲的代碼並用下面的代碼替代:
- _collision = [[UICollisionBehavior alloc] initWithItems:@[square, barrier]];
碰撞實例需要知道它所影響的每一個視圖,因此添加障礙物到列表中使得碰撞對障礙物也有作用。
編譯並運行應用,兩個物體碰撞並相互影響,如下圖所示:
碰撞行爲在每個關聯的物體周圍形成一個邊界,使得這些物體從可以互相穿越的物體變成實體無法穿越。
更新一下前面的示意圖,現在碰撞行爲與兩個視圖都關聯起來了:
但是現在還有一些有出入的地方。我們希望障礙物是不可移動的,但是當前設置下,當兩個物體碰撞的時候,障礙物被撞開並且旋轉着落向屏幕底部。
更奇怪的是,障礙物從底部彈起後似乎沒有趨於靜止的意思。這是因爲重力沒有對障礙物產生影響,這也解釋了爲什麼在方塊撞到障礙物之前它沒有移動。
你需要另一種解決問題的思路。既然障礙物是不可移動的,那麼力學引擎就沒有必要知道它的存在。但是如何檢測碰撞呢?
不可見的邊界和碰撞
把碰撞行爲的初始化代碼改回原先的樣子,使他只知道方塊的存在:
- _collision = [[UICollisionBehavior alloc] initWithItems:@[square]];
然後,添加如下邊界:
- // add a boundary that coincides with the top edge
- CGPoint rightEdge = CGPointMake(barrier.frame.origin.x +
- barrier.frame.size.width, barrier.frame.origin.y);
- [_collision addBoundaryWithIdentifier:@"barrier"
- fromPoint:barrier.frame.origin
- toPoint:rightEdge];
上述代碼添加了一個不可見的邊界,它正是障礙物的上邊界。紅色障礙物對用戶依然是可見的,但是力學引擎不知道它的存在;相反,添加的邊界對於力學引擎是可見的,對於用戶是不可見的。當方塊下落的時候,看起來與障礙物發生了碰撞,其實它碰到了不可移動的邊界。
編譯並運行應用,你看到的效果如下圖所示:
方塊現在從障礙物的邊界彈起,旋轉,然後繼續落到屏幕底部直到靜止。
到現在爲止,UIKit 力學的強大之處可見一斑:你只需要幾行簡單的代碼就可以實現相當複雜的效果。在這背後有許多複雜的邏輯,在下個章節會涉及力學引擎與應用中物體交互的具體方式。
碰撞的背後
每一個力學行爲都有一個 action 屬性,你可以定義一個 block 使其在動畫的每一步被執行。添加下面的代碼到viewDidLoad
:
- _collision.action = ^{
- NSLog(@"%@, %@",
- NSStringFromCGAffineTransform(square.transform),
- NSStringFromCGPoint(square.center));
- };
上面的代碼記錄了下落的方塊的中點位置核 transform 屬性。編譯並運行應用,你可以在 Xcode 的控制檯中看到調試信息。
在前 400 毫秒左右你會看到類似這樣的信息:
- 2013-07-26 08:21:58.698 DynamicsPlayground[17719:a0b] [1, 0, 0, 1, 0, 0], {150, 236}
- 2013-07-26 08:21:58.715 DynamicsPlayground[17719:a0b] [1, 0, 0, 1, 0, 0], {150, 243}
- 2013-07-26 08:21:58.732 DynamicsPlayground[17719:a0b] [1, 0, 0, 1, 0, 0], {150, 250}
可以看到力學引擎在動畫過程中不斷改變方塊的中點位置,或者說它的 frame。
當方塊撞到障礙物的時候,它開始旋轉,這時候的調試信息類似這樣:
- 2013-07-26 08:21:59.182 DynamicsPlayground[17719:a0b] [0.10679234, 0.99428135, -0.99428135, 0.10679234, 0, 0], {198, 325}
- 2013-07-26 08:21:59.198 DynamicsPlayground[17719:a0b] [0.051373702, 0.99867952, -0.99867952, 0.051373702, 0, 0], {199, 331}
- 2013-07-26 08:21:59.215 DynamicsPlayground[17719:a0b] [-0.0040036771, 0.99999201, -0.99999201, -0.0040036771, 0, 0], {201, 338}
你可以看到力學引擎根據物理模型計算並同時改變 transform 屬性和 frame 屬性來定位視圖。
雖然這些屬性的具體取值沒什麼意思,但是很重要的一點是他們每時每刻都在改變。因此如果你嘗試用代碼改變物體的 frame 或者 transform 屬性,這些值會被覆蓋。這意味着當你的物體受力學引擎控制的時候,你不能通過 transform 來縮放你的物體。
力學行爲的方法名裏用的是 items 而不是 views,這是因爲想要使用力學行爲的對象只需實現 UIDynamicItem
協議即可,定義如下:
- @protocol UIDynamicItem
- @property (nonatomic, readwrite) CGPoint center;
- @property (nonatomic, readonly) CGRect bounds;
- @property (nonatomic, readwrite) CGAffineTransform transform;
- @end
UIDynamicItem
協議爲力學引擎提供了讀寫 center 和 transform
屬性的權限,使它可以根據內部的計算結果移動物體。同時提供了 bounds 的讀權限,用以確定物體的大小,這不但在計算物體邊界的時候被用到,同時在物體受力時用於計算物體的質量。
這個協議說明力學引擎與 UIView
並不耦合,其實 UIKit 中還有一個類也實現了這個協議
–UICollectionViewLayoutAttributes
。所以可以通過力學引擎對
collection views 實現動畫效果。
碰撞通知
到現在,你添加了一些視圖和行爲,然後讓力學引擎接手剩下的任務。在接下來的內容中你會看到如何接收物體碰撞時的通知。
打開 ViewController.m 並且實現 UICollisionBehaviorDelegate
協議:
- @interface ViewController ()
- @end
還是在 viewDidLoad
中,在初始化碰撞行爲後,設置當前 view controller
爲其代理(delegate),代碼如下:
- _collision.collisionDelegate = self;
然後,添加一個碰撞行爲的代理方法:
- - (void)collisionBehavior:(UICollisionBehavior *)behavior beganContactForItem:(id)item
- withBoundaryIdentifier:(id)identifier atPoint:(CGPoint)p {
- NSLog(@"Boundary contact occurred - %@", identifier);
- }
當碰撞發生的時候,這個代理方法會被調用並且在控制檯打印出調試信息。爲了避免控制檯的信息太亂,你可以刪除之前在 _collision.action
裏添加的調試信息。
編譯運行,物體相互碰撞,你會在控制檯看到如下信息:
- 2013-07-26 08:44:37.473 DynamicsPlayground[18104:a0b] Boundary contact occurred - barrier
- 2013-07-26 08:44:37.689 DynamicsPlayground[18104:a0b] Boundary contact occurred - barrier
- 2013-07-26 08:44:38.256 DynamicsPlayground[18104:a0b] Boundary contact occurred - (null)
- 2013-07-26 08:44:38.372 DynamicsPlayground[18104:a0b] Boundary contact occurred - (null)
- 2013-07-26 08:44:38.455 DynamicsPlayground[18104:a0b] Boundary contact occurred - (null)
- 2013-07-26 08:44:38.489 DynamicsPlayground[18104:a0b] Boundary contact occurred - (null)
- 2013-07-26 08:44:38.540 DynamicsPlayground[18104:a0b] Boundary contact occurred - (null)
從調試信息中可以看到方塊碰了兩次障礙物,也就是之前添加的不可見的邊界。(null) 則是指參考視圖的邊界。
這些調試信息讀起來很有意思(認真的),但是如果能在碰撞時觸發一些視覺反饋,那就更有意思了。
在輸出調試信息的代碼之後添加如下代碼:
- UIView* view = (UIView*)item;
- view.backgroundColor = [UIColor yellowColor];
- [UIView animateWithDuration:0.3 animations:^{
- view.backgroundColor = [UIColor grayColor];
- }];
上述代碼會改變碰撞的物體的背景色爲黃色,然後漸變回灰色。
編譯運行,看一下實際效果:
每次方塊與邊界發生碰撞的時候,它都會閃現黃色。
UIKit 力學會根據物體的 bounds 計算並自動設置它們的物理屬性(如質量或彈性係數)。接下來你會看到如何使用 UIDynamicItemBehavior
類控制這些物理屬性。
設置物體屬性
在 viewDidLoad
的末尾,添加下面的代碼:
- UIDynamicItemBehavior* itemBehaviour = [[UIDynamicItemBehavior alloc] initWithItems:@[square]];
- itemBehaviour.elasticity = 0.6;
- [_animator addBehavior:itemBehaviour];
上面的代碼創建了一個物體行爲(item behavior),把它關聯到方塊,然後添加該行爲到動畫實例(animator)。彈性係數屬性(elasticity)控制物體的彈性,取 1.0 表示完全彈性碰撞,也就是說碰撞中沒有能量或速度的損失。你剛剛設置了方塊的彈性係數爲 0.6,意味着方塊在每次彈起的時候速度都會減慢。
編譯運行應用,你會發現現在的方塊比之前更有彈性,如下:
注: 如果你想知道我是如何生成如上圖片來展現方塊的歷史位置,其實相當簡單!我給行爲的 action 屬性添加了一個簡單的 block,每執行五次,用當前方塊的中點位置和 transform 屬性,添加一個新的方塊到當前視圖。
在上面的代碼中,你只改變了物體的彈性係數,然後物體的行爲類還有很多其他可以調整的屬性。有下列屬性:
- elasticity(彈性係數) – 決定了碰撞的彈性程度,比如碰撞時物體的彈性。
- friction(摩擦係數) – 決定了沿接觸面滑動時的摩擦力大小。
- density(密度) – 跟 size 結合使用,來計算物體的總質量。質量越大,物體加速或減速就越困難。
- resistance(阻力) – 決定線性移動的阻力大小,這根摩擦係數不同,摩擦係數只作用於滑動運動。
- angularResistance(轉動阻力) – 決定轉動運動的阻力大小。
- allowsRotation(允許旋轉) – 這個屬性很有意思,它在真實的物理世界沒有對應的模型。設置這個屬性爲 NO 物體就完全不會轉動,無力受到多大的轉動力。
動態添加行爲
現在的情況下,你的應用設置系統的所有行爲,然後由力學引擎處理系統的物理行爲,直至所有物體靜止。在下一步中,你會看到如何動態添加或刪除行爲。
打開 ViewController.m 並添加如下實例變量:
- BOOL _firstContact;
添加下面的代碼到碰撞代理方法collisionBehavior:beganContactForItem:withBoundaryIdentifier:atPoint:
的末尾:
- if (!_firstContact)
- {
- _firstContact = YES;
- UIView* square = [[UIView alloc] initWithFrame:CGRectMake(30, 0, 100, 100)];
- square.backgroundColor = [UIColor grayColor];
- [self.view addSubview:square];
- [_collision addItem:square];
- [_gravity addItem:square];
- UIAttachmentBehavior* attach = [[UIAttachmentBehavior alloc] initWithItem:view
- attachedToItem:square];
- [_animator addBehavior:attach];
- }
上面的代碼檢測到方塊和障礙物的第一次接觸時,創建第二個方塊並添加到碰撞和重力行爲中。此外,設置了一個吸附行爲,實現兩個物體之間加入虛擬的彈簧的效果。
編譯運行應用,當原有的方塊撞到障礙物時,你應該會看到一個新的方塊出現,如下:
雖然兩個方塊看起來被連接到一起,但是因爲沒有在屏幕上畫線條或是彈簧,你並不會看到視覺上的聯繫。
接下來做什麼?
現在你應該比較瞭解 UIKit 力學的核心概念了。
如果你有興趣學習更多關於 UIKit 力學的內容,可以關注我們的新書 iOS 7 教程集。書中結合你在本文學到的內容,有更深入的內容,展示瞭如何在現實場景中利用 UIKit 力學:
用戶可以上拉一個食譜來預覽它,當用戶鬆開食譜的時候,它會落回菜單中,或是停靠在屏幕頂部。最終的成品是一個有真實物理體驗的應用。
我希望你喜歡這個 UIKit 力學教程 – 我們覺得這很酷,並且期待看到你在應用中付諸有創意的交互。如果你有任何問題或評論,請加入下面的論壇討論!
你在這個教程中創建的 Dynamics Playground 工程的完整代碼可以在 github 上找到,文中每一步編譯運行都對應一次提交。