翻译:UIKit Dynamics Tutorial: Getting Started

本文译自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.png

UIDynamicAnimator会关联一个Reference View,并由Reference View提供坐标系统。
然后添加一个或多个行为,并和Square关联起来。
多部分行为可以关联多个item,那个item又可以关联多个行为。
上面的流程图展示了当前的行为和它们关联的对象。

现在已经可以看见障碍物了,但还没有和物理引擎关联起来,就像不存在一样。

碰撞反应

为了让正方形和障碍物相互碰撞,替换collision的初始化方法:

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

将互相碰撞的两个view作为参数传递给collision;障碍物才能生效。

编译运行,两个view会相互碰撞,就像下面这样:

GoodBarrier.png

collision 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容