玩轉(zhuǎn)iOS9的UIDynamics
UIDynamics在iOS7 SDK中是一個受歡迎的新加特性, 它基本上是一個支持UIView的物理引擎,可以讓我們自定義UI 控件的物理特性。這個API淺顯易懂,你可以輕松地創(chuàng)建很棒的動畫或者過渡效果。之前我在 這篇文章 中已經(jīng)涵蓋了其基本要點(diǎn), 而這一次,我們將看看iOS 9里的UIDynamics有什么新玩意。
碰撞邊界(Collision Bounds)
UIDynamics 的***個版本帶有碰撞系統(tǒng)(在 UICollisionBehavior 中)只支持矩形。這可以理解,因?yàn)閁IViews都是矩形架構(gòu),但是圓形的卻不常見,更不用說優(yōu)化一個自定義的貝塞爾曲線。在iOS 9中,UIDynamicItem協(xié)議里加了一個新屬性:UIDynamicItemCollisionBoundsType,支持以下枚舉類型:
- Rectangle
- Ellipse
- Path
這個屬性是只讀的,如果我們想修改它的話,需要提供我們的子類:
- class Ellipse: UIView {
- override var collisionBoundsType: UIDynamicItemCollisionBoundsType {
- return .Ellipse
- }
- }
這是默認(rèn)碰撞邊界的UIView。
而這是同一個帶.Ellipse屬性的UIView。
這涵蓋了圓形的視圖,如果我們突發(fā)奇想,畫一個更復(fù)雜,有連續(xù)的剛體,我們就可以使用.path枚舉類型啦,并且也要重寫該屬性:
- var collisionBoundingPath: UIBezierPath { get }
這個路線可以是任意你能所想到的,只要它的樣子是凸面的(即任意在多邊形內(nèi)兩個的點(diǎn),兩點(diǎn)間的線段完全包含在多邊形內(nèi)),并且是逆時針繞的。凸面這個條件也許限制的太死了,于是引入了UIDynamicItemGroup,它可以詳細(xì)描繪一組不同圖形的的組合圖形。這樣,只要該組合中的每個圖形都是凸面,即使得到的多邊形是凹面的也OK。
Field Behavior
Field Behavior是在整個場景中運(yùn)用的一種新behavior。一個最普通的例子就是我們一直默默地使用著的UIGravityBehavior,即場景中的每個物體都會受到一個向下的重力?,F(xiàn)在我們可以使用一組新的場力,就像徑向(距離場景中心越近,力越大)、噪聲(在場景內(nèi)隨機(jī)產(chǎn)生的不同的力)等等。
Dynamic Item Behavior(動力元素行為)
UIDynamicItemBehavior 包含了幾個有趣的新特性:
- var charge: CGFloat
- var anchored: Bool
charge 代表能夠影響一個元素在電磁場上如何移動的電荷(是的,聽起來很瘋狂),而anchored本質(zhì)上是將圖形變成了碰撞中的一個靜態(tài)物體,但沒有響應(yīng)事件(如果有什么東西撞上了它,它會絲毫不動),所以可以***地用來表示地板或墻壁。
Attachment Behavior(吸附行為)
UIAttachmentBehavior改進(jìn)后,現(xiàn)在像個具有新方法和屬性的偵探,如同frictionTorque和attachmentRange一樣?,F(xiàn)在吸附行為變得更加靈活,我們可以指定相對滑動的動作、固定吸附、繩索鏈接和我最喜歡的:針型吸附。想像一下兩個釘在一起的物體你就明白了。這些基本涵蓋了UIDynamics的新特性,現(xiàn)在,是時候丟下這個更新日志,并開始搭建一些很二的東西了。
讓我們玩球吧
我上周花了很多時間在球王(Ball King)上。這一個很棒的消磨時間的東西,這游戲的的理念很簡單,但是表現(xiàn)很出色。并且,它采用了獲得蘋果設(shè)計獎的Crossy Roadde 相同的理念:它不會以任何方式影響到玩家,比如游戲內(nèi)的榮譽(yù)。
我非常喜歡它的一點(diǎn)是球的物理模型,以及當(dāng)球打到籃板上時籃板的反應(yīng)。看起來用它來測試上面提到的UIDynamics新特性應(yīng)該會非常不錯。讓我們來看看如何一步步地打造屬于自己的簡單的版本吧:BallSwift
籃框
籃球架可以用一個UIView作為籃板,幾個UIView作為籃框的左右兩邊,最前面的view作為籃框本身(不帶物理剛體)。使用我們之前定義的類Ellipse,我們就可以創(chuàng)造我們的游戲場景的視覺表現(xiàn):
- /*
- Build the hoop, setup the world appearance
- */
- func buildViews() {
- board = UIView(frame: CGRect(x: hoopPosition.x, y: hoopPosition.y, width: 100, height: 100))
- board.backgroundColor = .whiteColor()
- board.layer.borderColor = UIColor(red: 0.98, green: 0.98, blue: 0.98, alpha: 1).CGColor
- board.layer.borderWidth = 2
- board.addSubview({
- let v = UIView(frame: CGRect(x: 30, y: 43, width: 40, height: 40))
- v.backgroundColor = .clearColor()
- v.layer.borderColor = UIColor(red: 0.4, green: 0.4, blue: 0.4, alpha: 1).CGColor
- v.layer.borderWidth = 5
- return v
- }())
- leftHoop = Ellipse(frame: CGRect(x: hoopPosition.x + 20, y: hoopPosition.y + 80, width: 10, height: 6))
- leftHoop.backgroundColor = .clearColor()
- leftHoop.layer.cornerRadius = 3
- rightHoop = Ellipse(frame: CGRect(x: hoopPosition.x + 70, y: hoopPosition.y + 80, width: 10, height: 6))
- rightHoop.backgroundColor = .clearColor()
- rightHoop.layer.cornerRadius = 3
- hoop = UIView(frame: CGRect(x: hoopPosition.x + 20, y: hoopPosition.y + 80, width: 60, height: 6))
- hoop.backgroundColor = UIColor(red: 177.0/255.0, green: 25.0/255.0, blue: 25.0/255.0, alpha: 1)
- hoop.layer.cornerRadius = 3
- [board, leftHoop, rightHoop, floor, ball, hoop].map({self.view.addSubview($0)})
- }
這里其實(shí)沒有什么新東西, 籃框以編程形式被創(chuàng)建在常量CGPoint hoopPosition上。但是視圖的順序很重要,因?yàn)槲覀兿胱尰@框要高于籃球(的拋投點(diǎn))。
#p#
螺母和螺栓(Nuts and bolts)
籃框的最重要的部分是左右臂,它們需要一個物理圓形身體(使得與球的碰撞顯得自然),需要用螺栓固定在板和前框。這兩個將成為基本的UIDynamicItems,并不會直接碰撞參與碰撞。新推出的針型吸附就是為此而生的,它可以把一切都***地結(jié)合在一起,因?yàn)槲覀兛梢栽谶@個比較粗糙的圖畫上看到:
在給定的確定空間點(diǎn)內(nèi),pin一次僅可連接幾個視圖:
- let bolts = [
- CGPoint(x: hoopPosition.x + 25, y: hoopPosition.y + 85), // leftHoop -> Board
- CGPoint(x: hoopPosition.x + 75, y: hoopPosition.y + 85), // rightHoop -> Board
- CGPoint(x: hoopPosition.x + 25, y: hoopPosition.y + 85), // hoop -> Board (L)
- CGPoint(x: hoopPosition.x + 75, y: hoopPosition.y + 85)] // hoop -> Board (R)
- // Build the board
- zip([leftHoop, rightHoop, hoop, hoop], offsets).map({
- (item, offset) in
- animator?.addBehavior(UIAttachmentBehavior.pinAttachmentWithItem(item, attachedToItem: board, attachmentAnchor: bolts))
- })
如果你不準(zhǔn)備繼續(xù)看看swfit版里的奇妙的功能,那你很可能不熟悉zip和map。這一開始看起來似乎有些刻意而為,但其實(shí)很簡單:每個視圖都用一個偏移點(diǎn)釘住吸附,然后我們得到一系列的元組,稍后將會在映射函數(shù)中使用。顧名思義,它創(chuàng)建了所給定的物體的數(shù)組中的每個元素間的映射。這使得籃框的左右邊都用螺栓固定在板和前框,如下:
- 左臂用螺栓固定在籃板的左側(cè)
- 右臂用螺栓固定在籃板右側(cè)
- 籃框用螺栓固定在籃板的左側(cè)
下一個步驟要求我們將籃板懸掛上,別將它固定死,這樣球一個碰撞就可以使得它轉(zhuǎn)動,就像在Ball King這個游戲中一樣:
- // Set the density of the hoop, and fix its angle
- // Hang the hoop
- animator?.addBehavior({
- let attachment = UIAttachmentBehavior(item: board, attachedToAnchor: CGPoint(x: hoopPosition.x, y: hoopPosition.y))
- attachment.length = 2
- attachment.damping = 5
- return attachment
- }())
- animator?.addBehavior({
- let behavior = UIDynamicItemBehavior(items: [leftHoop, rightHoop])
- behavior.density = 10
- behavior.allowsRotation = false
- return behavior
- }())
- // Block the board rotation
- animator?.addBehavior({
- let behavior = UIDynamicItemBehavior(items: [board])
- behavior.allowsRotation = false
- return behavior
- }())
籃框已經(jīng)做好了,下面該到籃球了,用一個圓形的自定義UIImageView子類視圖,如同Ellipse類:
然后,我們可以將球作為一個普通的UIImageView實(shí)例化:
- let ball: Ball = {
- let ball = Ball(frame: CGRect(x: 0, y: 0, width: 28, height: 28))
- ball.image = UIImage(named: "ball")
- return ball
- }()
***我們設(shè)置他的 物理屬性:
- // Set the elasticity and density of the ball
- animator?.addBehavior({
- let behavior = UIDynamicItemBehavior(items: [ball])
- behavior.elasticity = 1
- behavior.density = 3
- behavior.action = {
- if !CGRectIntersectsRect(self.ball.frame, self.view.frame) {
- self.setupBehaviors()
- self.ball.center = CGPoint(x: 40, y: self.view.frame.size.height - 100)
- }
- }
- return behavior
- }())
這段代碼里我設(shè)置了彈性大小(碰撞后反彈的幅度)、密度(就把它看作重量吧),還有當(dāng)球超出彈跳范圍時立即結(jié)束游戲的事件,即為重置游戲狀態(tài)(在主視圖中)。
Collisions and gravity(碰撞和重力)
我提到了UIDynamicItemBehavior的新屬性anchored,即禁用了對象的動態(tài)behavior,同時將其保留在在碰撞循環(huán)里。聽起來用它來搭建一個堅(jiān)固的地板會很不錯:
- // Anchor the floor
- animator?.addBehavior({
- let behavior = UIDynamicItemBehavior(items: [floor])
- behavior.anchored = true
- return behavior
- }())
如果你忘了設(shè)置這個屬性,你就會抓耳撓腮。反正我是這樣的。
好吧,一切都設(shè)置好啦,現(xiàn)在只需要一些重力和一組碰撞事件了:
- animator?.addBehavior(UICollisionBehavior(items: [leftHoop, rightHoop, floor, ball]))
- animator?.addBehavior(UIGravityBehavior(items: [ball]))
重力是應(yīng)用在默認(rèn)每秒一點(diǎn)作為的向下的力的場景behavior。碰撞behavior 作為相互碰撞的view的參數(shù)。游戲已經(jīng)搭建好啦,現(xiàn)在我們可以在球上施加一個瞬發(fā)力,并用我們的手指劃過屏幕:
- let push = UIPushBehavior(items: [ball], mode: .Instantaneous)
- push.angle = -1.35
- push.magnitude = 1.56
- animator?.addBehavior(push)
這下你該明白了吧,雖然場景邊緣真的畫的很low,但是搭建它真的很有趣(是的,云朵和灌木叢都是一樣的勾畫,就像 超級馬里奧 里中的)。
老規(guī)矩,你可以在我們的 GitHub頁面 找到源代碼。
下次見