Swift中的UIKit动力学(二)

碰撞背后的故事

在UIKit的动态引擎中,每个行为都有一个action属性,它的类型是一个函数(() -> Void),我们可以使用一个闭包来打印一下每一步行为的信息。我们在viewDidLoad方法加入下面这行代码:


collision.action = {
println("\(NSStringFromCGAffineTransform(square.transform)) \(NSStringFromCGPoint(square.center))");
};

上面的代码记录了蓝绿色方块坠落、碰撞时的centertransform属性。当编译运行时,在方块下坠的前一秒我们可以在控制台看到如下信息:

[1, 0, 0, 1, 0, 0] {150, 150}
[1, 0, 0, 1, 0, 0] {150, 150}
[1, 0, 0, 1, 0, 0] {150, 151}
[1, 0, 0, 1, 0, 0] {150, 151}
[1, 0, 0, 1, 0, 0] {150, 152}
[1, 0, 0, 1, 0, 0] {150, 154}

我们可以看到,随着方块的坠落,它的center属性在不断变化,也就是中点的x坐标没变,y坐标在一直变化,这时方块还没有与barrier发生碰撞。当方块与红色障碍物barrier碰撞时,我们可以看到方块的transform属性也有了变化:

[0.999995500003375, 0.0029999955000020251, -0.0029999955000020251, 0.999995500003375, 0, 0] {150, 250}
[0.99970233476860393, 0.02439757894140453, -0.02439757894140453, 0.99970233476860393, 0, 0] {151, 249}
[0.99894218654750844, 0.045983779049604996, -0.045983779049604996, 0.99894218654750844, 0, 0] {152, 249}

通过这些信息我们得知,动态引擎通过不断改变ransformcenter两个数据模型的数据来驱动View的行为。

虽然这些数据的精确度可能不是很高,但关键在于这些数据让我们知道了动力引擎是如何进行驱动的,通过这些数据也让我们知道了在行为动作背后也是有据可循的。因此,我们能不能通过程序改变这些数据呢,也就是从另一个层面来控制物体的行为动作。如果是这样的话,那么就需要我们自己计算出一套运行轨迹和行为的数据,而不是通过动作引擎去控制了。

这里有一个协议,它描述了动作行为的数据模型,那就是UIDynamicItem,它遵循NSObjectProtocol协议。UIDynamicItem协议提供了两个可读写的属性centertransform,物体的运动轨迹靠这两个属性来计算。同时还提供了一个只读的属性bounds,该属性运动物体的边界,它用于描述碰撞物体的边界周长,这样就可以计算碰撞时该物体的受力大小,并做出相应的动作。

因为UIDynamicItem是一个协议,所以这就说明了它与UIView是松耦合的关系。在UIKit中还有一个类遵循这个协议,就是UICollectionViewLayoutAttributes,这意味着动作引擎不但可以作用于一个View,还可以作用于一个集合中的View。

碰撞通知

到目前为止,我们的程序中已经添加了两个View和两个行为,并让他们产生碰撞行为,那么下面我们将看看当他们互相发生碰撞时我们如何捕获这个行为呢。

我们回到ViewController.swift文件,让ViewController类遵循UICollisionBehaviorDelegate协议:


class ViewController: UIViewController, UICollisionBehaviorDelegate {

viewDidLoad方法中,设置碰撞行为collision的碰撞代理:


collision.collisionDelegate = self;

然后我们实现UICollisionBehaviorDelegate协议的一个方法:


func collisionBehavior(behavior: UICollisionBehavior!, beganContactForItem item: UIDynamicItem!, withBoundaryIdentifier identifier: NSCopying!, atPoint p: CGPoint) {
println("Boundary contact occurred - \(identifier)");
}

这个方法在View每次发生碰撞时调用。我们让它在控制台打印一些信息。为了能更好的查看该方法中打印的信息,我们将之前设置的collision.action打印信息先去掉。

我们编译运行一下,可以看到当View之间发生交互,也就是碰撞时,我们在控制台看到了打印信息:

Boundary contact occurred - barrier
Boundary contact occurred - barrier
Boundary contact occurred - nil
Boundary contact occurred - nil
Boundary contact occurred - nil
Boundary contact occurred - nil

从上面的打印信息中可以看出,坠落的方块与标示符为barrier的View发生了两次碰撞,这就是我们之前添加的那个隐性的红色障碍物。标示符为nil的几次碰撞就是方块与引用的屏幕边界发生的碰撞。

我们接着在该方法中添加代码:


let collidingView = item as UIView;
collidingView.backgroundColor = UIColor.yellowColor();
UIView.animateWithDuration(0.3) {
collidingView.backgroundColor = UIColor.grayColor();
}

通过上面的代码可以看出,我们定义了一个collidingView常量,用item参数将其赋值,实际上这就是发生碰撞的View,就是那个小方块。然后我们将它的背景色改为黄色,然后经过0.3秒将其背景色从黄色改为了灰色。我们编译运行一下看看:

luping

我们可以看到当方块每次发生碰撞行为时,它都会闪着黄色。

到目前为止,我们所看到的一切都是由动作引擎帮我们实现的,比如说碰撞质量、弹力等,那么接下来,你会看到如何通过UIDynamicItemBehavior类由我们自己控制这些行为属性。

配置行为属性

viewDidLoad方法中添加如下代码:


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

上面的代码中,我们创建了一个行为项,它与方块View相关联,修改它的弹力为0.6,然后将其添加到行为中。elasticity的最大值为1,这意味着当方块与障碍物发生碰撞时,所以不会损失碰撞能量和速度,所以方块会一直弹。

我们编译运行一下,看看现在当方块发生碰撞时会有什么反应:

luping

我们也可以通过collision.action来记录方块的运行轨迹:


var updateCount = 0;
collision.action = {
if (updateCount % 3 == 0) {
let outline = UIView(frame: square.bounds);
outline.transform = square.transform;
outline.center = square.center;

outline.alpha = 0.5;
outline.backgroundColor = UIColor.clearColor();
outline.layer.borderColor = square.layer.presentationLayer().backgroundColor;
outline.layer.borderWidth = 1.0;
self.view.addSubview(outline);
}

++updateCount;
}

上面代码的意思是,每当collision.action执行三次时,我们添加一个View,大小与square相同,transformcenter也相同,透明度设为5,背景色设为空,边框色设为当前square的颜色。这样就可以记录下square运行轨迹了。

luping

在上面的代码中,我们只修改了弹力属性elasticity,其实还有其他的属性,我们来看一下:

  • 弹力(elasticity):设置物体发生碰撞时的弹力,比如当物体碰撞时弹开的高度、角度的大小,物体的韧性等。
  • 摩擦力(friction):设置物体滑动时的摩擦力。
  • 密度(density):设置物体密度,密度越大加速度越大。
  • 阻力(resistance):设置物体滑动时的阻力,与friction不同的是,它只作用于线性滑动时。
  • 角度阻力(angularResistance):物体进行旋转运动时的阻力设置。
  • 允许旋转(allowsRotation):该属性并不是模拟现实中的一些行为属性,它是物体是否可以旋转的开关属性。

动态添加行为

在目前的程序中,我们都是预先设置好了View的各种行为和行为属性,那么这一节我们会展现如何动态的添加或移除行为。

我们打开ViewController.swift文件,在viewDidLoad方法中添加一个属性:


var firstContact = false;

然后在碰撞代理的方法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);
}

我们又创建了一个正方形View,背景色设置为灰色,将它添加至碰撞和重力行为中,然后我们创建了一个关联行为,将这个正方形和我们之前创建的正方形关联起来。

编译运行一下,我们可以看到当之前创建的方块与障碍物发生碰撞后,会出现另一个方块:

luping

虽然你看到了两个方块似乎被连接起来了,但是之间没有连线,因为我们只是动态的添加了一个行为,然后由该行为达到了这种效果。

用户交互

现在你看到了,我们可以在系统运行时动态的添加或删除行为。在最后呢,我们再介绍一个行为,那就是UISnapBehavior。当你点击屏幕时,UISnapBehavior行为会让对象像弹簧一样跳到你点击的那个位置。

为了能更好的看清楚效果,我们将刚才添加的那段用于显示方块运行轨迹的代码注释掉,也就是下面这段代码:


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);
}

然后我们在ViewController.swift文件中添加两个属性:


var square: UIView!;
var snap: UISnapBehavior!;

viewDidLoad方法中,我们去掉申明squarelet关键字:


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

square申明为类的属性,而不是方法的常量,是因为我们需要追踪square的轨迹,这样我们就可以在ViewController类的任何方法和地方访问到square

然后我们重写touchesEnded方法,在该方法中创建Snap行为,也就是当用户点击屏幕时,会触发Snap行为:


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行为是否存在,如果存在就从引擎中移除。然后实例化Snap行为,作用项是square,弹跳的位置是我们点击屏幕的位置。编译运行看看,会发生什么呢?

luping

参考原文:UIKit Dynamics Tutorial in Swift

分享到: