Swift中的UIKit动力学
前端之家收集整理的这篇文章主要介绍了
Swift中的UIKit动力学,
前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。
重力学这个名词不论在哪个行业领域听起来似乎都很高大上。那么在Swift中的重力学是什么呢?那就是将我们移动端屏幕上毫无生命力的东西也置于万有引力中,使它们能够展现出好像真的由于引力而向下坠落以及碰到物体后自然的弹开的效果。
要想做到这一点,我们得需要两个利器:UIKit Dynamics
和Motion Effects
。
UIKit Dynamics
是UIKit
中一套完整的物理引擎。它可以让我们在程序中对界面元素添加一些行为从而达到诸如重力、弹簧等现实中的动作行为。你只需在引擎中注册界面元素,并指定好物理行为,其他的事就交给物理引擎去完成了。
Motion Effects
可以创建很酷的视差效果,就像你iPhone上横竖屏切换时那样。它基于Apple提供的重力加速器提供的数据计算分析,使我们的界面元素根据移动设备的倾斜方向做出相应的反应。
当这两者一起使用的时候,我们就可以让程序活起来,富有生命力。
让我们开始屌丝的逆袭
我们挑一些小例子来学习UIKit Dynamics
。
注:由于个人编码习惯,在Swift代码中我还是加了;
,其实可加可不加,根据大家喜好。
打开Xcode6新建项目,选择iOS Application/Single View Application
,名称随便取,我这里命名为UIKitDynamicsDemo
,我们可以看到Single View Application
的目录结构:
打开ViewController.swift
文件,在viewDidLoad
方法中添加如下代码:
// 创建一个正方形View,颜色设置为蓝绿色,加入当前的View中
let square = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100));
square.backgroundColor = UIColor.cyanColor();
self.view.addSubview(square);
上面的代码在我们的屏幕上添加了一个蓝绿色的正方形UIView
。运行一下,我们能看到一个木讷的蓝绿色正方形静静的待在屏幕中:
如果你用真机运行,你可以摇晃一下手机、倾斜手机、拿着手机手舞足蹈一下,看看那个木讷的方块有什么反应和变化么?答案是要有反应你就见鬼了。因为我们写的这几行代码只能让它杵在那一动不动,那怎么能让它动起来呢?接下来让我们见证奇迹的时刻!
在ViewController.swift
文件中添加两个属性:
// UIKit物理引擎
var animator: UIDynamicAnimator!;
// 重力行为
var gravity: UIGravityBehavior!;
注:代码中感叹号的作用这里不做过多介绍,请自行查阅官方文档。
然后在viewDidLoad
方法中再加入以下代码:
// 实例化UIKit物理引擎类,作用于ViewController的View
animator = UIDynamicAnimator(referenceView: self.view);
// 实例化重力行为类,目前只作用于刚才创建的正方形View
gravity = UIGravityBehavior(items: [square]);
// 将重力行为添加到UIKit物理引擎类中
animator.addBehavior(gravity);
现在再编译运行一下,这时我们可以看到这个蓝绿色正方形开始做自由落体运动了,一直跌落出屏幕下边缘然后消失。
我们来看看刚才我们添加的两个属性的类型:
UIDynamicAnimator属于UIKit
物理引擎中的类。它的作用是跟踪你添加到物理引擎中的各种行为动作,比如这里的重力行为,并且提供整个上下文。实例化UIDynamicAnimator
时,它的构造函数需要传入一个referenceView
参数,用于告知它要跟踪并制定坐标的View
。
UIGravityBehavior
是一个模拟重力的模型,可作用于一个或多个元素。它的构造函数需要传入一个数组,该数组的内容就是我们希望有重力表现的一个元素或多个元素。
大多数的行为都有一些配置属性,比如重力行为就有可以改变角度和速率的属性:
// 实例化UIKit物理引擎类,目前只作用于刚才创建的正方形View
animator = UIDynamicAnimator(referenceView: self.view);
// 实例化重力行为类,目前只作用于刚才创建的正方形View
gravity = UIGravityBehavior(items: [square]);
// 角度
gravigy.angle = 1.6;
// 速率
gravigy.magnitude = 0.1;
// 将重力行为添加到UIKit物理引擎类中
animator.addBehavior(gravity);
上述代码中的angle
是重力行为的角度属性,angle
的值为0时,方块会水平向右移动,随着值的增大,方块会顺时针改变角度。不过我们要模拟现实中的重力,所以该属性一般不设置,不设置时默认是垂直向下移动。magnitude
是重力行为的速率属性,值越大下降的速度越快,当magnitude
属性的值为0时,方块就不会下降了,所以最小的速率是0.1。
注意:在现实世界中,重力加速度大约是g = 9.80665m/s^2
,就是9.8米每平方秒。根据牛顿第二定律,我们可以使用0.5 * g * Time^2
公式来计算下坠距离。
在UIKit的重力世界中,计算重力加速度的公式是一样的,但是单位有所不同。不是米而是像素,即g = 1000pixels/s^2
,我们同样可以使用牛顿第二定律来计算我们的方块在单位时间内下降的距离。我们只需要知道重力加速度g
越大,坠落速度越快,所以上述代码中的magnitude
属性就差不多是这意思。
不能让我们的方块一坠千里
从目前代码的运行状况知道,我们的方块下降到屏幕底部时丝毫没有停止的意思,直接坠崖而下,看都看不见。我们希望我们的屏幕类似一个盒子一样,方块在盒子中,当下降到底部时就停止,那么我们就需要设置一个边界。
在ViewController.swift
文件中再添加一个属性:
// 碰撞行为
var collision: UICollisionBehavior!;
然后在viewDidLoad
方法中加入以下几行代码:
// 实例化碰撞行为类,目前只作用于刚才创建的正方形View
collision = UICollisionBehavior(items: [square]);
// 将参考视图的边界作为碰撞边界
collision.translatesReferenceBoundsIntoBoundary = true;
// 将碰撞行为添加到UIKit物理引擎类中
animator.addBehavior(collision);
上面的代码创建了边界行为,它会将一个或多个边界与指定的View联系起来,并使两者有具有交互行为。
从上面代码我可以注意到collision.translatesReferenceBoundsIntoBoundary = true;
这行代码,它的意思是将UIDynamicAnimator
引用的View的边界作为碰撞行为的触发边界,这样就不用我们再去设置边界的坐标了,非常好用。
接着我们编译运行看看,此时小方块坠落到屏幕底部时会产生碰撞效果,并且还会反弹几下,是不是很逼真呢。
碰撞行为进阶应用
接下来我们在屏幕中再添加一个View,长方形并横在屏幕中间,在// 创建一个长方形View,颜色设置为红色,加入当前的View中
let barrier = UIView(frame:CGRect(x: 0, y: 300, width: 140, height: 20));
barrier.backgroundColor = UIColor.redColor();
self.view.addSubview(barrier);
但是我们发现这个红色的障碍物和方块并没有任何交互行为,这里有一点很重要:只有被行为关联起来的Views才会具有交互行为。我们来看看下面这张关系图:
UIDynamicAnimator
引擎引用了当前屏幕的view,该view给引擎提供了整个坐标系统。每个行为可以关联多个元素,每个元素都可以被多个行为关联,上面的关系图可以很清晰的表现出当前app中各个行为与元素的关联关系。由于所有行为都没有与长方形view关联,所以barrier
基本可以被忽略。
唤醒Barrier
如果我们想让barrier
活起来,我们得让碰撞行为将它关联起来:
// 实例化碰撞行为类,目前作用于刚才创建的正方形View和长方形View
collision = UICollisionBehavior(items: [square,barrier]);
将barrier
添加到碰撞行为关联元素的数组中。这样的话,square
和barrier
这两个view都会有碰撞行为,所以当他们相撞时就会产生碰撞效果。
编译并运行,让我们来看看睡醒的barrier
会产生什么效果:
我们再来看看现在app中行为与元素的关系图:
来看看我给这个红色长方形view起的名字:barrier
,没错,障碍物。但是它现在是一个称职的障碍物吗?显然不是,当它与蓝绿色正方形碰撞时,它会于正方形一起旋转坠落。
等等,为什么barrier
坠落到屏幕底部后不像square
会有蹦跶一下然后停止,反而在缓慢旋转,感觉向失重一样。嗯哼,这是因为我们并没有将重力行为与barrier
关联起来,所以它会向失重一样开始旋转。
这显然不是我想要的结果,我希望square
坠落碰到barrier
后,barrier
纹丝不动,然后square
被撞的七荤八素。我们要拦住那个square
!
隐形的边界与碰撞效果
我们先去掉barrier
的碰撞效果,将:
collision = UICollisionBehavior(items: [square,barrier]);
改为:
collision = UICollisionBehavior(items: [square]);
然后在这行下面添加另外一行代码:
collision = UICollisionBehavior(items: [square]);
collision.addBoundaryWithIdentifier("barrier", forPath: UIBezierPath(rect: barrier.frame));
这行代码的意思是在碰撞效果中添加一个隐形的边界,它的位置和形状和barrier
的一样。这样就会让人产生一种错觉,认为红色的barrier
成为了边界,其实真正的边界隐藏在它之后。编译运行一下,我们来看看会发生什么:
我们看到了当蓝绿色方块下降碰到红色障碍物时产生了碰撞行为,蓝绿色方块弹开并翻转落下。
从上面内容我们了解到,Swift的UIKit动力引擎非常强大,通过简短的几行代码就可以帮助我们实现惊艳的实现行为。下篇文章会向大家详细的介绍碰撞行为背后的故事。
参考原文:UIKit Dynamics Tutorial in Swift
碰撞背后的故事
在UIKit的动态引擎中,每个行为都有一个action
属性,它的类型是一个函数(() -> Void),我们可以使用一个闭包来打印一下每一步行为的信息。我们在viewDidLoad
方法加入下面这行代码:
collision.action = {
println("\(NSStringFromCGAffineTransform(square.transform)) \(NSStringFromCGPoint(square.center))");
};
上面的代码记录了蓝绿色方块坠落、碰撞时的center
和transform
属性。当编译运行时,在方块下坠的前一秒我们可以在控制台看到如下信息:
[1,1,0] {150,150}
[1,151}
[1,152}
[1,154}
我们可以看到,随着方块的坠落,它的center
属性在不断变化,也就是中点的x坐标没变,y坐标在一直变化,这时方块还没有与barrier
发生碰撞。当方块与红色障碍物barrier
碰撞时,我们可以看到方块的transform
属性也有了变化:
[0.999995500003375,0.0029999955000020251,-0.0029999955000020251,0.999995500003375,250}
[0.99970233476860393,0.02439757894140453,-0.02439757894140453,0.99970233476860393,0] {151,249}
[0.99894218654750844,0.045983779049604996,-0.045983779049604996,0.99894218654750844,0] {152,249}
通过这些信息我们得知,动态引擎通过不断改变ransform
和center
两个数据模型的数据来驱动View的行为。
虽然这些数据的精确度可能不是很高,但关键在于这些数据让我们知道了动力引擎是如何进行驱动的,通过这些数据也让我们知道了在行为动作背后也是有据可循的。因此,我们能不能通过程序改变这些数据呢,也就是从另一个层面来控制物体的行为动作。如果是这样的话,那么就需要我们自己计算出一套运行轨迹和行为的数据,而不是通过动作引擎去控制了。
这里有一个协议,它描述了动作行为的数据模型,那就是UIDynamicItem
,它遵循NSObjectProtocol
协议。UIDynamicItem
协议提供了两个可读写的属性transform
,物体的运动轨迹靠这两个属性来计算。同时还提供了一个只读的属性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秒将其背景色从黄色改为了灰色。我们编译运行一下看看:
我们可以看到当方块每次发生碰撞行为时,它都会闪着黄色。
到目前为止,我们所看到的一切都是由动作引擎帮我们实现的,比如说碰撞质量、弹力等,那么接下来,你会看到如何通过UIDynamicItemBehavior
类由我们自己控制这些行为属性。
在let itemBehavIoUr = UIDynamicItemBehavior(items: [square]);
itemBehavIoUr.elasticity = 0.6;
animator.addBehavior(itemBehavIoUr);