Monday, November 12, 2012

Animating a View to travel down steps in iPad

Suppose we want to create an animation where a ball appears to travel down a flight of stairs. In this blog, we’ll demonstrate a specific algorithm for moving a view down a series of four steps; a general procedure can be devised from the process that we present. Let’s get started!

Start Xcode, select “Create a new Xcode project,” choose the Single View Application template, and click Next. Name the project Stairs, and select options as shown here:

Click Next, choose a location to save the project, and click Create.

First, we’ll need to add a couple of UIView objects to our app, one to represent the ball, and the other to represent the flight of stairs. Create these objects by navigating to File | New File… and choosing the Objective – C Class template. Click Next. In the “Subclass of” dropdown choose UIView. Name The class “Ball” and click Next. In the next window, click Create to save the class in the project location. Repeat the above steps to create another UIView class called “Steps.” When finished, the navigator should look something like this:

Open the Ball.m file and make the following changes:

#import "Ball.h"

@implementation Ball

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
        self.backgroundColor = [UIColor clearColor];
    }
    return self;
}

- (void)drawRect:(CGRect)rect
{
    // Drawing code
    // Draw a red circle in the frame
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetRGBFillColor(context, 1.0, 0.0, 0.0, 1.0);
    CGContextAddEllipseInRect(context, rect);
    CGContextFillPath(context);
}

@end

In initWithFrame: we are setting a clearColor background, which gives our Ball view a transparent background. In drawRect: we use the function CGContextAddEllipseInRect to create a red ellipse inside the view. This gives us a red ellipse inside a transparent rectangle. Since we’ll later constrain the ball’s width and height to the same value, the result will be a red circle representing a ball.
Now open Steps.h and add two properties as shown here:

#import <UIKit/UIKit.h>

@interface Steps : UIView

@property (nonatomic, assign) float stepWidth, stepHeight;

@end

Because the ball will be traveling down a flight of steps, we must know the width and height of each step.

Open Steps.m and make these changes:

#import "Steps.h"

@implementation Steps

@synthesize stepWidth, stepHeight;

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
        self.backgroundColor = [UIColor clearColor];
        self.stepWidth = self.frame.size.width / 4;
        self.stepHeight = self.frame.size.height / 4;
    }
    return self;
}

- (void)drawRect:(CGRect)rect
{
    // Drawing code
    self.stepWidth = self.bounds.size.width / 4;
    self.stepHeight = self.bounds.size.height / 4;
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetRGBFillColor(context, 0.0, 0.0, 0.0, 1.0);
    CGContextMoveToPoint(context, 0, 0);
    CGContextAddLineToPoint(context, stepWidth * 1, 0);
    CGContextAddLineToPoint(context, stepWidth * 1, stepHeight * 1);
    CGContextAddLineToPoint(context, stepWidth * 2, stepHeight * 1);
    CGContextAddLineToPoint(context, stepWidth * 2, stepHeight * 2);
    CGContextAddLineToPoint(context, stepWidth * 3, stepHeight * 2);
    CGContextAddLineToPoint(context, stepWidth * 3, stepHeight * 3);
    CGContextAddLineToPoint(context, stepWidth * 4, stepHeight * 3);
    CGContextAddLineToPoint(context, stepWidth * 4, stepHeight * 4);
    CGContextAddLineToPoint(context, 0, stepHeight * 4);
    CGContextClosePath(context);
    CGContextFillPath(context);
}

@end

In the initWithFrame: method, we set the background color to transparent, and set the stepHeight and stepWidth to ¼ the height and width of the frame. This ensures that there will be four evenly spaced steps in the view. In the drawRect: method, we just draw a series of steps, then close and fill the polygon using a black color.

Now we turn our attention to the view controller. Open ViewController.h:

#import <UIKit/UIKit.h>
#import "Ball.h"
#import "Steps.h"

@interface ViewController : UIViewController

@property (nonatomic, strong) Ball *ball;
@property (nonatomic, strong) Steps *steps;
@property (nonatomic, strong) NSTimer *movementTimer;

@end

We make properties of the Ball and Steps as well as adding an NSTimer object to assist us in moving the ball. Now open ViewController.m:

#import "ViewController.h"

BOOL isAllowedToDrop[3] = { YES, YES, YES };

@interface ViewController ()

@end

@implementation ViewController

@synthesize ball, steps;
@synthesize movementTimer;

- (Ball *)ball
{
    if (!ball) {
        ball = [[Ball alloc] init];
    }
    return ball;
}

- (Steps *)steps
{
    if (!steps) {
        steps = [[Steps alloc] init];
    }
    return steps;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
    self.steps.frame = CGRectMake(0, 500, self.view.frame.size.width, 300);
    [self.view addSubview:self.steps];
    self.ball.frame = CGRectMake(20, 450, 50, 50);
    [self.view addSubview:self.ball];
   
    self.movementTimer = [NSTimer scheduledTimerWithTimeInterval:0.0125
                                                          target:self
                                                        selector:@selector(moveBall)
                                                        userInfo:nil
                                                         repeats:YES];
}

- (void)moveBall
{
   
    CGFloat ballX = self.ball.frame.origin.x;
    ballX ++;
    CGFloat ballY = self.ball.frame.origin.y;
    if (ballX > self.steps.stepWidth &&
        isAllowedToDrop[0]) {
        [self dropBallWithX:&ballX andY:&ballY atStep:1];
    }
    if (ballX > self.steps.stepWidth * 2 &&
        isAllowedToDrop[1]) {
        [self dropBallWithX:&ballX andY:&ballY atStep:2];
    }
    if (ballX > self.steps.stepWidth * 3 &&
        isAllowedToDrop[2]) {
        [self dropBallWithX:&ballX andY:&ballY atStep:3];
    }
    self.ball.frame = CGRectMake(ballX, ballY,
                                 self.ball.frame.size.width, self.ball.frame.size.height);
}

- (void)dropBallWithX:(CGFloat *) x andY:(CGFloat *) y atStep:(int)step
{
    CGFloat radius = self.ball.frame.size.width / 2;
    CGPoint ballCenter = CGPointMake(self.ball.frame.size.width / 2,
                                     self.ball.frame.size.height / 2);
    CGPoint stepCorner = CGPointMake(self.steps.stepWidth * step,
                                     self.steps.frame.origin.y + (step - 1) * self.steps.stepHeight);
    CGFloat distance = sqrtf((ballCenter.x - stepCorner.x) * (ballCenter.x - stepCorner.x) +
                             (ballCenter.y - stepCorner.y) * (ballCenter.y - stepCorner.y) );
    if (distance > radius && *y < stepCorner.y) {
        *x += 1;
        *y += 4;
    } else {
        *y = self.steps.frame.origin.y + self.steps.stepHeight +
             ((step - 1) * self.steps.stepHeight) - self.ball.frame.size.height;
        isAllowedToDrop[step - 1] = NO;
    }
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

The ball is only allowed to drop off of each step once: for this reason we create a local BOOL array called isAllowedToDrop, and initialize it with three YES values (since the ball has not yet dropped off of any step). Ball and Steps are lazily instantiated, which ensures that they are created the first time their getter methods are called.

In viewDidLoad: we add the steps and the ball as subviews of the main view. Then we start a repeating timer which fires every 0.0125 seconds. Each time this timer fires, the moveBall: method will be called.

The moveBall method first moves the ball one pixel to the right. Then it checks each step, comparing the x origin point of the ball to the edge of the step. In each case, if the x point of the ball is greater than the x point of the edge of the step (and the ball hasn’t dropped off of that step yet), the method dropBallWithX: andY: atStep: is called.

The movement of a ball across the edge of a step can be described as a continuation of horizontal motion while the ball is dropping (y is increasing). In this case, if the distance between the corner of the step and the center of the ball is greater than the radius of the ball, and the y coordinate of the ball is higher than the y coordinate of the step’s corner, the ball is moved down 4 points for every 1 point moved in to the right in the x direction. As soon as the top of the ball falls below the top of the step, the ball is moved to rest on the step and, since this step has been traversed, the BOOL isAllowedToDrop corresponding to the step is set to NO. When the method returns, the ball’s frame is changed according to the new values of ballX and ballY, which makes the ball view appear to move.
Notice that we pass pointers to ballX and ballY to dropBallWithX: andY: atStep:. This is done because we can only return a single value from a function or method. In order to change both the ball’s x and y position, we change the contents of the x and y pointers within dropBallWithX: andY: atStep:. This is a C language convention called pass by address.

We’re done. Run the application and view the result:

As long as the height of the steps is greater than the height of the ball, the app will work as expected, but if the height of the ball is greater than the height of the each step, the ball will appear to go below the bottom of each step before snapping back up and continuing its forward motion. This can be corrected by changing the height of the ball.

Another thing that might add some realism would be to “bounce” the ball as it hits the bottom of each step. Try to do this, and as always, Have Fun!


Source : edumobile[dot]org

0 comments:

Post a Comment