本文译自UIKit Dynamics Tutorial: Getting Started
iOS鼓励开发者设计app时使用触摸、手势及方向旋转,不同于简单的图形,就像现实世界物理驱动一样。
结果就是用户与界面有了更深的连接,而不是简单的拟真。
这听起来像个艰难的任务,看起来真实比感觉真实更容易一些,然而,现在有了新的工具可以用:UIKit Dynamics和Motion Effects。
-
UIKit Dynamics是UIKit中的物理引擎,它能够帮助创建真实的物理行为:重力、吸附(弹簧)、弹性。
定义好想要的物理特征,物理引擎会帮你完成剩下的工作。 -
Motion Effects能够帮助你创建酷酷的视差效果,就像iOS7的主屏幕一样。
基本上你可以通过手机的加速计来响应手机方向的变化。
开始
UIKit dynamics有很多乐趣,最好的学习方法就是开始动手。
打开Xcode,选择File / New / Project … ,然后选iOS Application / Single View Application,
再输入项目名称DynamicsDemo。项目就创建好了,打开ViewController.swift,
将下面的代码添加到viewDidLoad的最后。
let square = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
square.backgroundColor = UIColor.grayColor()
view.addSubview(square)
这段代码简单的在界面上创建了一个正方形UIView。
编译运行一下,一个孤零零的正方形在屏幕上,就像下面这样:
如果你是在真机上运行的,试着倾斜下你的手机,头朝下,活着摇一摇,什么都没发生?
那就对了 — 所有事情都要先计划。当你添加view到界面上后它会一直在那个地方,直到给它加上了动态效果。
添加重力
还是ViewController.swift,添加下面的属性到viewDidLoad的上方:
var animator: UIDynamicAnimator!
var gravity: UIGravityBehavior!
这些属性是implicitly-unwrapped optionals(隐式解析可选)(属性名后加了感叹号)。
这些属性一定是可选的,因为你不能用init方法初始化。
你可以用隐式解析可选是因为初始化后我们知道那些属性不可能是nil。
这样可以防止你每次访问属性时都加上感叹号。
将下面的代码添加到viewDidLoad的最后:
animator = UIDynamicAnimator(referenceView: view)
gravity = UIGravityBehavior(items: [square])
animator.addBehavior(gravity)
说明一下。现在编译运行下。你会看到正方形缓慢地下坠,直到触底,就像下面这样:
FallingSquare.png刚添加的代码里,有几个动态类:
-
UIDynamicAnimator是UIKit的物理引擎。这个类能够跟踪你添加的各种行为,如重力,并提供整体上下文。
当你创建一个animator的实例时,传递一个引用view来定义它的坐标系统。 -
UIGravityBehavior是重力行为的模型,可以给一个或多个item施加力度,让你模拟物理相互作用。
当你创意一个行为的实例时,设置一组item跟行为关联起来 — 必须是view。
你可以通过这种方式选择让哪个item受到行为的影响,在本例子中item受到重力影响。
大部分行为类都有几个配置参数,举例,重力行为可以改变它的角度和量级。
试着修改这些属性让物体的下降速度加快,或者对角用不同的速度。
注意:在物理世界中,重力表示每秒下降多少米,大约是9.8 m/s2。
利用牛顿第二定律,你可以用下列公式计算一个物体在重力的影响下离底面有多远:
distance = 0.5 × g × time2
在UIKit Dynamics,公式是相同的,但单位不同。当然不是米,以每秒数千像素为单位。
利用牛顿第二定律,你的view根据你提供的重力正常工作。
你真的需要理论知识吗,不;你只需要知道g越大下坠速度越快,不需要知道底层算法。
设置边界
尽管看不见它,即使已经碰到了屏幕最底部正方形依然会不停地下坠。
为了让正方形留在屏幕上,你需要给它定义边界。
添加另一个属性到ViewController.swift:
var collision: UICollisionBehavior!
将下面的代码添加到viewDidLoad的最后:
collision = UICollisionBehavior(items: [square])
collision.translatesReferenceBoundsIntoBoundary = true
animator.addBehavior(collision)
上面这段代码创建了一个碰撞检测行为,
不是直接用坐标描绘边界,另外还是设置属性translatesReferenceBoundsIntoBoundary为true。
这是因为UIDynamicAnimator在设置view的边界时使用了bounds属性。
编译运行,正方形碰到屏幕底部后回弹了一下,然后静止不动了。
SquareAtRest.png这是个令人印象深刻的行为,特别是只写了那么少的代码。
处理碰撞
下一步,在下坠的过程中添加一个障碍,正方形会撞到障碍。
将下面的代码插入到viewDidLoad中正方形的下面:
let barrier = UIView(frame: CGRect(x: 0, y: 300, width: 130, height: 20))
barrier.backgroundColor = UIColor.redColor()
view.addSubview(barrier)
编译运行,一个红色的障碍横插在屏幕中间。但是,障碍并不会阻碍正方形下坠:
BadBarrier.png这不是我们想要的效果,还有个重要的提示:dynamics只会影响和行为关联的view。
图解:
DynamicClasses.pngUIDynamicAnimator会关联一个Reference View,并由Reference View提供坐标系统。
然后添加一个或多个行为,并和Square关联起来。
多部分行为可以关联多个item,那个item又可以关联多个行为。
上面的流程图展示了当前的行为和它们关联的对象。
现在已经可以看见障碍物了,但还没有和物理引擎关联起来,就像不存在一样。
碰撞反应
为了让正方形和障碍物相互碰撞,替换collision的初始化方法:
collision = UICollisionBehavior(items: [square, barrier])
将互相碰撞的两个view作为参数传递给collision;障碍物才能生效。
编译运行,两个view会相互碰撞,就像下面这样:
GoodBarrier.pngcollision behavior给每个view的四周都添加了一个看不见的边框;让原本可以相互穿过的view变的更坚硬。
更新之前的图解,collision behavior现在将两个view关联起来了:
DynamicClasses2.png然而,两个view之间的交互仍然存在一些问题。障碍物应该不静止不动的,但障碍物被撞后会朝着底部下坠。
更奇怪的是,障碍物在触底后会回弹,没有像正方形那样静止不动,原因就是gravity behavior没有给障碍物施加影响。
这就是为什么障碍物在被正方形撞到之前都不会动。
看起来要换个方法解决问题。障碍物是静止不动的,所以不需要和dynamics engine关联起来。但要如何检测碰撞呢?
看不见的边框碰撞
collision behavior的初始化还原为最初的状态:
collision = UICollisionBehavior(items: [square])
collision的下面再加一行:
// add a boundary that has the same frame as the barrier
collision.addBoundaryWithIdentifier("barrier", forPath: UIBezierPath(rect: barrier.frame))
上面这段代码在障碍物相同的frame放置了一个看不见的边框。
红色的障碍物仍然可见,但没有添加到dynamics engine,
而用户虽然看不见边框,但边框仍然可以触发碰撞。
正方形在下坠时,撞上了障碍物,但实际上撞的是看不见的边框。
编译运行,就像下面这张图一样:
BestBarrier.png正方形在撞到边框后,旋转了一下,然后一直下坠直到触底。
现在UIKit Dynamics的功能大致清楚了:只需要少量的代码就可以完成复杂的物理现象。
还有一些隐藏的功能;下一节将展示更多物理引擎的细节。
碰撞检测的幕后推手
每个dynamic behavior都有个action属性,动画每一帧都会执行action block。
添加下面的代码到viewDidLoad:
collision.action = {
println("\(NSStringFromCGAffineTransform(square.transform)) \(NSStringFromCGPoint(square.center))")
}
这段代码打印了正方形的center和transform属性。
编译运行,会看到log输出到console window。
400毫秒以内的log看上去是这个样子:
[1, 0, 0, 1, 0, 0], {150, 236}
[1, 0, 0, 1, 0, 0], {150, 243}
[1, 0, 0, 1, 0, 0], {150, 250}
dynamics engine在动画的每一帧都会修改正方形的center。
当正方形撞到了障碍物会开发旋转,log信息会像下面这样:
[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}
可以看到dynamics engine使用了底层物理模型修改view的transform和frame偏移,从而改变view的位置。
虽然了解这些属性的精确值没什么用,但重要的是知道哪些属性被用到了。
因此,如果写代码修改view的frame或transform,这些值会被覆盖掉。
这意味着,当view在dynamics的控制下时,不可以使用transform属性。
dynamic behaviors的方法签名使用术语而不是view。
唯一的条件是实现协议UIDynamicItem,就像下面这样:
protocol UIDynamicItem : NSObjectProtocol {
var center: CGPoint { get set }
var bounds: CGRect { get }
var transform: CGAffineTransform { get set }
}
UIDynamicItem协议提供了动态读写访问center和transform属性,从而达到基于内部算法来移动item的效果。
它也有对bounds属性的访问权限,可以用来确定item的大小。这使得它能够在四周创建碰撞边框以及计算它的质量。
这个协议意味着dynamics与UIView解耦合;的确还有另一个UIKit类不是view确使用了这个协议:UICollectionViewLayoutAttributes。
这使得dynamics可以给collection views添加动画。
碰撞通知
到目前为止,你已经添加了一些view和behaviors,然后让dynamics接管他们。
在这一节中将学习如何在他们发生碰撞时收到通知。
还是在ViewController.swift,给类定义添加UICollisionBehaviorDelegate:
class ViewController: UIViewController, UICollisionBehaviorDelegate {
在viewDidLoad中,在collision初始化后设置viewController为委托:
collision.collisionDelegate = self
下一步,添加collision behavior委托方法:
func collisionBehavior(behavior: UICollisionBehavior!, beganContactForItem item: UIDynamicItem!, withBoundaryIdentifier identifier: NSCopying!, atPoint p: CGPoint) {
println("Boundary contact occurred - \(identifier)")
}
在发生碰撞时会执行这个委托方法。只在控制台输出log。为了避免控制台的log太乱,可以选择删除collision.action的输出。
编译运行,当两个view即将碰撞时,会看到这样的log:
Boundary contact occurred - barrier
Boundary contact occurred - barrier
Boundary contact occurred - nil
Boundary contact occurred - nil
Boundary contact occurred - nil
Boundary contact occurred - nil
从这个log信息从可以看出正方形和barrier碰撞了两次。
(null)identifier是外层reference view的边框。
这些log信息有很好的易读性,但如果item在回弹出现一些视觉指示会更友好。
在输出log代码的下面,添加这些代码:
let collidingView = item as UIView
collidingView.backgroundColor = UIColor.yellowColor()
UIView.animateWithDuration(0.3) {
collidingView.backgroundColor = UIColor.grayColor()
}
这段代码是在item发生碰撞时将它的背景色设置为黄色,又很快重新回到了灰色。
编译运行后可以看到这样的效果:
YellowCollision.png正方形在碰到边界是会闪成黄色。
到目前为止,UIKit Dynamics会根据view的bounds自动计算物理属性(比如质量、弹力)。
下一步将学习如何使用UIDynamicItemBehavior来控制物理属性。
配置属性
将下面的代码添加到viewDidLoad方法的最后:
let itemBehaviour = UIDynamicItemBehavior(items: [square])
itemBehaviour.elasticity = 0.6
animator.addBehavior(itemBehaviour)
这段代码创建了一个item behavior,跟square关联起来了,然后添加到了animator。
elasticity属性控制item的反弹力;值等于1时表示完全弹性碰撞;意味着,碰撞后不会减速或停下。
square设置为了0.6,这意味着每反弹一次速度就会下降一点。
编译运行,会发现square变的非常有弹性:
PrettyBounce.png在上面的代码中只修改了elasticity;然而,还可以修改其他的属性:
- elasticity – 确定在碰撞时有多大的弹性
- friction – 确定在沿表面滑动时有多少阻力
- density – 结合size时,会给item一个总质量,质量越大越难加速,质量越大减速越快。
- resistance – 确定在直线移动时会收到多少阻力。跟friction的区别是,这仅适用于滑动。
- angularResistance – 确定在旋转运动时会收到多少阻力。
- allowsRotation – 这是个有趣的属性,它并不模拟真实世界的物理特性。当它设置为NO时便不会再旋转,无论有多少旋转力度。
动态添加behaviors
目前,
打开ViewController.swift,在viewDidLoad方法的上面添加属性:
var firstContact = false
在collision delegate的委托方法collisionBehavior(behavior:beganContactForItem:withBoundaryIdentifier:atPoint:)的最后添加:
if (!firstContact) {
firstContact = true
let square = UIView(frame: CGRect(x: 30, y: 0, width: 100, height: 100))
square.backgroundColor = UIColor.grayColor()
view.addSubview(square)
collision.addItem(square)
gravity.addItem(square)
let attach = UIAttachmentBehavior(item: collidingView, attachedToItem:square)
animator.addBehavior(attach)
}
上面的代码检查了barrier和square的初次接触,创建了第二个正方形,且添加了碰撞和重力行为。
此外,还设置了吸附行为,做出了虚拟弹簧的吸附效果。
编译运行,当正方形和障碍物初次碰撞后会有一个新的正方形初现,就像下面这样:
Attachment.png虽然两个正方形之间有连接,但看不见连接线或弹簧,因为没有在屏幕上绘制它。
交互
如你所见,物理系统在工作时动态地添加和删除行为。在最后一节,将添加另一种dynamics behaviour,UISnapBehavior,
无论用户点击屏幕的什么位置,UISnapBehavior会控制view跳到指定位置,并附带弹簧动画。
为了让屏幕只呈现UISnapBehavior的效果。删除上一节中添加的代码:包括firstContact属性和collisionBehavior()方法中的if判断。
在viewDidLoad方法的上面添加两个属性:
var square: UIView!
var snap: UISnapBehavior!
square变成了属性,这样在viewController的任何地方都可以访问。
下一步将使用snap。
在viewDidLoad中,移除掉squar前面的let关键字,它将变成新属性,而不是局部变量:
square = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
最后,添加touchesEnded方法实现,当用户点击屏幕时创建一个新的snap behavior:
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
if (snap != nil) {
animator.removeBehavior(snap)
}
let touch = touches.anyObject() as UITouch
snap = UISnapBehavior(item: square, snapToPoint: touch.locationInView(view))
animator.addBehavior(snap)
}
这段代码非常直接,先检查snap behavior有没有,如果有就删除。
然后根据用户点击的位置创建一个新的snap behavior,然后添加到animator。
编译运行。在四周点一点,square会快速移动到你点击的位置!
何去何从
到这里你应该已经对UIKit Dynamics的核心要点有了深刻理解。
这里可以下载本文的最终示例。
UIKit Dynamics给app带来了强大的物理引擎。你可以将反弹、弹簧和重力添加到你的app中,让用户更加身临其境。
SandwichFlowDynamics.png如果你想了解更多关于UIKit Dynamics的知识,可以阅读iOS 7 By Tutorials。
网友评论