iOS开发

翻译:UIKit Dynamics Tutorial: Gett

2016-01-27  本文已影响130人  康富贵

本文译自UIKit Dynamics Tutorial: Getting Started

iOS鼓励开发者设计app时使用触摸、手势及方向旋转,不同于简单的图形,就像现实世界物理驱动一样。
结果就是用户与界面有了更深的连接,而不是简单的拟真。

这听起来像个艰难的任务,看起来真实比感觉真实更容易一些,然而,现在有了新的工具可以用:UIKit Dynamics和Motion Effects。

开始

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

刚添加的代码里,有几个动态类:

大部分行为类都有几个配置参数,举例,重力行为可以改变它的角度和量级。
试着修改这些属性让物体的下降速度加快,或者对角用不同的速度。

注意:在物理世界中,重力表示每秒下降多少米,大约是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;然而,还可以修改其他的属性:

动态添加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

上一篇下一篇

猜你喜欢

热点阅读