美文网首页iOS开发
翻译:UIKit Dynamics Tutorial: Gett

翻译:UIKit Dynamics Tutorial: Gett

作者: 康富贵 | 来源:发表于2016-01-27 21:01 被阅读130次

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

    相关文章

      网友评论

        本文标题:翻译:UIKit Dynamics Tutorial: Gett

        本文链接:https://www.haomeiwen.com/subject/mmzdkttx.html