Repeating an animation at changing speeds

I recently came across the following problem: I wanted to handle a repeated animation that would continuously change its speed over time. In particular, I’m building a game with a flashing square, and I want it to flash more quickly if the player is getting closer to dying. Conceptually, this is very easy, and it’s tempting to use the speed property of CALayer or CAAnimation to help — but don’t use that! It’s not that easy.

The problem

The problem is that speed is meant to be manipulated outside of animations – you set it up before you start the animation, not during. If you try to change the speed of a CAAnimation while it’s happening, your app will barf (in the debug logs). If you change the speed of your CALayer, your app won’t barf, but your animation will jump. I think what’s happening is analogous to the reason the functions sin(x) and sin(2x) will be different-speed versions of each other, yet if you suddenly switch from one to the other, you can easily get a discontinuity (aka a jump):

A solution

I spent a while thinking about ways to address this. I was very tempted to figure out how far into the animation the view was, then mess with other CAMediaTiming properties to place the speed-changed animation at the right offset to continue seamlessly (basically match up the phase shifts of sin(x) and sin(2x) to eliminate the jump). I think this approach can be done. But!! Fearless readers, do not tread down that path. Bitter, bitter experience has taught me many a time, the priceless adage:

Simple code is better.

Messing with phase shifts is never simple. So, I opted instead to forego Core Animation’s repeatCount mechanism, and simply restart the animation after each cycle. This way, if I want to alter the speed, I can very easily do it between cycles. Technically, the speed won’t change the instant you want it to, but if the cycles are short, the user won’t notice the difference.

And, to follow another of my favorite adages (tongue in cheek here):

Why write 10 lines of code, when you can write 30 in a more reusable fashion, and a blog post too?

So of course I made a reusable class to handle this situation for anyone facing the same challenge.

It’s called DynamicRepeatAnimation; you use it like this:

- (void)setupDefaultState {
  // Set up the original view state (A).
  label.center = CGPointMake(160, 420);
}

- (void)setupAlternateState {
  // Set up the other view state (B).
  label.center = CGPointMake(160, 100);
}

- (void)startAnimating {
  self.animation = [DynamicRepeatAnimation animationWithDelegate:self];
  animation.oneCycleDuration = 2.0;  // in seconds
  [animation start];  // Starts repeating A <-> B
  // ... later ...
  animation.oneCycleDuration = 0.5;  // smooth transition ensues!
}

The code

This code is more specific than a lot of what’s in the moriarty library, so I decided to just leave this class attached to this post, and not cross-listed anywhere else. Open source, Apache 2 license. Here’s the source:

  • DynamicRepeatAnimation.h
  • DynamicRepeatAnimation.m
  • Dynamic Repeat Animation sample Xcode project
  • And, for the curious, a code listing:

    //
    //  DynamicRepeatAnimation.h
    //
    //  Created by Tyler Neylon on 4/12/11.
    //
    //  A class to manage a repeated animation that
    //  may change speed over time.
    //

    #import

    @protocol DynamicRepeatDelegate

    - (void)setupDefaultState;
    - (void)setupAlternateState;

    @end

    @interface DynamicRepeatAnimation : NSObject {
     @private
      // weak
      id delegate;

      BOOL isAnimating;
      NSTimeInterval oneCycleDuration;
    }

    @property (nonatomic) NSTimeInterval oneCycleDuration;

    + (DynamicRepeatAnimation *)animationWithDelegate:(id)delegate;

    - (void)start;
    - (void)stop;

    @end
    //
    //  DynamicRepeatAnimation.m
    //
    //  Created by Tyler Neylon on 4/12/11.
    //

    #import "DynamicRepeatAnimation.h"

    @interface DynamicRepeatAnimation ()

    - (void)startOneCycle;
    - (void)animationDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context;

    @end

    @implementation DynamicRepeatAnimation

    @synthesize oneCycleDuration;

    + (DynamicRepeatAnimation *)animationWithDelegate:(id)delegate {
      DynamicRepeatAnimation *animation = [[DynamicRepeatAnimation new] autorelease];
      animation-&gt;delegate = delegate;
      animation-&gt;oneCycleDuration = 1.0;
      return animation;
    }

    - (void)start {
      if (isAnimating) return;
      isAnimating = YES;
      [self startOneCycle];
    }

    - (void)stop {
      isAnimating = NO;
    }

    #pragma mark private methods

    - (void)startOneCycle {
      if (!isAnimating || oneCycleDuration == 0.0) return;

      [delegate setupDefaultState];
      [UIView beginAnimations:@"dynamicRepeatAnimation" context:NULL];
      [UIView setAnimationRepeatAutoreverses:YES];
      [UIView setAnimationDuration:(oneCycleDuration / 2.0)];
      [UIView setAnimationDelegate:self];
      [UIView setAnimationDidStopSelector:@selector(animationDidStop:finished:context:)];
      [delegate setupAlternateState];
      [UIView commitAnimations];
    }

    - (void)animationDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context {
      [self startOneCycle];
    }

    @end

    One Trackback

    1. […] I wrote a useful class today that I posted over at the Bynomial iOS blog.  When players are near death (in the game), the top bar flashes at different speeds – faster if they’re closer to dying.  Some of the technical details were interesting to figure out.  Read all about it in the original post. […]

    Post a Comment

    Your email is never published nor shared. Required fields are marked *

    *
    *