UIKit Dynamics 教程:抛掷 Views

本文译自:UIKit Dynamics Tutorial: Tossing Views

本文中,我们将学习如何用手势很自然的将 view 抛掷出屏幕。

可能你已经看到该技术在 Tweetbot 中的流行了。

本文非常适合中级开发者,因为对 view 抛掷操作中,使用的技术嵌套了多个效果,例如使用内置的 UIKit 框架对 view 进行旋转 (rotation)、飞离 (fly-away) 动画。

如果你之前还不了解 UIKit dynamics,也不用担心 - 本文会一步一步的引导你进行学习。

事不宜迟,我们开始抛掷 view 吧!

目录:


开始

提醒:学习中,本节是可选的,主要是针对想要从零开始的读者。有经验的 iOS 开发者可以忽略本节,直接到下一节 (已经为你准备好一个工程了)。

打开 Xcode,选择 File\New\Project…,选择 iOS\Application\Single View Application模板,然后点击 Next。将工程命名为 DynamicToss,并将 device family 设置为 iPhone。

现在选中左边的工程名称,并确保选中 Xcode 窗口顶部的 General。在 Deployment Info/Device Orientation 中,不要勾选 ‘Landscape Left’ and ‘Landscape Right’,因为这个程序只以 portrait 模式运行。

接着下载一个图片,这个图片来自gameartguppy.com

将工程解压出来,然后将图片文件添加到工程中。

接着,打开 Main.storyboard,并添加一个 image view 控件,相关值设置为:(X=33, Y=137, Width=254, Height=172, Image=goldfish_feature.jpg)。现在屏幕上的看起来应该是这样的:

接着,拖两个 UIView 到 view controller 中,这两个 UIView 用来对手势进行跟踪。按照下面的值进行设置:

  • View 1: (X=156, Y=219, Width=8, Height=8, Background=red)
  • View 2: (X=80, Y=420, Width=8, Height=8. Background=blue)

至此,view 看起来应该是这样的:

将下面的私有属性添加到 RWTViewController.m 中:

1
2
3
4
5
6
7
8
9
10
11
@property (nonatomic, weak) IBOutlet UIView *image;
@property (nonatomic, weak) IBOutlet UIView *redSquare;
@property (nonatomic, weak) IBOutlet UIView *blueSquare;
@property (nonatomic, assign) CGRect originalBounds;
@property (nonatomic, assign) CGPoint originalCenter;
@property (nonatomic) UIDynamicAnimator *animator;
@property (nonatomic) UIAttachmentBehavior *attachmentBehavior;
@property (nonatomic) UIPushBehavior *pushBehavior;
@property (nonatomic) UIDynamicItemBehavior *itemBehavior;

选中 Main.storyboard 文件,并右键单击 View Controller。拖拽住出现的 blueSquare outlet,将其与蓝色正方形 view 连接上:这个动作会将属性连接至 view 对象。

同样也把红色正方形连接好,最后是名字位 image 的属性。现在 3 个 view 属性应该已经连接好了,看起来如下图所示:

红色和蓝色的正方形代表 UIDynamics 物理引擎对图像进行动画效果的点。

蓝色正方形简单的代表触摸的开始,例如,你的手指与屏幕第一次交互时的位置。红色的正方形用来跟踪手指的移动。

你需要对 UIDynamics 框架进行配置,以使得当移动点时,图片也跟着串联的方式做物理动画。

现在还有一件事件需要做 - 为 view 配置一个手势识别。打开 RWTViewController.m,将下面的方法添加到文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (IBAction) handleAttachmentGesture:(UIPanGestureRecognizer*)gesture
{
CGPoint location = [gesture locationInView:self.view];
CGPoint boxLocation = [gesture locationInView:self.image];
switch (gesture.state) {
case UIGestureRecognizerStateBegan:{
NSLog(@"you touch started position %@",NSStringFromCGPoint(location));
NSLog(@"location in image started is %@",NSStringFromCGPoint(boxLocation));
break;
}
case UIGestureRecognizerStateEnded: {
NSLog(@"you touch ended position %@",NSStringFromCGPoint(location));
NSLog(@"location in image ended is %@",NSStringFromCGPoint(boxLocation));
break;
}
default:
break;
}
}

我们将要添加一个手势识别,来检测拖拽,也称为平移 (panning),当事件发生时,会调用上面这个方法。目前,这个方法简单的显示出手指在两个不同坐标系空间 (view 和 image view) 的位置。

为了添加手势识别,打开 Main.storyboard,并拖一个 Pan Gesture Recognizer 到 view 中。然后从 Pan Gesture Recognizer control-drag 到我们的 View Controller,并将其连接到 handleAttachmentGesture: 方法上。

现在编译并运行程序。在屏幕上轻扫或者拖动,可以在控制台看到类似如下信息:

1
2
3
4
2014-07-13 14:01:49.666 DynamicToss[3999:60b] you touch started position {127, 365}
2014-07-13 14:01:49.667 DynamicToss[3999:60b] location in image started is {94, 228}
2014-07-13 14:01:50.097 DynamicToss[3999:60b] you touch ended position {113, 464}
2014-07-13 14:01:50.097 DynamicToss[3999:60b] location in image ended is {80, 327}

非常棒!现在已经完成了基本的 UI 配置 - 接下来是时候开始 dynamic了!

UIDynamicAnimator 和 UIAttachmentBehavior

提醒:如果你忽略了上一章节的内容,请先从这里下载为你准备好的工程

首先我们要做的事情就是让 image view 跟随我们的拖拽一起移动。这将通过 UIKit Dynamics中的 UIAttachmentBehavior 来实现。

打开 RWTViewController.m,并将下面的代码添加到 viewDidLoad 中,位于 [super viewDidLoad] 下方:

1
2
3
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
self.originalBounds = self.image.bounds;
self.originalCenter = self.image.center;

上面的代码将配置一个 UIDynamicAnimator,这是 UIKit 中的一个引擎,用于支持最基本的物理动画效果。参照 self.view 定义了 animator 的坐标系。

通过给 animator 添加行为 (behaviors),可以让你做一些事情,例如具有重力效果的进行 attaching view、pushing view等。

下面我们开始给 animator 添加第一个行为:UIAttachmentBehavior - 使得 image view 跟着手指的手势进行移动。

为了完成这样的效果,将下面的代码添加到 handleAttachmentGesture: 中,位于 case UIGestureRecognizerStateBegan 的两个 NSLog 语句下方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1
[self.animator removeAllBehaviors];
// 2
UIOffset centerOffset = UIOffsetMake(boxLocation.x - CGRectGetMidX(self.image.bounds),
boxLocation.y - CGRectGetMidY(self.image.bounds));
self.attachmentBehavior = [[UIAttachmentBehavior alloc] initWithItem:self.image
offsetFromCenter:centerOffset
attachedToAnchor:location];
// 3
self.redSquare.center = self.attachmentBehavior.anchorPoint;
self.blueSquare.center = location;
// 4
[self.animator addBehavior:self.attachmentBehavior];

我们来细看一下上述代码的作用:

  1. 首先移除 animator 中所有已有的行为。
  2. 创建一个 UIAttachmentBehavior,将用户 tap image view 上的点连接到一个锚点 (anchor point) 上 (实际上这恰好是完全相同的点)。随后,对锚点的改变,会引发 image view 的移动。

将锚点连接到一个 view 上,就像安装了一个无形的拉杆:将锚点连接到 view 中固定的位置上。

  1. 更新红色正方形,表示出锚点,而蓝色正方形则表示连接到 image view 内部的点 (现在他们都是一样的)。
  2. 将这个行为添加到 animator 中,使其具有效果。

接下来需要告诉锚点跟住你的手指。为此,将下面的代码添加到 handleAttachmentGesture:,位于 fefault (与手势变化一起改变):

1
2
[self.attachmentBehavior setAnchorPoint:[gesture locationInView:self.view]];
self.redSquare.center = self.attachmentBehavior.anchorPoint;

上面的代码将锚点和红色正方形设置位手指当前的位置。当手指移动时,手势识别会调用这个方法,以更新相应的锚点信息。另外,animator 也会自动的按照这个锚点对 view 进行更新。

编译并运行程序,现在可以拖拽 view 了:

当完成拖拽,要是能降 view 还原到之前的位置就更好了。为此,将下面的方法添加到文件中:

1
2
3
4
5
6
7
8
9
10
- (void)resetDemo
{
[self.animator removeAllBehaviors];
[UIView animateWithDuration:0.45 animations:^{
self.image.bounds = self.originalBounds;
self.image.center = self.originalCenter;
self.image.transform = CGAffineTransformIdentity;
}];
}

然后在方法 handleAttachmentGesture: 中调用这个方法,具体位于 UIGestureRecognizerStateEnded

1
[self resetDemo];

编译并运行程序,现在完成对图片的拖拽之后,该图片应该能够回到最开始的位置。

UIPushBehavior

接下来,我们要完成这样的效果:当停止拖拽时,将 view 从屏幕中移除。那么当手势释放时,需要给这个 view 赋予动量,让它能继续沿着轨迹前行。为此,需要使用 UIPushBehavior

首先,定义两个常量,将其添加到文件头部:

1
2
static const CGFloat ThrowingThreshold = 1000;
static const CGFloat ThrowingVelocityPadding = 35;

为了让 view 持续移动,其中 ThrowingThreshold 表示 view 必须移动 (相对于立即返回到原点) 多快才行。ThrowingVelocityPadding 是一个神奇的常量,它将对抛掷 view 的快慢产生影响 (这需要反复试验得出结果)。

最后,在 handleAttachmentGesture:中,用下面的代码替换 UIGestureRecognizerStateEnded case:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
case UIGestureRecognizerStateEnded: {
[self.animator removeBehavior:self.attachmentBehavior];
//1
CGPoint velocity = [gesture velocityInView:self.view];
CGFloat magnitude = sqrtf((velocity.x * velocity.x) + (velocity.y * velocity.y));
if (magnitude > ThrowingThreshold) {
//2
UIPushBehavior *pushBehavior = [[UIPushBehavior alloc]
initWithItems:@[self.image]
mode:UIPushBehaviorModeInstantaneous];
pushBehavior.pushDirection = CGVectorMake((velocity.x / 10) , (velocity.y / 10));
pushBehavior.magnitude = magnitude / ThrowingVelocityPadding;
self.pushBehavior = pushBehavior;
[self.animator addBehavior:self.pushBehavior];
//3
NSInteger angle = arc4random_uniform(20) - 10;
self.itemBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[self.image]];
self.itemBehavior.friction = 0.2;
self.itemBehavior.allowsRotation = YES;
[self.itemBehavior addAngularVelocity:angle forItem:self.image];
[self.animator addBehavior:self.itemBehavior];
//4
[self performSelector:@selector(resetDemo) withObject:nil afterDelay:0.4];
}
else {
[self resetDemo];
}
break;
}

我们来看看代码都做了些什么:

  1. 获得手势的拖拽速度。

利用速度和勾股定理 (Pythagorean theorem),计算出速度的幅度 - 也就是由 x 方向上的速度和 y 方向上的速度构成的三角形斜边。

要想理解背后的理论,请看 Trigonometry for Game Programming tutorial。

  1. 假设手势的幅度超过了最低阀值,那么就配置一个 push behavior。

push behavior 可以是持续性的,或者瞬间的应用到某个对象中。这里,我们需要给 image 一个瞬间的 push 行为。

我们希望 image 的移动方向是由 x 和 y 轴上的速度转换为一个矢量决定的。最后,将 push 行为添加到 animator 上。

  1. 在这里给 image 添加一些旋转效果,让图片飞离屏幕。这里有复杂的数学知识介绍

其中的一些效果取决于发起手势操作的手指与边缘的距离。

在此处,设置不同的值,观察它们移动的效果,然后使用最好的那个值即可。

  1. 指定时间间隔之后,以动画的形式将 imamge 还原到最初状态。

编译并运行程序,现在你应该可以用令人愉快的方式将 view 抛出屏幕!

何去何从?

这里可以下载到本文的最终示例工程

恭喜你,你现在已经学会如何做一些有趣的 UIKit dynamic 动画了 - 让程序的界面效果非常甜蜜。

如果你想要学习更多关于 UIKit Dynamics 的知识,可以阅读 iOS 7 for Tutorials 中两章 UIKit Dynamics 内容。