SHERRIE CRANE
SHERRIE CRANE

Reputation: 133

Creating a continuous animation

I want to create an animation that moves up or down the screen according to how fast the user taps the screen. The problem I am having is that I don't know how to create an infinite loop so I am firing a timer which presents issues. Here is my current code.

-(void)setPosOfCider {
    CGFloat originalY = CGRectGetMinY(cider.frame);
    float oY = originalY;
    float posY = averageTapsPerSecond * 100;
    float dur = 0;
    dur = (oY - posY) / 100;
    [UIImageView animateWithDuration:dur animations:^(void) {
        cider.frame = CGRectMake(768, 1024 - posY, 768, 1024);
    }];
}

Suggested fix(Doesn't work):

   - (void)viewDidLoad
{
    [super viewDidLoad];


    // Do any additional setup after loading the view, typically from a nib.
    scroll.pagingEnabled = YES;
    scroll.scrollEnabled = YES;
    scroll.contentSize = CGSizeMake(768 * 3, 1024); // 3 pages wide.
    scroll.delegate = self;

    self.speedInPointsPerSecond = 200000;
    self.tapEvents = [NSMutableArray array];
}

-(void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [self startDisplayLink];
}

-(IBAction)tapped {
    [self.tapEvents addObject:[NSDate date]];

    // if less than two taps, no average speed

    if ([self.tapEvents count] < 1)
        return;

    // only average the last three taps

    if ([self.tapEvents count] > 2)
        [self.tapEvents removeObjectAtIndex:0];

    // now calculate the average taps per second of the last three taps

    NSDate *start = self.tapEvents[0];
    NSDate *end   = [self.tapEvents lastObject];

    self.averageTapsPerSecond = [self.tapEvents count] / [end timeIntervalSinceDate:start];
}

- (void)startDisplayLink
{
    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
    self.lastTime = CACurrentMediaTime();
    [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}

- (CGFloat)yAxisValueBasedUponTapsPerSecond
{
    CGFloat y = 1024 - (self.averageTapsPerSecond * 100.0);

    return y;
}

- (void)handleDisplayLink:(CADisplayLink *)displayLink
{
    CFTimeInterval now = CACurrentMediaTime();
    CGFloat elapsed = now - self.lastTime;
    self.lastTime = now;
    if (elapsed <= 0) return;

    CGPoint center = self.cider.center;
    CGFloat destinationY = [self yAxisValueBasedUponTapsPerSecond];

    if (center.y == destinationY)
    {
        // we don't need to move it at all

        return;
    }
    else if (center.y > destinationY)
    {
        // we need to move it up

        center.y -= self.speedInPointsPerSecond * elapsed;
        if (center.y < destinationY)
            center.y = destinationY;
    }
    else
    {
        // we need to move it down

        center.y += self.speedInPointsPerSecond * elapsed;
        if (center.y > destinationY)
            center.y = destinationY;
    }

    self.cider.center = center;
}

Upvotes: 1

Views: 3702

Answers (2)

Rob
Rob

Reputation: 437402

An even simpler approach is to employ the UIViewAnimationOptionBeginFromCurrentState option when using block-based animations:

[UIView animateWithDuration:2.0
                      delay:0.0
                    options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction
                 animations:^{
                     // set the new frame
                 }
                 completion:NULL];

That stops the current block-based animation and starts the new one from the current location. Note, effective iOS 8 and later, this is now the default behavior, generally obviating the need for the UIViewAnimationOptionBeginFromCurrentState altogether. In fact, the default behavior considers not only the position of the view in question, but also the current velocity, ensuring a smooth transition. See WWDC 2014 video Building Interruptible and Responsive Interactions for more information.)


Another approach would be to have your taps (which adjust the averageTapsPerSecond) to stop any existing animation, grab the view's current frame from the presentation layer (which reflects the state of the view mid-animation), and then just start a new animation:

// grab the frame from the presentation layer (which is the frame mid-animation)

CALayer *presentationLayer = self.viewToAnimate.layer.presentationLayer;
CGRect frame = presentationLayer.frame;

// stop the animation

[self.viewToAnimate.layer removeAllAnimations];

// set the frame to be location we got from the presentation layer

self.viewToAnimate.frame = frame;

// now, starting from that location, animate to the new frame

[UIView animateWithDuration:3.0 delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
    self.viewToAnimate.frame = ...; // set the frame according to the averageTapsPerSecond
} completion:nil];

In iOS 7, you would use the rendition of animateWithDuration with the initialSpringVelocity: parameter, to ensure a smooth transition from the object's current movement to the new animation. See the aforementioned WWDC video for examples.


Original answer:

To adjust a view on the basis of the number of taps per second, you could use a CADisplayLink, which is like a NSTimer, but ideally suited for animation efforts like this. Bottom line, translate your averageTapsPerSecond into a y coordinate, and then use the CADisplayLink to animate the moving of some view to that y coordinate:

#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>

@interface ViewController ()

@property (nonatomic, strong) CADisplayLink *displayLink;
@property (nonatomic) CFTimeInterval lastTime;

@property (nonatomic) CGFloat speedInPointsPerSecond;

@property (nonatomic, strong) NSMutableArray *tapEvents;
@property (nonatomic) CGFloat averageTapsPerSecond;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.speedInPointsPerSecond = 200.0;
    self.tapEvents = [NSMutableArray array];

    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
    [self.view addGestureRecognizer:tap];
}

- (void)handleTap:(UITapGestureRecognizer *)gesture
{
    [self.tapEvents addObject:[NSDate date]];

    // if less than two taps, no average speed

    if ([self.tapEvents count] < 2)
        return;

    // only average the last three taps

    if ([self.tapEvents count] > 3)
        [self.tapEvents removeObjectAtIndex:0];

    // now calculate the average taps per second of the last three taps

    NSDate *start = self.tapEvents[0];
    NSDate *end   = [self.tapEvents lastObject];

    self.averageTapsPerSecond = [self.tapEvents count] / [end timeIntervalSinceDate:start];
}

- (CGFloat)yAxisValueBasedUponTapsPerSecond
{
    CGFloat y = 480 - self.averageTapsPerSecond * 100.0;

    return y;
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    [self startDisplayLink];
}

- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];

    [self stopDisplayLink];
}

- (void)startDisplayLink
{
    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
    self.lastTime = CACurrentMediaTime();
    [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}

- (void)stopDisplayLink
{
    [self.displayLink invalidate];
    self.displayLink = nil;
}

- (void)handleDisplayLink:(CADisplayLink *)displayLink
{
    CFTimeInterval now = CACurrentMediaTime();
    CGFloat elapsed = now - self.lastTime;
    self.lastTime = now;
    if (elapsed <= 0) return;

    CGPoint center = self.viewToAnimate.center;
    CGFloat destinationY = [self yAxisValueBasedUponTapsPerSecond];

    if (center.y == destinationY)
    {
        // we don't need to move it at all

        return;
    }
    else if (center.y > destinationY)
    {
        // we need to move it up

        center.y -= self.speedInPointsPerSecond * elapsed;
        if (center.y < destinationY)
            center.y = destinationY;
    }
    else
    {
        // we need to move it down

        center.y += self.speedInPointsPerSecond * elapsed;
        if (center.y > destinationY)
            center.y = destinationY;
    }

    self.viewToAnimate.center = center;
}

@end

Hopefully this illustrates the idea.

Also, to use the above, make sure you've added the QuartzCore framework to your project (though in Xcode 5, it seems to do that for you).


For standard, constant speed animation, you can use the following:

You would generally do this with an animation that repeats (i.e. UIViewAnimationOptionRepeat) and that autoreverses (i.e. UIViewAnimationOptionAutoreverse):

[UIView animateWithDuration:1.0
                      delay:0.0
                    options:UIViewAnimationOptionAutoreverse | UIViewAnimationOptionRepeat
                 animations:^{
                     cider.frame = ... // specify the end destination here
                 }
                 completion:nil];

For a simple up-down-up animation, that's all you need. No timers needed.

Upvotes: 2

ArturOlszak
ArturOlszak

Reputation: 2663

There is one line you need to add to Rob's answer.

[UIView animateWithDuration:dur delay:del options:UIViewAnimationOptionAutoreverse|UIViewAnimationOptionRepeat animations:^{
    [UIView setAnimationRepeatCount:HUGE_VAL];

    //Your code here

} completion:nil];

If you need to stop that animation, use this line (remember to add QuartzCore framework):

[view.layer removeAllAnimations];

Upvotes: 0

Related Questions