使用UIKit Dynamics创建动画效果

原文地址:http://www.appcoda.com/intro-uikit-dynamics-tutorial/

关于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















评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值