UIKit Dynamics:開始入門 —《Graphics & Animation系列一》

翻譯自raywenderlich網站iOS教程Graphics & Animation系列
介紹
UIKit Dynamics是一個集成到UIKit中的完整物理引擎。它容許您經過添加諸如重力,附件(彈簧)和力量等行爲來建立感受真實的界面。您定義了您但願界面元素採用的物理特徵,動態引擎負責其他部分。git

Motion Effects能夠建立很酷視差效果,就像在傾斜iOS 7主屏幕時看到的同樣。基本上,咱們能夠利用手機加速計提供的數據來建立對手機方向變化做出反應的接口。github

當一塊兒使用時,運動和動態成爲用戶體驗工具的重要組成部分,使您的交互栩栩如生。用戶將經過看到它以天然,動態的方式迴應他們的行爲。編程

準備開始swift

ViewController.swift 添加以下代碼在viewDidLoad:框架

let square = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
    square.backgroundColor = UIColor.grayColor
    view.addSubview(square)

運行以後效果以下:
clipboard.pngide

增長重力效果
仍然在 ViewController.swift中,在viewDidLoad上方添加如下屬性:工具

var animtor: UIDynamicAnimator!
var gravity: UIGravityBehavior!

這些屬性是隱式解包的optionals(如類型名稱後面的!所示)。 這些屬性必須是可選的,由於咱們沒有在init方法中初始化它們。 此時可使用隱式解包的optionals,由於咱們知道這些屬性在初始化後不會爲零。 能夠防止每次使用的時候須要!來解包。動畫

添加如下代碼在viewDidLoad的結尾處:網站

animtor = UIDynamicAnimator(referenceView: view)
  gravity = UIGravityBehavior(items: [square])
  animtor.addBehavior(gravity)

如今構建並運行應用程序。 能夠看到你的方塊慢慢地開始加速,直到它落在屏幕的底部。
在剛剛添加的代碼中,這裏有幾個動態類:ui

  • UIDynamicAnimator是UIKit物理引擎。這個類跟蹤你添加到引擎的各類行爲,好比引力,並提供總體上下文。當建立animator的實例時,將傳入animator用於定義其座標系的參考視圖。
  • UIGravityBehavior模擬重力的行爲並對一個或多個項目施加做用力,能夠建模物理交互。當建立一個行爲的實例時,將它與一組項目相關聯 - 一般是視圖。 經過這種方式,能夠選擇哪些項目受到行爲的影響,在這種狀況下哪些項目會受到重力的影響。

大多數行爲都有一些配置屬性;例如,重力行爲能夠改變它的角度和大小。嘗試修改這些屬性以使對象以不一樣的加速度向上,側向或對角線傾斜。

注:關於單位的簡單說法:在物理世界中,重力(g)以米每平方秒錶示,大約等於9.8米/秒2。 
使用牛頓第二定律,你能夠用下面的公式計算物體在重力影響下的落差:
distance = 0.5 × g × time2
在UIKit Dynamics中,公式相同,但單位不一樣。使用每秒數千像素單位的單位 ,而不是米。 
使用牛頓第二定律,仍然能夠根據提供的重力組件隨時計算出視角。

固然咱們並不須要知道這些細節,只須要知道g值越大意味着物體降低的越快。

設置邊界

爲了保持方塊在屏幕的邊界內,須要定義一個邊界。
添加另外一個屬性在 ViewController.swift

var collision: UICollisionBehavior!

viewDidLoad:下面添加這幾行:

collision = UICollisionBehavior(items: [square])
    collision.translatesReferenceBoundsIntoBoundary = true
    animtor.addBehavior(collision)

上面的代碼建立了一個碰撞行爲,它定義了一個或多個關聯項與之交互的邊界。

上述代碼不是明確添加邊界座標,而是將translatesReferenceBoundsIntoBoundary屬性設置爲true。 這會致使邊界使用提供給UIDynamicAnimator的參考視圖的邊界。

運行時能夠看到正方形與屏幕底部碰撞,稍微反彈,而後中止,以下所示:

clipboard.png

以上咱們用不多的代碼實現了一個很酷的效果

處理碰撞

接下來,添加一個不可移動的障礙,降低的方塊將碰撞和互動。
將如下代碼插入viewDidLoad中添加square的代碼下面:

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

構建並運行你的應用程序; 你會在屏幕上看到一個紅色的「障礙」。 然而,事實證實,這個障礙並非那麼有效:

clipboard.png

這不是咱們想要的效果,但它確實提供了一個重要的提示:動態只會影響與行爲相關的視圖:

clipboard.png

UIDynamicAnimator與提供座標系的參考視圖相關聯。 而後添加一個或多個行爲,這些行爲會對與其相關聯的項目施加做用力。 大多數行爲能夠與多個項目相關聯,而且每一個項目能夠與多個行爲相關聯。 上圖顯示了應用中的當前行爲及其關聯。

當前代碼中的任何行爲都不能「意識到」屏障,因此就下層動態引擎而言,屏障甚至不存在。

讓對象響應碰撞

爲了使正方形與障礙碰撞,找到初始化碰撞行爲的代碼並將其替換爲如下內容:

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

碰撞對象須要知道它應該與之交互的每一個視圖; 所以將障礙添加到物品列表中容許碰撞物體也做用於障礙物。

構建並運行應用程序; 這兩個對象相互碰撞並相互做用,以下圖所示:

clipboard.png

碰撞行爲在與其相關的每一個項目周圍造成「邊界」; 這將它們從能夠經過彼此的對象變成更堅實的對象。

更新前面的圖,能夠看到碰撞行爲如今與兩個視圖相關聯:

clipboard.png

可是,這兩個對象之間的交互仍然存在不太正確的地方。 屏障被認爲是不可移動的,可是當兩個物體在當前配置中碰撞時,屏障會被打破位置並開始向屏幕底部旋轉。

更奇怪的是,屏障從屏幕底部反彈而且不像平方那樣安定下來 - 這頗有意義,由於重力行爲不會與屏障相互做用。 這也解釋了爲何屏障不會移動,直到正方形與它碰撞。

如今須要一個不一樣的方法來解決問題。 因爲障礙視圖是不可移動的,因此動力學引擎不須要知道它的存在。 可是如何檢測到碰撞?

看不見的邊界和碰撞

將碰撞行爲初始化更改回其原始形式,以便僅識別方塊:

collision = UICollisionBehavior(items: [square])

緊隨此行後,添加如下內容:

collision.addBoundary(withIdentifier: "barrier" as NSCopying, for: UIBezierPath(rect: barrier.frame))

上面的代碼添加了一個與屏障視圖具備相同框架的不可見邊界。 紅色屏障對用戶而言仍然可見,但對動態引擎不可見,而邊界對動態引擎可見但對用戶不可見。 當方塊落下時,它彷佛與屏障相互做用,但它實際上碰撞了不動的邊界。

構建並運行,以下所示:

clipboard.png

方塊如今從邊界反彈,旋轉一點,而後繼續往屏幕底部前進的地方休息。

到目前爲止,UIKit Dynamics的功能已經變得至關清晰:只需幾行代碼就能夠完成不少工做。 引擎蓋下有不少事情要作, 下一節將向展現動態引擎如何與應用程序中的對象交互的一些細節。

碰撞的細節

每一個動態行爲都有一個動做屬性。 將如下代碼添加到viewDidLoad中:

collision.action = {
        print("\(NSStringFromCGAffineTransform(self.square.transform)) \(NSStringFromCGPoint(self.square.center))")
    }

上面的代碼記錄降低方塊的中心和變換屬性。運行應用程序,將在Xcode控制檯窗口中看到這些日誌消息。

對於1〜400毫秒,日誌消息:

[1, 0, 0, 1, 0, 0], {150, 236}
[1, 0, 0, 1, 0, 0], {150, 243}
[1, 0, 0, 1, 0, 0], {150, 250}

在這裏能夠看到動態引擎正在改變每一個動畫步驟中的方塊的中心 - 也就是它的幀。

只要方塊碰到屏障,它就開始旋轉,這會產生以下的日誌消息:

[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}

在這裏能夠看到,動態引擎正在使用變換和幀偏移的組合來根據底層物理模型定位視圖。

雖然動態適用於這些屬性的確切值可能沒有多大意義,但知道它們正在被應用很重要。 所以,若是以編程方式更改對象的框架或轉換屬性,則能夠預期這些值將被覆蓋。 這意味着當它處於動態的控制之下時,不能使用變換來縮放對象。

將dynamic behaviors應用於對象的惟一要求是它採用UIDynamicItem協議,以下所示:

protocol UIDynamicItem : NSObjectProtocol {
  var center: CGPoint { get set }
  var bounds: CGRect { get }
  var transform: CGAffineTransform { get set }
}

UIDynamicItem協議提供動態讀寫訪問中心和變換屬性,容許它根據其內部計算移動項目。 它還具備對邊界的讀取權限,用於肯定項目的大小。 這容許它在物品的周邊周圍產生碰撞邊界,而且在施加力時計算物品的質量。

這個協議意味着動態與UIView不緊密耦合; 的確有另外一個UIKit類不是視圖,但仍然採用這個協議:UICollectionViewLayoutAttributes。

碰撞通知

到目前爲止,已經添加了一些視圖和行爲,而後讓動態接管。 在下一步中,將瞭解如何在物品碰撞時接收通知。

仍然在ViewController.swift中,經過更新類聲明來採用UICollisionBehaviorDelegate協議:

class ViewController: UIViewController, UICollisionBehaviorDelegate {

viewDidLoad中,在初始化碰撞對象以後將視圖控制器設置爲委託,以下所示:

collision.collisionDelegate = self
func collisionBehavior(_ behavior: UICollisionBehavior, beganContactFor item: UIDynamicItem, withBoundaryIdentifier identifier: NSCopying?, at p: CGPoint) {
    print("Boundary contact occurred - \(String(describing: identifier))")
}

這種委託方法在發生衝突時被調用。 它將打印出一條日誌消息給控制檯。 爲了不使用大量消息弄亂控制檯日誌,請刪除在上一節中添加的collision.action日誌記錄。

運行將在控制檯中看到如下條目:

Boundary contact occurred - Optional(barrier)
Boundary contact occurred - Optional(barrier)

從上面的日誌消息中能夠看到方塊與「標識符」(barrierView)屏障相撞兩次;

在print下面添加如下內容:

let collidingView = item as! UIView
    UIView.animate(withDuration: 1) {
        collidingView.backgroundColor = .gray
    }

上面的代碼將碰撞項目的背景顏色淡化爲灰色。

構建並運行以查看這種效果:

clipboard.png
到目前爲止,UIKit Dynamics已經根據物品的界限自動設置物品的物理屬性(如質量和彈性)。 接下來,將看到如何使用UIDynamicItemBehavior類本身控制這些物理屬性。

配置item屬性

viewDidLoad中,將如下內容添加到方法的末尾:

let itemBehaviour = UIDynamicItemBehavior(items: [square])
    itemBehaviour.elasticity = 0.6
    animtor.addBehavior(itemBehaviour)

上面的代碼建立一個項目行爲,將其與方塊關聯,而後將行爲對象添加到動畫設計器中。 彈性屬性控制着物品的彈性; 值爲1.0表示徹底彈性碰撞; 也就是說,在碰撞中沒有能量或速度丟失的地方。 咱們將方塊的彈性設置爲0.6,這意味着每次反彈時平方將失去速度。

構建並運行你的應用程序,你會注意到這個廣場如今表現得更加酷,以下所示:

clipboard.png

注:製做上面的圖片並顯示方塊路徑輪廓的代碼以下:
func addPosition()  {
    var updateCount = 0
    collision.action = {
        if updateCount % 3 == 0 {
            let outLine = UIView(frame: self.square.bounds)
            outLine.transform = self.square.transform
            outLine.center = self.square.center
            
            outLine.alpha = 0.5
            outLine.backgroundColor = .clear
            outLine.layer.borderColor = self.square.layer.presentation()?.backgroundColor
            outLine.layer.borderWidth = 1.0
            self.view.addSubview(outLine)
        }
        updateCount += 1
    }
}

在上面的代碼中,只改變了物品的彈性; 然而,該項目的行爲類有許多其餘屬性能夠在代碼中操做。 以下:

elasticity - 決定「彈性」碰撞的方式,即物體在碰撞中的彈性或「橡膠狀」程度。
     friction - 決定沿表面滑動時的運動阻力。
     density - 當與大小相結合時,這將給出物品的總體質量。質量越大,加速或減速物體越難。
     resistance - 決定抵抗任何線性移動的數量。這與僅適用於滑動運動的摩擦造成對比。
     angularResistance - 肯定抵抗任何旋轉運動的量。
     allowsRotation - 若是將此屬性設置爲NO,則無論發生的旋轉力如何,對象都不會旋轉。

動態添加行爲

在下一步中,將看到如何動態地添加和刪除行爲。

打開ViewController.swift並在viewDidLoad上方添加如下屬性:

var firstContact = false

將如下代碼添加到碰撞代理方法的末尾func collisionBehavior(_ behavior: UICollisionBehavior, beganContactFor item: UIDynamicItem, withBoundaryIdentifier identifier: NSCopying?, at p: CGPoint)

if !firstContact {
        firstContact = true

        let square = UIView(frame: CGRect(x: 30, y: 0, width: 100, height: 100))
        square.backgroundColor = .gray
        view.addSubview(square)
        collision.addItem(square)
        gravity.addItem(square)
        let attach = UIAttachmentBehavior(item: collidingView, attachedTo: square)
        animtor.addBehavior(attach)
    }

上面的代碼檢測屏障和正方形之間的初始接觸,建立第二個正方形並將其添加到碰撞和重力行爲中。 另外,還能夠設置一個附件行爲,以建立用虛擬彈簧附加一對對象的效果。

構建並運行; 當原始方塊碰到屏障時,應該會看到一個新的方塊,以下所示:

clipboard.png

用戶交互

正如剛剛看到的,當物理系統已經運動時,咱們能夠動態添加和刪除行爲。 在最後一節中,每當用戶點擊屏幕時,都會添加另外一種類型的動態行爲UISnapBehavior。 一個UISnapBehavior使一個對象跳躍到一個有彈性的彈簧式動畫的指定位置。

刪除上一節添加的代碼:collisionBehavior()中的firstContact屬性和if語句。 在屏幕上只能看到一個方塊的UISnapBehavior效果會更容易。

viewDidLoad上添加兩個屬性:

var square: UIView!
var snap: UISnapBehavior!

這將跟蹤方塊視圖,以便您能夠從視圖控制器的其餘位置訪問它。 您將在下一個使用捕捉對象。

viewDidLoad中,從square聲明中刪除let關鍵字,以便它使用新屬性而不是局部變量:

square = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))

最後,爲touchesEnded添加一個實現,以在用戶觸摸屏幕時建立並添加新的捕捉行爲:

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    if snp != nil {
        animtor.removeBehavior(snp)
    }
    let touchs = touches as NSSet
    
    let touch = touchs.anyObject() as! UITouch
    snp = UISnapBehavior(item: square, snapTo: touch.location(in: view))
    animtor.addBehavior(snp)
}

這段代碼很是簡單。 首先,它檢查是否存在現有的快照行爲並將其刪除。 而後建立一個新的捕捉行爲,將方塊對齊到用戶的觸摸位置,並將其添加到動畫製做工具中。

構建並運行應用程序。 嘗試點擊; 方塊會跑到觸摸的地方

這裏是最終demo,此demo是raywenderlich下面iOS的Graphics & Animation整個教程系列的集合。

相關文章
相關標籤/搜索