翻译自raywenderlich网站iOS教程Graphics & Animation系列
介绍
UIKit Dynamics是一个集成到UIKit中的完整物理引擎。它允许您通过添加诸如重力,附件(弹簧)和力量等行为来创建感觉真实的界面。您定义了您希望界面元素采用的物理特征,动态引擎负责其余部分。

Motion Effects可以创建很酷视差效果,就像在倾斜iOS 7主屏幕时看到的一样。基本上,我们可以利用手机加速计提供的数据来创建对手机方向变化作出反应的接口。

当一起使用时,运动和动态成为用户体验工具的重要组成部分,使您的交互栩栩如生。用户将通过看到它以自然,动态的方式回应他们的行为。

准备开始

ViewController.swift 添加如下代码在viewDidLoad:

    let square = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
    square.backgroundColor = UIColor.grayColor
    view.addSubview(square)

运行之后效果如下:
clipboard.png

增加重力效果
仍然在 ViewController.swift中,在viewDidLoad上方添加以下属性:

var animtor: UIDynamicAnimator!
var gravity: UIGravityBehavior!

这些属性是隐式解包的optionals(如类型名称后面的!所示)。 这些属性必须是可选的,因为我们没有在init方法中初始化它们。 此时可以使用隐式解包的optionals,因为我们知道这些属性在初始化后不会为零。 可以防止每次使用的时候需要!来解包。

添加以下代码在viewDidLoad的结尾处:

  animtor = UIDynamicAnimator(referenceView: view)
  gravity = UIGravityBehavior(items: [square])
  animtor.addBehavior(gravity)
    

现在构建并运行应用程序。 可以看到你的方块慢慢地开始加速,直到它落在屏幕的底部。
在刚刚添加的代码中,这里有几个动态类:

  • UIDynamicAnimator是UIKit物理引擎。这个类跟踪你添加到引擎的各种行为,比如引力,并提供整体上下文。当创建animator的实例时,将传入animator用于定义其坐标系的参考视图。
  • UIGravityBehavior模拟重力的行为并对一个或多个项目施加作用力,可以建模物理交互。当创建一个行为的实例时,将它与一组项目相关联 - 通常是视图。 通过这种方式,可以选择哪些项目受到行为的影响,在这种情况下哪些项目会受到重力的影响。

大多数行为都有一些配置属性;例如,重力行为可以改变它的角度和大小。尝试修改这些属性以使对象以不同的加速度向上,侧向或对角线倾斜。

注:关于单位的简单说法:在物理世界中,重力(g)以米每平方秒表示,大约等于9.8米/秒2。 
使用牛顿第二定律,你可以用下面的公式计算物体在重力影响下的落差:
distance = 0.5 × g × time2
在UIKit Dynamics中,公式相同,但单位不同。使用每秒数千像素单位的单位 ,而不是米。 
使用牛顿第二定律,仍然可以根据提供的重力组件随时计算出视角。

当然我们并不需要知道这些细节,只需要知道g值越大意味着物体下降的越快。

设置边界

为了保持方块在屏幕的边界内,需要定义一个边界。
添加另一个属性在 ViewController.swift

var collision: UICollisionBehavior!

viewDidLoad:下面添加这几行:

    collision = UICollisionBehavior(items: [square])
    collision.translatesReferenceBoundsIntoBoundary = true
    animtor.addBehavior(collision)
   

上面的代码创建了一个碰撞行为,它定义了一个或多个关联项与之交互的边界。

上述代码不是明确添加边界坐标,而是将translatesReferenceBoundsIntoBoundary属性设置为true。 这会导致边界使用提供给UIDynamicAnimator的参考视图的边界。

运行时可以看到正方形与屏幕底部碰撞,稍微反弹,然后停止,如下所示:

clipboard.png

以上我们用很少的代码实现了一个很酷的效果

处理碰撞

接下来,添加一个不可移动的障碍,下降的方块将碰撞和互动。
将以下代码插入viewDidLoad中添加square的代码下面:

    let barrier = UIView(frame: CGRect(x: 0, y: 300, width: 130, height: 20))
    barrier.backgroundColor = UIColor.red
    view.addSubview(barrier)

构建并运行你的应用程序; 你会在屏幕上看到一个红色的“障碍”。 然而,事实证明,这个障碍并不是那么有效:

clipboard.png

这不是我们想要的效果,但它确实提供了一个重要的提示:动态只会影响与行为相关的视图:

clipboard.png

UIDynamicAnimator与提供坐标系的参考视图相关联。 然后添加一个或多个行为,这些行为会对与其相关联的项目施加作用力。 大多数行为可以与多个项目相关联,并且每个项目可以与多个行为相关联。 上图显示了应用中的当前行为及其关联。

当前代码中的任何行为都不能“意识到”屏障,所以就下层动态引擎而言,屏障甚至不存在。

让对象响应碰撞

为了使正方形与障碍碰撞,找到初始化碰撞行为的代码并将其替换为以下内容:

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

碰撞对象需要知道它应该与之交互的每个视图; 因此将障碍添加到物品列表中允许碰撞物体也作用于障碍物。

构建并运行应用程序; 这两个对象相互碰撞并相互作用,如下图所示:

clipboard.png

碰撞行为在与其相关的每个项目周围形成“边界”; 这将它们从可以通过彼此的对象变成更坚实的对象。

更新前面的图,可以看到碰撞行为现在与两个视图相关联:

clipboard.png

但是,这两个对象之间的交互仍然存在不太正确的地方。 屏障被认为是不可移动的,但是当两个物体在当前配置中碰撞时,屏障会被打破位置并开始向屏幕底部旋转。

更奇怪的是,屏障从屏幕底部反弹并且不像平方那样安定下来 - 这很有意义,因为重力行为不会与屏障相互作用。 这也解释了为什么屏障不会移动,直到正方形与它碰撞。

现在需要一个不同的方法来解决问题。 由于障碍视图是不可移动的,所以动力学引擎不需要知道它的存在。 但是如何检测到碰撞?

看不见的边界和碰撞

将碰撞行为初始化更改回其原始形式,以便仅识别方块:

collision = UICollisionBehavior(items: [square])

紧随此行后,添加以下内容:

    collision.addBoundary(withIdentifier: "barrier" as NSCopying, for: UIBezierPath(rect: barrier.frame))

上面的代码添加了一个与屏障视图具有相同框架的不可见边界。 红色屏障对用户而言仍然可见,但对动态引擎不可见,而边界对动态引擎可见但对用户不可见。 当方块落下时,它似乎与屏障相互作用,但它实际上碰撞了不动的边界。

构建并运行,如下所示:

clipboard.png

方块现在从边界反弹,旋转一点,然后继续往屏幕底部前进的地方休息。

到目前为止,UIKit Dynamics的功能已经变得相当清晰:只需几行代码就可以完成很多工作。 引擎盖下有很多事情要做, 下一节将向展示动态引擎如何与应用程序中的对象交互的一些细节。

碰撞的细节

每个动态行为都有一个动作属性。 将以下代码添加到viewDidLoad中:

    collision.action = {
        print("\(NSStringFromCGAffineTransform(self.square.transform)) \(NSStringFromCGPoint(self.square.center))")
    }

上面的代码记录下降方块的中心和变换属性。运行应用程序,将在Xcode控制台窗口中看到这些日志消息。

对于1〜400毫秒,日志消息:

[1, 0, 0, 1, 0, 0], {150, 236}
[1, 0, 0, 1, 0, 0], {150, 243}
[1, 0, 0, 1, 0, 0], {150, 250}

在这里可以看到动态引擎正在改变每个动画步骤中的方块的中心 - 也就是它的帧。

只要方块碰到屏障,它就开始旋转,这会产生如下的日志消息:

    
[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}

在这里可以看到,动态引擎正在使用变换和帧偏移的组合来根据底层物理模型定位视图。

虽然动态适用于这些属性的确切值可能没有多大意义,但知道它们正在被应用很重要。 因此,如果以编程方式更改对象的框架或转换属性,则可以预期这些值将被覆盖。 这意味着当它处于动态的控制之下时,不能使用变换来缩放对象。

将dynamic behaviors应用于对象的唯一要求是它采用UIDynamicItem协议,如下所示:

protocol UIDynamicItem : NSObjectProtocol {
  var center: CGPoint { get set }
  var bounds: CGRect { get }
  var transform: CGAffineTransform { get set }
}

UIDynamicItem协议提供动态读写访问中心和变换属性,允许它根据其内部计算移动项目。 它还具有对边界的读取权限,用于确定项目的大小。 这允许它在物品的周边周围产生碰撞边界,并且在施加力时计算物品的质量。

这个协议意味着动态与UIView不紧密耦合; 的确有另一个UIKit类不是视图,但仍然采用这个协议:UICollectionViewLayoutAttributes。

碰撞通知

到目前为止,已经添加了一些视图和行为,然后让动态接管。 在下一步中,将了解如何在物品碰撞时接收通知。

仍然在ViewController.swift中,通过更新类声明来采用UICollisionBehaviorDelegate协议:

class ViewController: UIViewController, UICollisionBehaviorDelegate {

viewDidLoad中,在初始化碰撞对象之后将视图控制器设置为委托,如下所示:

collision.collisionDelegate = self
func collisionBehavior(_ behavior: UICollisionBehavior, beganContactFor item: UIDynamicItem, withBoundaryIdentifier identifier: NSCopying?, at p: CGPoint) {
    print("Boundary contact occurred - \(String(describing: identifier))")
}

这种委托方法在发生冲突时被调用。 它将打印出一条日志消息给控制台。 为了避免使用大量消息弄乱控制台日志,请删除在上一节中添加的collision.action日志记录。

运行将在控制台中看到以下条目:

Boundary contact occurred - Optional(barrier)
Boundary contact occurred - Optional(barrier)

从上面的日志消息中可以看到方块与“标识符”(barrierView)屏障相撞两次;

在print下面添加以下内容:

    let collidingView = item as! UIView
    UIView.animate(withDuration: 1) {
        collidingView.backgroundColor = .gray
    }

上面的代码将碰撞项目的背景颜色淡化为灰色。

构建并运行以查看这种效果:

clipboard.png
到目前为止,UIKit Dynamics已经根据物品的界限自动设置物品的物理属性(如质量和弹性)。 接下来,将看到如何使用UIDynamicItemBehavior类自己控制这些物理属性。

配置item属性

viewDidLoad中,将以下内容添加到方法的末尾:

    let itemBehaviour = UIDynamicItemBehavior(items: [square])
    itemBehaviour.elasticity = 0.6
    animtor.addBehavior(itemBehaviour)

上面的代码创建一个项目行为,将其与方块关联,然后将行为对象添加到动画设计器中。 弹性属性控制着物品的弹性; 值为1.0表示完全弹性碰撞; 也就是说,在碰撞中没有能量或速度丢失的地方。 我们将方块的弹性设置为0.6,这意味着每次反弹时平方将失去速度。

构建并运行你的应用程序,你会注意到这个广场现在表现得更加酷,如下所示:

clipboard.png

注:制作上面的图片并显示方块路径轮廓的代码如下:
func addPosition()  {
    var updateCount = 0
    collision.action = {
        if updateCount % 3 == 0 {
            let outLine = UIView(frame: self.square.bounds)
            outLine.transform = self.square.transform
            outLine.center = self.square.center
            
            outLine.alpha = 0.5
            outLine.backgroundColor = .clear
            outLine.layer.borderColor = self.square.layer.presentation()?.backgroundColor
            outLine.layer.borderWidth = 1.0
            self.view.addSubview(outLine)
        }
        updateCount += 1
    }
}

在上面的代码中,只改变了物品的弹性; 然而,该项目的行为类有许多其他属性可以在代码中操作。 如下:

     elasticity - 决定“弹性”碰撞的方式,即物体在碰撞中的弹性或“橡胶状”程度。
     friction - 决定沿表面滑动时的运动阻力。
     density - 当与大小相结合时,这将给出物品的整体质量。质量越大,加速或减速物体越难。
     resistance - 决定抵抗任何线性移动的数量。这与仅适用于滑动运动的摩擦形成对比。
     angularResistance - 确定抵抗任何旋转运动的量。
     allowsRotation - 如果将此属性设置为NO,则不管发生的旋转力如何,对象都不会旋转。
     

动态添加行为

在下一步中,将看到如何动态地添加和删除行为。

打开ViewController.swift并在viewDidLoad上方添加以下属性:

var firstContact = false      

将以下代码添加到碰撞代理方法的末尾func collisionBehavior(_ behavior: UICollisionBehavior, beganContactFor item: UIDynamicItem, withBoundaryIdentifier identifier: NSCopying?, at p: CGPoint)

     if !firstContact {
        firstContact = true

        let square = UIView(frame: CGRect(x: 30, y: 0, width: 100, height: 100))
        square.backgroundColor = .gray
        view.addSubview(square)
        collision.addItem(square)
        gravity.addItem(square)
        let attach = UIAttachmentBehavior(item: collidingView, attachedTo: square)
        animtor.addBehavior(attach)
    }

上面的代码检测屏障和正方形之间的初始接触,创建第二个正方形并将其添加到碰撞和重力行为中。 另外,还可以设置一个附件行为,以创建用虚拟弹簧附加一对对象的效果。

构建并运行; 当原始方块碰到屏障时,应该会看到一个新的方块,如下所示:

clipboard.png

用户交互

正如刚刚看到的,当物理系统已经运动时,我们可以动态添加和删除行为。 在最后一节中,每当用户点击屏幕时,都会添加另一种类型的动态行为UISnapBehavior。 一个UISnapBehavior使一个对象跳跃到一个有弹性的弹簧式动画的指定位置。

删除上一节添加的代码:collisionBehavior()中的firstContact属性和if语句。 在屏幕上只能看到一个方块的UISnapBehavior效果会更容易。

viewDidLoad上添加两个属性:

var square: UIView!
var snap: UISnapBehavior!

这将跟踪方块视图,以便您可以从视图控制器的其他位置访问它。 您将在下一个使用捕捉对象。

viewDidLoad中,从square声明中删除let关键字,以便它使用新属性而不是局部变量:

square = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))

最后,为touchesEnded添加一个实现,以在用户触摸屏幕时创建并添加新的捕捉行为:

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    if snp != nil {
        animtor.removeBehavior(snp)
    }
    let touchs = touches as NSSet
    
    let touch = touchs.anyObject() as! UITouch
    snp = UISnapBehavior(item: square, snapTo: touch.location(in: view))
    animtor.addBehavior(snp)
}

这段代码非常简单。 首先,它检查是否存在现有的快照行为并将其删除。 然后创建一个新的捕捉行为,将方块对齐到用户的触摸位置,并将其添加到动画制作工具中。

构建并运行应用程序。 尝试点击; 方块会跑到触摸的地方

这里是最终demo,此demo是raywenderlich下面iOS的Graphics & Animation整个教程系列的集合。


CharlieWang
20 声望2 粉丝

Be a cartoon heart