UIKit 力学教程

简介: 建议点按下面的标题,跳至原文,以下转载的页面布局太垃圾!UIKit 力学教程 Colin Eberhardt on October 9, 2013这篇文章还可以在这里找到 英语, 俄语, 韩语Ray:这篇教程节选自 iOS 7 教程集,它是 iOS 7 盛宴的一部分,希望你能喜欢。

建议点按下面的标题,跳至原文,以下转载的页面布局太垃圾!

UIKit 力学教程

 Colin Eberhardt on October 9, 2013




这篇文章还可以在这里找到 英语, 俄语, 韩语


Ray:这篇教程节选自 iOS 7 教程集,它是 iOS 7 盛宴的一部分,希望你能喜欢。

你可能已经注意到 iOS 7 中似乎有一些自相矛盾的地方,苹果在建议放弃真实世界的隐喻和拟物化同时,又鼓励创造体验真实的用户界面。

在实践中这意味着什么呢?iOS 7 的设计目标是鼓励创造能像真实的物理对象一样响应触摸、手势和方向变化的数字界面,而不是像素的简单堆砌。最终,区别于形式上的拟物化,让用户与界面产生更为深刻的联系。

这个任务听起来很艰巨,因为做一个看起来很真实的数字界面,要比做一个体验真实的界面简单得多。值得庆幸的是,你有一些很赞的新工具可以帮助你完成这个任务:UIKit 力学(Dynamics)和动态效果(Motion Effects)。

译者注:关于 UIKit Dynamics 的中译名,我与许多开发者有过讨论,有动力、动力模型、动态等译法。但我认为译为力学更为贴切,希望文中出现的力学知识能让你认同我的看法。

  • UIKit 力学是一个集成到 UIKit 的完整的物理引擎。它使你可以通过添加诸如重力、吸附(弹簧)和作用力等行为(behavior),创造体验真实的界面。你只需定义你的界面元素需要遵从的物理特性,剩下的事交给力学引擎处理即可。
  • 动态效果让你能够创造相当酷的视差效果,例如 iOS 7 主屏的动态背景。简单来说,你可以利用手机的加速计提供的数据,开发能够响应手机方向变化的界面。

同时使用动态效果和力学效果,是让数字界面和体验变得栩栩如生的利器。当你的用户看到你的应用以一种自然的、动态的形式响应他们的操作时,就和你的应用产生了更深层次的联系。

开始吧

UIKit 力学非常有意思,最好的学习方法就是从一些小的例子开始。

打开 Xcode,选择 File / New / Project … 然后选择 iOSApplicationSingle View Application 并且命名新的工程为 DynamicsPlayground。创建完工程后,打开 ViewController.m 并且添加下面的代码到 viewDidLoad 的末尾:

UIView* square = [[UIView alloc] initWithFrame:

                                CGRectMake(100, 100, 100, 100)];

square.backgroundColor = [UIColor grayColor];

[self.view addSubview:square];

以上代码简单地添加了一个方块 UIView 到界面上。

编译运行,你可以看到如下图所示方块:


如果你在真机上运行 App,尝试转动手机,上下颠倒,或者摇动它。什么都没发生?那就对了,理应如此。因为当你向界面中添加一个视图的时候,你希望他保持稳定的 frame,直到你添加一些力学行为到界面中。

添加重力

继续编辑 ViewController.m,添加以下实例变量:

UIDynamicAnimator* _animator;

UIGravityBehavior* _gravity;

viewDidLoad 末尾添加以下代码:

_animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];

_gravity = [[UIGravityBehavior alloc] initWithItems:@[square]];

[_animator addBehavior:_gravity];

我稍后再解释这些代码,现在,你只需编译运行你的程序。你应该会看到方块渐渐地加速下坠,直到落到屏幕之外,如下图所示:


在刚添加的代码中,出现了一些力学相关的类:

  • UIDynamicAnimator 是 UIKit 物理引擎。这个类会记录你添加到引擎中的各种行为(比如重力),并且提供全局上下文。当你创建动画实例时,需要传入一个参考视图用于定义坐标系。
  • UIGravityBehavior 把重力的行为抽象成模型,并且对一个或多个元素施加作用力,让你可以建立物理交互模型。当你创建一个行为实例的时候,你需要把它关联到一组元素上,一般是一组视图。这样你就能选择受该行为影响的元素,在这个例子中就是指受重力影响的元素。

大部分行为有一些可配置属性。比如重力行为允许你改变它的角度和量级。尝试修改这些属性使你的物体向上、侧向或斜向以不同的加速度移动。

注意:关于单位:在物理学中,重力(g)单位是米每平方秒,约为 9.8 m/s2。根据牛顿第二定律,你可以用下面公式计算在重力作用下,物体移动的距离:

距离 = 0.5 × g × 时间^2

在 UIKit 力学中,公式依然适用,但单位有所不同。单位中的米每平方秒要用像素每平方秒代替。基于重力参数,应用牛顿第二定律你任然可以计算出视图在任意时间的位置。

你真的需要了解这些么?未必,你只需要知道更大的 g 意味着掉落得更快,但是了解背后的数学原理有利无弊。

设置边界

虽然你看不到,但是当方块消失在屏幕边缘的时候,它其实还在继续下落。为了使它保持在屏幕范围之内,你需要定义一个边界。

ViewController.m 里添加另一个实例变量:

UICollisionBehavior* _collision;

viewDidLoad 末尾加入以下代码:

_collision = [[UICollisionBehavior alloc]

                                      initWithItems:@[square]];

_collision.translatesReferenceBoundsIntoBoundary = YES;

[_animator addBehavior:_collision];

上面的代码创建了一个碰撞行为,定义了一个或多个边界,以决定相关元素之间如何互相影响。

上面的代码没有显式地添加边界坐标,而是设置 translatesReferenceBoundsIntoBoundary 属性为 YES。这意味着用提供给 UIDynamicAnimator 的视图的 bounds 作为边界。

编译并运行,你会看到方块在碰到屏幕底部之后,轻轻弹起,并最终静止,如下图所示:


这是一个很赞的行为,特别是看到如此少的代码量。

处理碰撞

接下来你要添加一个固定的障碍物,他会跟下落的方块碰撞并相互影响。在 viewDidLoad 中添加方块的代码之后加入以下代码:

UIView* barrier = [[UIView alloc] initWithFrame:CGRectMake(0, 300, 130, 20)];

barrier.backgroundColor = [UIColor redColor];

[self.view addSubview:barrier];

编译并运行应用,你可以看到一个红色的“障碍物”横跨在屏幕中间。但是你会发现他没有起到任何作用,方块直接穿过了障碍物:


这显然不是你想要的,这也说明了很重要的一点:力学只影响关联到行为上的视图。

下面是一个简单的示意图:


关联 UIDynamicAnimator 到提供坐标系的参考视图,然后添加一个或多个行为来对关联的物体施加作用力。大部分行为可以与多个物体关联,每个物体可以与多个行为关联。上图展示了当前应用中的行为以及他们的关系。

当前代码里的行为都没有涉及到障碍物,因此在力学引擎中,这个障碍物并不存在。

使物体响应碰撞

为了让方块与障碍物碰撞,找到初始化碰撞行为的代码并用下面的代码替代:

_collision = [[UICollisionBehavior alloc] initWithItems:@[square, barrier]];

碰撞实例需要知道它所影响的每一个视图,因此添加障碍物到列表中使得碰撞对障碍物也有作用。

编译并运行应用,两个物体碰撞并相互影响,如下图所示:


碰撞行为在每个关联的物体周围形成一个边界,使得这些物体从可以互相穿越的物体变成实体无法穿越。

更新一下前面的示意图,现在碰撞行为与两个视图都关联起来了:


但是现在还有一些有出入的地方。我们希望障碍物是不可移动的,但是当前设置下,当两个物体碰撞的时候,障碍物被撞开并且旋转着落向屏幕底部。

更奇怪的是,障碍物从底部弹起后似乎没有趋于静止的意思。这是因为重力没有对障碍物产生影响,这也解释了为什么在方块撞到障碍物之前它没有移动。

你需要另一种解决问题的思路。既然障碍物是不可移动的,那么力学引擎就没有必要知道它的存在。但是如何检测碰撞呢?

不可见的边界和碰撞

把碰撞行为的初始化代码改回原先的样子,使他只知道方块的存在:

_collision = [[UICollisionBehavior alloc] initWithItems:@[square]];

然后,添加如下边界:

// add a boundary that coincides with the top edge

CGPoint rightEdge = CGPointMake(barrier.frame.origin.x +

                                barrier.frame.size.width, barrier.frame.origin.y);

[_collision addBoundaryWithIdentifier:@"barrier"

                            fromPoint:barrier.frame.origin

                              toPoint:rightEdge];

上述代码添加了一个不可见的边界,它正是障碍物的上边界。红色障碍物对用户依然是可见的,但是力学引擎不知道它的存在;相反,添加的边界对于力学引擎是可见的,对于用户是不可见的。当方块下落的时候,看起来与障碍物发生了碰撞,其实它碰到了不可移动的边界。

编译并运行应用,你看到的效果如下图所示:


方块现在从障碍物的边界弹起,旋转,然后继续落到屏幕底部直到静止。

到现在为止,UIKit 力学的强大之处可见一斑:你只需要几行简单的代码就可以实现相当复杂的效果。在这背后有许多复杂的逻辑,在下个章节会涉及力学引擎与应用中物体交互的具体方式。

碰撞的背后

每一个力学行为都有一个 action 属性,你可以定义一个 block 使其在动画的每一步被执行。添加下面的代码到 viewDidLoad

_collision.action =  ^{

    NSLog(@"%@, %@"

          NSStringFromCGAffineTransform(square.transform)

          NSStringFromCGPoint(square.center));

};

上面的代码记录了下落的方块的中点位置核 transform 属性。编译并运行应用,你可以在 Xcode 的控制台中看到调试信息。

在前 400 毫秒左右你会看到类似这样的信息:

2013-07-26 08:21:58.698 DynamicsPlayground[17719:a0b] [1, 0, 0, 1, 0, 0], {150, 236}

2013-07-26 08:21:58.715 DynamicsPlayground[17719:a0b] [1, 0, 0, 1, 0, 0], {150, 243}

2013-07-26 08:21:58.732 DynamicsPlayground[17719:a0b] [1, 0, 0, 1, 0, 0], {150, 250}

可以看到力学引擎在动画过程中不断改变方块的中点位置,或者说它的 frame。

当方块撞到障碍物的时候,它开始旋转,这时候的调试信息类似这样:

2013-07-26 08:21:59.182 DynamicsPlayground[17719:a0b] [0.10679234, 0.99428135, -0.99428135, 0.10679234, 0, 0], {198, 325}

2013-07-26 08:21:59.198 DynamicsPlayground[17719:a0b] [0.051373702, 0.99867952, -0.99867952, 0.051373702, 0, 0], {199, 331}

2013-07-26 08:21:59.215 DynamicsPlayground[17719:a0b] [-0.0040036771, 0.99999201, -0.99999201, -0.0040036771, 0, 0], {201, 338}

你可以看到力学引擎根据物理模型计算并同时改变 transform 属性和 frame 属性来定位视图。

虽然这些属性的具体取值没什么意思,但是很重要的一点是他们每时每刻都在改变。因此如果你尝试用代码改变物体的 frame 或者 transform 属性,这些值会被覆盖。这意味着当你的物体受力学引擎控制的时候,你不能通过 transform 来缩放你的物体。

力学行为的方法名里用的是 items 而不是 views,这是因为想要使用力学行为的对象只需实现 UIDynamicItem 协议即可,定义如下:

@protocol UIDynamicItem 

 

@property (nonatomic, readwrite) CGPoint center;

@property (nonatomic, readonly) CGRect bounds;

@property (nonatomic, readwrite) CGAffineTransform transform;

 

@end

UIDynamicItem 协议为力学引擎提供了读写 center 和 transform 属性的权限,使它可以根据内部的计算结果移动物体。同时提供了 bounds 的读权限,用以确定物体的大小,这不但在计算物体边界的时候被用到,同时在物体受力时用于计算物体的质量。

这个协议说明力学引擎与 UIView并不耦合,其实 UIKit 中还有一个类也实现了这个协议 – UICollectionViewLayoutAttributes。所以可以通过力学引擎对 collection views 实现动画效果。

碰撞通知

到现在,你添加了一些视图和行为,然后让力学引擎接手剩下的任务。在接下来的内容中你会看到如何接收物体碰撞时的通知。

打开 ViewController.m 并且实现 UICollisionBehaviorDelegate 协议:

@interface ViewController () 

 

@end

还是在 viewDidLoad 中,在初始化碰撞行为后,设置当前 view controller 为其代理(delegate),代码如下:

_collision.collisionDelegate = self;

然后,添加一个碰撞行为的代理方法:

- (void)collisionBehavior:(UICollisionBehavior *)behavior beganContactForItem:(id)item 

            withBoundaryIdentifier:(id)identifier atPoint:(CGPoint)p {

    NSLog(@"Boundary contact occurred - %@", identifier);

}

当碰撞发生的时候,这个代理方法会被调用并且在控制台打印出调试信息。为了避免控制台的信息太乱,你可以删除之前在 _collision.action 里添加的调试信息。

编译运行,物体相互碰撞,你会在控制台看到如下信息:

2013-07-26 08:44:37.473 DynamicsPlayground[18104:a0b] Boundary contact occurred - barrier

2013-07-26 08:44:37.689 DynamicsPlayground[18104:a0b] Boundary contact occurred - barrier

2013-07-26 08:44:38.256 DynamicsPlayground[18104:a0b] Boundary contact occurred - (null)

2013-07-26 08:44:38.372 DynamicsPlayground[18104:a0b] Boundary contact occurred - (null)

2013-07-26 08:44:38.455 DynamicsPlayground[18104:a0b] Boundary contact occurred - (null)

2013-07-26 08:44:38.489 DynamicsPlayground[18104:a0b] Boundary contact occurred - (null)

2013-07-26 08:44:38.540 DynamicsPlayground[18104:a0b] Boundary contact occurred - (null)

从调试信息中可以看到方块碰了两次障碍物,也就是之前添加的不可见的边界。(null) 则是指参考视图的边界。

这些调试信息读起来很有意思(认真的),但是如果能在碰撞时触发一些视觉反馈,那就更有意思了。

在输出调试信息的代码之后添加如下代码:

UIView* view = (UIView*)item;

view.backgroundColor = [UIColor yellowColor];

[UIView animateWithDuration:0.3 animations:^{

    view.backgroundColor = [UIColor grayColor];

}];

上述代码会改变碰撞的物体的背景色为黄色,然后渐变回灰色。

编译运行,看一下实际效果:


每次方块与边界发生碰撞的时候,它都会闪现黄色。

UIKit 力学会根据物体的 bounds 计算并自动设置它们的物理属性(如质量或弹性系数)。接下来你会看到如何使用 UIDynamicItemBehavior 类控制这些物理属性。

设置物体属性

viewDidLoad 的末尾,添加下面的代码:

UIDynamicItemBehavior* itemBehaviour = [[UIDynamicItemBehavior alloc] initWithItems:@[square]];

itemBehaviour.elasticity = 0.6;

[_animator addBehavior:itemBehaviour];

上面的代码创建了一个物体行为(item behavior),把它关联到方块,然后添加该行为到动画实例(animator)。弹性系数属性(elasticity)控制物体的弹性,取 1.0 表示完全弹性碰撞,也就是说碰撞中没有能量或速度的损失。你刚刚设置了方块的弹性系数为 0.6,意味着方块在每次弹起的时候速度都会减慢。

编译运行应用,你会发现现在的方块比之前更有弹性,如下:


注: 如果你想知道我是如何生成如上图片来展现方块的历史位置,其实相当简单!我给行为的 action 属性添加了一个简单的 block,每执行五次,用当前方块的中点位置和 transform 属性,添加一个新的方块到当前视图。

在上面的代码中,你只改变了物体的弹性系数,然后物体的行为类还有很多其他可以调整的属性。有下列属性:

  • elasticity(弹性系数) – 决定了碰撞的弹性程度,比如碰撞时物体的弹性。
  • friction(摩擦系数) – 决定了沿接触面滑动时的摩擦力大小。
  • density(密度) – 跟 size 结合使用,来计算物体的总质量。质量越大,物体加速或减速就越困难。
  • resistance(阻力) – 决定线性移动的阻力大小,这根摩擦系数不同,摩擦系数只作用于滑动运动。
  • angularResistance(转动阻力) – 决定转动运动的阻力大小。
  • allowsRotation(允许旋转) – 这个属性很有意思,它在真实的物理世界没有对应的模型。设置这个属性为 NO 物体就完全不会转动,无力受到多大的转动力。

动态添加行为

现在的情况下,你的应用设置系统的所有行为,然后由力学引擎处理系统的物理行为,直至所有物体静止。在下一步中,你会看到如何动态添加或删除行为。

打开 ViewController.m 并添加如下实例变量:

BOOL _firstContact;

添加下面的代码到碰撞代理方法 collisionBehavior:beganContactForItem:withBoundaryIdentifier:atPoint: 的末尾:

if (!_firstContact)

{

    _firstContact = YES;

 

    UIView* square = [[UIView alloc] initWithFrame:CGRectMake(30, 0, 100, 100)];

    square.backgroundColor = [UIColor grayColor];

    [self.view addSubview:square];

 

    [_collision addItem:square];

    [_gravity addItem:square];

 

    UIAttachmentBehavior* attach = [[UIAttachmentBehavior alloc] initWithItem:view

                                                               attachedToItem:square];

    [_animator addBehavior:attach];

}

上面的代码检测到方块和障碍物的第一次接触时,创建第二个方块并添加到碰撞和重力行为中。此外,设置了一个吸附行为,实现两个物体之间加入虚拟的弹簧的效果。

编译运行应用,当原有的方块撞到障碍物时,你应该会看到一个新的方块出现,如下:


虽然两个方块看起来被连接到一起,但是因为没有在屏幕上画线条或是弹簧,你并不会看到视觉上的联系。

接下来做什么?

现在你应该比较了解 UIKit 力学的核心概念了。

如果你有兴趣学习更多关于 UIKit 力学的内容,可以关注我们的新书 iOS 7 教程集。书中结合你在本文学到的内容,有更深入的内容,展示了如何在现实场景中利用 UIKit 力学:


用户可以上拉一个食谱来预览它,当用户松开食谱的时候,它会落回菜单中,或是停靠在屏幕顶部。最终的成品是一个有真实物理体验的应用。

我希望你喜欢这个 UIKit 力学教程 – 我们觉得这很酷,并且期待看到你在应用中付诸有创意的交互。如果你有任何问题或评论,请加入下面的论坛讨论!

你在这个教程中创建的 Dynamics Playground 工程的完整代码可以在 github 上找到,文中每一步编译运行都对应一次提交。



Colin Eberhardt

Colin Eberhardt has been writing code and tutorials for many years, covering a wide range of technologies and platforms. Most recently he has turned his attention to iOS. Colin is CTO of ShinobiControls, creators of charts, grids and other powerful iOS controls.

You can check out their app,
ShinobiPlay, in the App Store.




目录
相关文章