玩转iOS 9的UIDynamics

2015年07月22日 17:56 0 点赞 0 评论 更新于 2017-05-06 20:49

UIDynamics 是 iOS 7 SDK 中颇受欢迎的新增特性,它本质上是一个支持 UIView 的物理引擎,借助该引擎,我们能够自定义 UI 控件的物理特性。此 API 易于理解,利用它可以轻松创建出色的动画或过渡效果。本文将深入探究 iOS 9 中 UIDynamics 的新特性。

碰撞边界(Collision Bounds)

在 UIDynamics 的首个版本中,其碰撞系统(在 UICollisionBehavior 里)仅支持矩形碰撞边界。这不难理解,因为 UIView 通常采用矩形架构,而圆形碰撞边界较为少见,更不用说自定义的贝塞尔曲线了。不过在 iOS 9 中,UIDynamicItem 协议新增了一个属性:UIDynamicItemCollisionBoundsType,它支持以下枚举类型:

  • Rectangle
  • Ellipse
  • Path

该属性为只读属性,若要修改其值,需提供自定义子类,示例代码如下:

class Ellipse: UIView {
override var collisionBoundsType: UIDynamicItemCollisionBoundsType {
return .ellipse
}
}

上述代码展示了默认碰撞边界的 UIView 以及带有 .ellipse 属性的 UIView。对于圆形视图,我们可以使用 .ellipse 类型;若要绘制更复杂的连续刚体图形,则可使用 .path 枚举类型,同时需要重写以下属性:

var collisionBoundingPath: UIBezierPath { get }

这里的路径可以是任意形状,但需满足凸面条件(即多边形内任意两点间的线段完全包含在多边形内),并且路径方向为逆时针。由于凸面条件限制较为严格,因此引入了 UIDynamicItemGroup,它可以详细描绘一组不同图形的组合图形。只要组合中的每个图形都是凸面的,即便最终得到的多边形是凹面的也没问题。

场行为(Field Behavior)

场行为是在整个场景中应用的一种新行为。一个常见的例子是我们一直在使用的 UIGravityBehavior,在该行为下,场景中的每个物体都会受到向下的重力作用。如今,我们可以使用一组新的场力,例如径向力(距离场景中心越近,力越大)、噪声力(在场景内随机产生不同的力)等。

动力元素行为(Dynamic Item Behavior)

UIDynamicItemBehavior 包含了几个有趣的新特性:

  • var charge: CGFloat:代表能够影响元素在电磁场上移动的电荷。
  • var anchored: Bool:本质上是将图形变为碰撞中的静态物体,且不响应事件(当有物体撞上它时,它不会移动),可用于表示地板或墙壁。

吸附行为(Attachment Behavior)

UIAttachmentBehavior 经过改进后,具备了新的方法和属性,如 frictionTorqueattachmentRange。现在,吸附行为变得更加灵活,我们可以指定相对滑动动作、固定吸附、绳索链接以及针型吸附。以针型吸附为例,可想象两个钉在一起的物体。

实战:打造简单篮球游戏(BallSwift)

我上周花费了不少时间玩《球王(Ball King)》这款游戏,它的理念简单却表现出色,并且采用了与获得苹果设计奖的《Crossy Road》相同的理念,不会以任何方式影响玩家,例如游戏内荣誉。我尤其喜欢游戏中球的物理模型以及球打到篮板上时篮板的反应,接下来我们将利用 UIDynamics 的新特性,一步步打造一个简单的版本——BallSwift。

搭建篮框

篮球架可使用一个 UIView 作为篮板,几个 UIView 作为篮框的左右两边,最前面的 view 作为篮框本身(不带物理刚体)。借助之前定义的 Ellipse 类,我们可以创建游戏场景的视觉表现,代码如下:

/*
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 = .white
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 = .clear
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 = .clear
leftHoop.layer.cornerRadius = 3

rightHoop = Ellipse(frame: CGRect(x: hoopPosition.x + 70, y: hoopPosition.y + 80, width: 10, height: 6))
rightHoop.backgroundColor = .clear
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].forEach { self.view.addSubview($0) }
}

在上述代码中,篮框以编程方式创建在常量 CGPoint hoopPosition 上。需要注意的是,视图的顺序很重要,因为我们希望篮框位于篮球抛投点的上方。

固定篮框部件

篮框的左右臂需要具备物理圆形身体,以实现与球的自然碰撞,并通过螺栓固定在篮板和前框上。这两个部件将作为基本的 UIDynamicItem,但不直接参与碰撞。新推出的针型吸附功能正好适用于此,代码如下:

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)
]

zip([leftHoop, rightHoop, hoop, hoop], offsets).forEach { (item, offset) in
animator?.addBehavior(UIAttachmentBehavior.pinAttachment(with: item, attachedTo: board, attachmentAnchor: offset))
}

通过上述代码,实现了以下固定效果:

  • 左臂用螺栓固定在篮板的左侧。
  • 右臂用螺栓固定在篮板右侧。
  • 篮框用螺栓固定在篮板的左侧。

接下来,我们需要将篮板悬挂起来,使其在球碰撞时能够转动,就像《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
}()

创建篮球

篮球可使用一个圆形的自定义 UIImageView 子类视图,类似于 Ellipse 类。然后将球作为普通的 UIImageView 实例化,并设置其物理属性:

let ball: Ball = {
let ball = Ball(frame: CGRect(x: 0, y: 0, width: 28, height: 28))
ball.image = UIImage(named: "ball")
return ball
}()

// Set the elasticity and density of the ball
animator?.addBehavior {
let behavior = UIDynamicItemBehavior(items: [ball])
behavior.elasticity = 1
behavior.density = 3
behavior.action = {
if !self.ball.frame.intersects(self.view.frame) {
self.setupBehaviors()
self.ball.center = CGPoint(x: 40, y: self.view.frame.size.height - 100)
}
}
return behavior
}()

在上述代码中,我们设置了球的弹性大小(碰撞后反弹的幅度)、密度(可看作重量),并添加了当球超出弹跳范围时重置游戏状态的事件。

添加碰撞和重力效果

使用 UIDynamicItemBehavioranchored 属性,我们可以创建一个坚固的地板:

// Anchor the floor
animator?.addBehavior {
let behavior = UIDynamicItemBehavior(items: [floor])
behavior.anchored = true
return behavior
}()

最后,添加重力和碰撞事件:

animator?.addBehavior(UICollisionBehavior(items: [leftHoop, rightHoop, floor, ball]))
animator?.addBehavior(UIGravityBehavior(items: [ball]))

为了让球运动起来,我们可以在球上施加一个瞬发力,通过手指划过屏幕来实现:

let push = UIPushBehavior(items: [ball], mode: .instantaneous)
push.angle = -1.35
push.magnitude = 1.56
animator?.addBehavior(push)

至此,游戏搭建完成。尽管场景边缘的绘制可能不够精美,但搭建过程十分有趣。

作者信息

洞悉

洞悉

共发布了 515 篇文章