天天看點

UIKit 力學教程

建議點按下面的标題,跳至原文,以下轉載的頁面布局太垃圾!

<a target="_blank" href="http://www.raywenderlich.com/zh-hans/52617/uikit-%E5%8A%9B%E5%AD%A6%E6%95%99%E7%A8%8B">UIKit 力學教程</a>

你可能已經注意到 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 () 

還是在 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 力學教程 – 我們覺得這很酷,并且期待看到你在應用中付諸有創意的互動。如果你有任何問題或評論,請加入下面的論壇讨論!

<a target="_blank" href="http://twitter.com/share?url=http%3A%2F%2Fbit.ly%2FGOc6q2&amp;via=rwenderlich&amp;text=UIKit%20%E5%8A%9B%E5%AD%A6%E6%95%99%E7%A8%8B&amp;related=rwenderlich&amp;lang=en&amp;count=horizontal&amp;counturl=http%3A%2F%2Fwww.raywenderlich.com%2Fzh-hans%2F52617%2Fuikit-%25e5%258a%259b%25e5%25ad%25a6%25e6%2595%2599%25e7%25a8%258b">Tweet</a>

<a target="_blank" href="http://www.raywenderlich.com/u/ColinEberhardt">Colin Eberhardt</a>

<a target="_blank" href="http://www.twitter.com/@ColinEberhardt">Follow Colin Eberhardt on Twitter</a>

繼續閱讀