关于UIKit Dynamics 可以参考的文章有:
http://onevcat.com/2013/06/uikit-dynamics-started/
http://www.cocoachina.com/newbie/basic/2013/0616/6415.html
在开始这个教程之前,先了解一下UIKit Dynamics一些必要的知识。UIKit Dynamics 是 UIKit framework的一部分,使用它时不需要加入额外的框架。它可被应用于UIView 对象和UIView的子类(例如 UIButton、UILabel)。这个库的核心是UIDynamicAnimator类,它负责产生app所需的逼真的动画效果。虽然UIDynamicAnimator是UIKit Dynamics的核心,但是它自己并不做任何事情。其它类的对象必须加入到它里面,这些对象叫做行为,正式的叫做UIDynamicBehaviors。一个UIKit Dynamics behavior 代表着真实世界的物理行为。下面的是与behavior相关的一些类:
UIGravityBehavior: 重力行为
UICollisionBehavior: 碰撞行为
UIPushBehavior: 可以为一个UIView施加一个力的作用,这个力可以是持续的,也可以只是一个冲量。当然我们可以指定力的大小,方向和作用点等等信息。
UIAttachmentBehavior: 描述一个view和一个锚相连接的情况
UISnapBehavior: 将UIView通过动画吸附到某个点上
另外,还有一个重要的类UIDynamicItemBehavior,它有以下的一些属性:
elasticity: 它的取值范围为0.0到1.0,它指定了两个对象或对象和边界之间的碰撞弹性系数的多少。
density: 此属性表示一个物体的质量。其数值越大,物体的质量越大。
resistance: 使用这个属性可以修改一个物体的速度的阻尼
friction: 两个对象间的摩擦系数
angularResistance: 一个对象的角速度的阻尼
allowsRotation: 是否允许旋转
创建一个工程,选择Tabbed Application模板,选择如下:
重力效果
我们将使用一个圆形的UIView对象,它看起来像一个球,在此我们会附上重力行为。
打开Main.storyboard文件,在First View Controller里删除已有的内容。并把bar button item的标题由First改为Ball,如下图所示:
打开FirstViewController.m 文件,导入如下框架:
#import <QuartzCore/QuartzCore.h>
在接口中加入如下的属性说明:
@interface FirstViewController ()
@property (nonatomic, strong) UIDynamicAnimator *animator;
@property (nonatomic, strong) UIView *orangeBall;
@end
声明了一个UIDynamicAnimator对象,来执行动画。一个UIView 对象,表示一个圆形的view。
在viewDidLoad中,初始化这两个对象:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//ball view
self.orangeBall = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 50, 50)];
self.orangeBall.backgroundColor = [UIColor orangeColor];
self.orangeBall.layer.cornerRadius = 25.0;
self.orangeBall.layer.borderColor = [UIColor blackColor].CGColor;
self.orangeBall.layer.borderWidth = 1.0;
[self.view addSubview:self.orangeBall];
//初始化animator
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
}
再声明一个私有方法,如下
- (void)demoGravity
{
UIGravityBehavior *gravityBehavior = [[UIGravityBehavior alloc] initWithItems:@[self.orangeBall]];
[self.animator addBehavior:gravityBehavior];
}
初始化UIGravityBehavior对象时,NSArray对象表示重力效果所要应用的view,这里的view 是 orangeBall。
在viewDidLoad 里调用这个方法:
- (void)viewDidLoad
{
...
...
[self demoGravity];
}
运行模拟器,效果如下:
你回发现,ball view 会一直掉落到屏幕的外边。
每一behavior都已一个action属性,这是一个block,在dynamic动画的过程中会一直执行。
gravityBehavior.action = ^{
NSLog(@"%f", self.orangeBall.center.y);
};
增加碰撞
初始化一个UICollisionBehavior 对象,设置边界
-(void)demoGravity{
…
…
UICollisionBehavior *collisionBehavior = [[UICollisionBehavior alloc] initWithItems:@[self.orangeBall]];
[collisionBehavior addBoundaryWithIdentifier:@"tabbar"
fromPoint:self.tabBarController.tabBar.frame.origin
toPoint:CGPointMake(self.tabBarController.tabBar.frame.origin.x + self.tabBarController.tabBar.frame.size.width, self.tabBarController.tabBar.frame.origin.y)];
[self.animator addBehavior:collisionBehavior];
}
反弹效果看起来不明显,可以改变碰撞的elasticity来解救这个问题。
在demoGravity方法中,加入如下的代码:
UIDynamicItemBehavior *ballBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[self.orangeBall]];
ballBehavior.elasticity = 0.75;
[self.animator addBehavior:ballBehavior];
elasticity值的范围在0.0至1.0之间,0.0意味着完全没有弹性,1.0表示完全弹性碰撞。效果如下:
有时候,在碰撞的过程中需要执行一些额外的动作,因此UIKit提供了UICollisionBehaviourDelegate 协议。它提供了两个item或者一个item和一个边界在碰撞开始和结束的代理方法。
打开FirstViewController.h文件,采用这个协议
@interface FirstViewController : UIViewController <UICollisionBehaviorDelegate>
在FirstViewController.m文件中的 demoGravity方法中,找到下面的代码:
[self.animator addBehavior:collisionBehavior];
在它下面,指定代理对象,如下:
collisionBehavior.collisionDelegate = self;
现在尝试一些代理方法,在碰撞开始的时候,改变ball view的颜色,在碰撞结束的时候,恢复ball view 的颜色,如下:
-(void)collisionBehavior:(UICollisionBehavior *)behavior beganContactForItem:(id)item withBoundaryIdentifier:(id)identifier atPoint:(CGPoint)p{
self.orangeBall.backgroundColor = [UIColor blueColor];
}
-(void)collisionBehavior:(UICollisionBehavior *)behavior endedContactForItem:(id)item withBoundaryIdentifier:(id)identifier{
self.orangeBall.backgroundColor = [UIColor orangeColor];
}
在上面的代理方法中,在碰撞开始的时候,是ball view的背景颜色编程蓝色,当结束的时候,再变成橙色。在模拟器中效果,你可以看到效果。(注意:在下面的demo中,由于质量损失,可能无法正确的显示)。
复杂的情况
增加3个view,作为障碍物。前两个不可移动,位于主视图的左右两边。第三个view能够旋转,当ball view撞上它的时候。
创建一个新的方法来做新的工作,声明如下:
@interface FirstViewController ()
...
...
-(void)playWithBall;
@end
实现如下:
- (void)playWithBall
{
UIView *obstacle1 = [[UIView alloc] initWithFrame:CGRectMake(0.0, 80.0, 120.0, 20.0)];
obstacle1.backgroundColor = [UIColor blueColor];
UIView *obstacle2 = [[UIView alloc] initWithFrame:CGRectMake(170.0, 200.0, 150.0, 20.0)];
obstacle2.backgroundColor = [UIColor blueColor];
UIView *obstacle3 = [[UIView alloc] initWithFrame:CGRectMake(self.view.frame.size.width / 2 - 75,
320.0,
150.0,
20.0)];
obstacle3.backgroundColor = [UIColor blackColor];
[self.view addSubview:obstacle1];
[self.view addSubview:obstacle2];
[self.view addSubview:obstacle3];
}
在viewDidLoad方法中,注释掉 demoGravity方法,并调用新的方法:
- (void)viewDidLoad
{
...
...
//[self demoGravity];
[self playWithBall];
}
在
viewDidLoad方法中,找到下面初始化ball view 的代码
self.orangeBall = [[UIView alloc] initWithFrame:CGRectMake(100.0, 100.0, 50.0, 50.0)];
注释掉它,用下面的代码代替:
self.orangeBall = [[UIView alloc] initWithFrame:CGRectMake(0.0, 10.0, 50.0, 50.0)];
修改后,ball view 的起点将会比左边的第一个障碍物更高。
在前面的例子中,我们给ball 加上了重力行为,因此它能朝底部运动。在这里,我们的新思路是,给动画加上一个UIDynamicPushBehavior。push发生在触摸view的时候,而不是运行app的时候。现在,回到playWithBall方法:
增加一个重力行为:
UIGravityBehavior *gravityBehavior = [[UIGravityBehavior alloc] initWithItems:@[self.orangeBall]];
[self.animator addBehavior:gravityBehavior];
上面的代码,跟上一个例子里的代码一样。现在,增加碰撞行为:
UICollisionBehavior *collisionBehavior = [[UICollisionBehavior alloc] initWithItems:@[self.orangeBall, obstacle1, obstacle2, obstacle3]];
collisionBehavior.translatesReferenceBoundsIntoBoundary = YES;
[collisionBehavior addBoundaryWithIdentifier:@"tabbar"
fromPoint:self.tabBarController.tabBar.frame.origin
toPoint:CGPointMake(self.tabBarController.tabBar.frame.origin.x + self.tabBarController.tabBar.frame.size.width, self.tabBarController.tabBar.frame.origin.y)];
collisionBehavior.collisionMode = UICollisionBehaviorModeEverything;
collisionBehavior.collisionDelegate = self;
[self.animator addBehavior:collisionBehavior];
collisionBehavior.translatesReferenceBoundsIntoBoundary = YES;
是把主视图的边界作为碰撞边界。
collisionBehavior.collisionMode = UICollisionBehaviorModeEverything;
决定了参与的碰撞项目的所有边都会有期望的行为。
现在,碰撞行为已经设定,让我们设置有关ball view和障的一些属性。这里使用的是UIDynamicItemBehavior对象及其属性:
UIDynamicItemBehavior *ballBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[self.orangeBall]];
ballBehavior.elasticity = 0.9;
ballBehavior.resistance = 0.0;
ballBehavior.friction = 0.0;
ballBehavior.allowsRotation = NO;
[self.animator addBehavior:ballBehavior];
至于前两个障碍物,他们的属性是一样的。如前所述,当碰撞发生的时候,我们希望它们保持静止,并且不旋转,不在任何方向上有移动。对于旋转的问题,设置allowsRotation为NO。为阻止任何方向的移动,必须使用density属性来增加质量。这看起来很奇怪,但是把 density设置为1.0,并不能保持障碍物静止。实际上,需要把density设置为很大的值,否则的画我们会发现一些轻微的移动。如下:
UIDynamicItemBehavior *obstacles1And2Behavior = [[UIDynamicItemBehavior alloc] initWithItems:@[obstacle1, obstacle2]];
obstacles1And2Behavior.allowsRotation = NO;
obstacles1And2Behavior.density = 100000.0;
[self.animator addBehavior:obstacles1And2Behavior];
至于第三个障碍物,它的行为正好相反,我们将让它旋转,并且不会增加它的密度。
UIDynamicItemBehavior *obstacle3Behavior = [[UIDynamicItemBehavior alloc] initWithItems:@[obstacle3]];
obstacle3Behavior.allowsRotation = YES;
[self.animator addBehavior:obstacle3Behavior];
最后,当开始触摸屏幕的时候产生一个推力。声明一个私有的属性,来判断ball 是否在移动。
@interface FirstViewController ()
…
…
@property (nonatomic) BOOL isBallRolling;
@end
重写touchesBegan:withEvent: 方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
if (!self.isBallRolling) {
UIPushBehavior *pushBehavior = [[UIPushBehavior alloc] initWithItems:@[self.orangeBall] mode:UIPushBehaviorModeInstantaneous];
pushBehavior.magnitude = 1.5;
[self.animator addBehavior:pushBehavior];
self.isBallRolling = YES;
}
}
如果ball view还没有动,我们就为ball创建推力行为对象。我们设定参数为UIPushBehaviorModeInstantaneous,是为了ball 从一开始就处于最大的速度,而不是逐渐增速。magnitude 属性实际上定义了ball view的速度。angle属性,可以用来定义push 的方向。
运行app,点击屏幕,效果如下:
Want to play?
如果在屏幕的底部增加一个滑动的view,可以把上面的例子编程一个碰撞游戏。
首先,声明一个UIView对象,代表滑板。在声明一个CGPoint属性,来保存滑板中心的位置。
@interface FirstViewController ()
...
...
@property (nonatomic, strong) UIView *paddle;
@property (nonatomic) CGPoint paddleCenterPoint;
@end
返回到playWithBall方法,创建paddle view。找到下面的代码:
[self.view addSubview:obstacle1];
[self.view addSubview:obstacle2];
[self.view addSubview:obstacle3];
在它下面增加如下的代码:
self.paddle = [[UIView alloc] initWithFrame:CGRectMake(self.view.frame.size.width / 2 - 75,
self.tabBarController.tabBar.frame.origin.y - 35.0,
150.0,
30.0)];
self.paddle.backgroundColor = [UIColor greenColor];
self.paddle.layer.cornerRadius = 15.0;
self.paddleCenterPoint = self.paddle.center;
[self.view addSubview:self.paddle];
稍微修改碰撞行为,把paddle加入到数组里,如下:
UICollisionBehavior *collisionBehavior = [[UICollisionBehavior alloc] initWithItems:@[self.orangeBall, self.paddle, obstacle1, obstacle2, obstacle3]];
设置paddle碰撞行为的一些属性,在playWithBall方法里,如下:
UIDynamicItemBehavior *paddleBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[self.paddle]];
paddleBehavior.allowsRotation = NO;
paddleBehavior.density = 100000.0;
[self.animator addBehavior:paddleBehavior];
重写touchesMoved:withEvent方法
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
UITouch *touch = [touches anyObject];
CGPoint touchLocation = [touch locationInView:self.view];
CGFloat yPoint = self.paddleCenterPoint.y;
CGPoint paddleCenter = CGPointMake(touchLocation.x, yPoint);
self.paddle.center = paddleCenter;
[self.animator updateItemUsingCurrentState:self.paddle];
}
这样,paddle在Y轴上一直是个常量,paddle可以随手指移动。重要的是,如果没有调用updateItemUsingCurrentState方法,会看起来没有任何的变化。在touchMove的时候会一直调用并把更新的视觉状态作为参数。
现在,当ball 和 paddle 碰撞的时候,给它们一些额外的推力。使用UICollisionBehaviorDelegate代理。下面的代码片段,是在两个item碰撞时调用,每次两个view碰撞,就加速ball视图。
-(void)collisionBehavior:(UICollisionBehavior *)behavior beganContactForItem:(id)item1 withItem:(id)item2 atPoint:(CGPoint)p{
if (item1 == self.orangeBall && item2 == self.paddle) {
UIPushBehavior *pushBehavior = [[UIPushBehavior alloc] initWithItems:@[self.orangeBall] mode:UIPushBehaviorModeInstantaneous];
pushBehavior.angle = 0.0;
pushBehavior.magnitude = 0.75;
[self.animator addBehavior:pushBehavior];
}
}
运行app,效果如下:
动态的菜单
上一节的例子,使我们熟悉了关于UIKit Dynamic一些概念,除了教学的目的,跳动的ball view 没有任何用处。因此,我认为当你完成这个教程时,能做出有用并可以重复使用的东西,是很重要的。
UIKit Dynamic能提供不错的效果,当展示or影藏一个菜单时,这就是我们将要做的。我的目标是:使用UISwipeGestureRecognizer手势,从左到右,一个menu将会出现在屏幕中。使用同样的手势,从右往左,menu将会关闭。在menu view的后面,有一个半透明的view会覆盖住mainview,当menu影藏的时候,它将会消失。它看起来是这样的:
当然,menu中选项都没有用, 这里我们只关心show/hide。它看起来有点困难,但是别担心,你最终会发现,它会很容易。
在开始之前,我先声明一下。在我们的解决方案中,不会使用额外的view controller,仅仅使用UIView 对象,它很适合一个单独的view controller带有menu菜单的情况。
打开Main.storyboard,在第二个View Controller中,编辑所要展示的信息。
双击Second标题,重命名为Menu:
打开SecondViewController.m,声明一些变量。menu 将用 table view 来展示。UIDynamicAnimator来处理动画。这里声明了一个私有方法,setupMenuView:
@interface SecondViewController ()
@property (nonatomic, strong) UIView *menuView;
@property (nonatomic, strong) UIView *backgroundView;
@property (nonatomic, strong) UITableView *menuTable;
@property (nonatomic, strong) UIDynamicAnimator *animator;
-(void)setupMenuView;
@end
在最后#import下,定义一个宏
#define menuWidth 150.0
现在实现setupMenuView方法:创建backgroun view, 创建menu view,最后创建table view
创建background view,它完全透明
-(void)setupMenuView{
// Setup the background view.
self.backgroundView = [[UIView alloc] initWithFrame:self.view.bounds];
self.backgroundView.backgroundColor = [UIColor lightGrayColor];
self.backgroundView.alpha = 0.0;
[self.view addSubview:self.backgroundView];
}
menu view,初始化时,在屏幕的可见区域外,靠左边:
// Setup the menu view.
self.menuView = [[UIView alloc] initWithFrame:CGRectMake(-menuWidth,
20.0,
menuWidth,
self.view.frame.size.height - self.tabBarController.tabBar.frame.size.height)];
self.menuView.backgroundColor = [UIColor colorWithRed:0.2 green:0.2 blue:0.2 alpha:1.0];
[self.view addSubview:self.menuView];
table view:
// Setup the table view.
self.menuTable = [[UITableView alloc] initWithFrame:self.menuView.bounds
style:UITableViewStylePlain];
self.menuTable.backgroundColor = [UIColor clearColor];
self.menuTable.separatorStyle = UITableViewCellSeparatorStyleNone;
self.menuTable.scrollEnabled = NO;
self.menuTable.alpha = 1.0;
self.menuTable.delegate = self;
self.menuTable.dataSource = self;
[self.menuTable reloadData];
[self.menuView addSubview:self.menuTable];
实现代理:
@interface SecondViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
return 1;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return 5;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"];
}
NSString *menuOptionText = [NSString stringWithFormat:@"Option %d", indexPath.row + 1];
cell.textLabel.text = menuOptionText;
cell.textLabel.textColor = [UIColor lightGrayColor];
cell.textLabel.font = [UIFont fontWithName:@"Futura" size:13.0];
cell.textLabel.textAlignment = NSTextAlignmentCenter;
cell.backgroundColor = [UIColor clearColor];
return cell;
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return 50.0;
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
[[tableView cellForRowAtIndexPath:indexPath] setSelected:NO];
}
在viewDidLoad中,调用setupMenuView方法:
- (void)viewDidLoad
{
[super viewDidLoad];
[self setupMenuView];
}
在这个方法中,初始化 animator 对象:
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
再声明一个私有方法:
@interface SecondViewController ()
…
…
-(void)toggleMenu:(BOOL)shouldOpenMenu;
@end
shouldOpenMenu 用来指定menu 是打开还是关闭。
在我们实现它之前,看看显示菜单视图需要怎样的动态行为。首先,需要一个碰撞行为使它在一个无形的边界停下来,而不是跑到屏幕的外边。然后,需要一个朝右边的重力行为,这样的话menu view 看起来是从边界拉出来的。这两个都要用UIDynamicItemBehavior对象设置弹性系数,另外还可以加上一个推力,这样menu view 能够移动的更快。我们使用同样的行为朝相反的方向来关闭menu。
-(void)toggleMenu:(BOOL)shouldOpenMenu{
[self.animator removeAllBehaviors];
CGFloat gravityDirectionX = (shouldOpenMenu) ? 1.0 : -1.0;
CGFloat pushMagnitude = (shouldOpenMenu) ? 20.0 : -20.0;
CGFloat boundaryPointX = (shouldOpenMenu) ? menuWidth : -menuWidth;
UIGravityBehavior *gravityBehavior = [[UIGravityBehavior alloc] initWithItems:@[self.menuView]];
gravityBehavior.gravityDirection = CGVectorMake(gravityDirectionX, 0.0);
[self.animator addBehavior:gravityBehavior];
UICollisionBehavior *collisionBehavior = [[UICollisionBehavior alloc] initWithItems:@[self.menuView]];
[collisionBehavior addBoundaryWithIdentifier:@"menuBoundary"
fromPoint:CGPointMake(boundaryPointX, 20.0)
toPoint:CGPointMake(boundaryPointX, self.tabBarController.tabBar.frame.origin.y)];
[self.animator addBehavior:collisionBehavior];
UIPushBehavior *pushBehavior = [[UIPushBehavior alloc] initWithItems:@[self.menuView]
mode:UIPushBehaviorModeInstantaneous];
pushBehavior.magnitude = pushMagnitude;
[self.animator addBehavior:pushBehavior];
UIDynamicItemBehavior *menuViewBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[self.menuView]];
menuViewBehavior.elasticity = 0.4;
[self.animator addBehavior:menuViewBehavior];
self.backgroundView.alpha = (shouldOpenMenu) ? 0.5 : 0.0;
}
最开始移除所有的behavior,这样做是因为我们需要使用相反的手势来隐藏menu。
在viewDidLoad中,创建两个手势:
UISwipeGestureRecognizer *showMenuGesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self
action:@selector(handleGesture:)];
showMenuGesture.direction = UISwipeGestureRecognizerDirectionRight;
[self.view addGestureRecognizer:showMenuGesture];
UISwipeGestureRecognizer *hideMenuGesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self
action:@selector(handleGesture:)];
hideMenuGesture.direction = UISwipeGestureRecognizerDirectionLeft;
[self.menuView addGestureRecognizer:hideMenuGesture];
方法handleGesture还没有声明,现在声明:
@interface SecondViewController ()
…
…
-(void)handleGesture:(UISwipeGestureRecognizer *)gesture;
@end
实现如下:
-(void)handleGesture:(UISwipeGestureRecognizer *)gesture{
if (gesture.direction == UISwipeGestureRecognizerDirectionRight) {
[self toggleMenu:YES];
}
else{
[self toggleMenu:NO];
}
}
最终的效果如下:
可以从这里下载源码:
download the complete Xcode project from here