5
Nov
2008
 

Core Animation Tutorial: Interrupting Animation Progress

by Matt Long

Starting and stopping animations in Core Animation is as simple as adding and removing your animation from the layer upon which is being run. In this post I am going to talk about how to interrupt animation progress and how to determine whether an animation completed its full run or was interrupted. This is accomplished with the animation delegate -animationDidStop:finished.

Differentiating Keypath From Key

Each core animation layer contains a dictionary that enables you to add and remove animations based on a key that you define. I created a layer-backed view that receives mouse clicks and animates the position of the layer toward that mouse click.

- (void)mouseDown:(NSEvent*)event;
{
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
    [animation setDelegate:self];
    [animation setFromValue:[NSValue valueWithPoint:previousValue]];
    previousValue = NSMakePoint([event locationInWindow].x, [event locationInWindow].y);
    [animation setToValue:[NSValue valueWithPoint:previousValue]];
    [animation setDuration:2.0];
    
    [objectLayer addAnimation:animation forKey:@"follow"];
}

Notice that when I create the animation, I am creating it with a keypath–in this case position. The keypath is different from the key by which we store the animation in the animations dictionary contained by the layer. In the sample code I add the animation to the dictionary using a key called “follow”. This could be any string that is meaningful to keep track of the animation in question. Consider it a tag.

I only make this distinction because I’ve seen some confusion about the difference between the two. To summarize, the keypath specifies which layer property you want to animate while the key is the key by which you will store, recall, and delete an animation from the animation dictionary contained by the layer.

Adding The Animation, Again

In the code above, you’ll also notice that we are adding the animation to the layer for the same key each time we receive a -mouseDown. If we are setting a new object in the dictionary each time, then how are we able to keep track of whether or not the animation finished?

By setting a delegate for the animation, we can be notified when animation has been removed from the animations dictionary. We implement the -animationDidStop:finished delegate.

- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
{
    NSLog(@"Animation interrupted: %@", (!flag)?@"Yes" : @"No");
}

There are several things going on here. When we call [objectLayer addAnimation:animation forKey:@"follow"]; in our -mouseDown code, the animation that was there previously, if there was one, is replaced by the new one. If the user clicked on the screen before the animation completed, the animation is removed and the delegate -animationDidStop gets called. If instead of clicking the mouse again before the current animation is finished, you just let it finish, the animation is also removed from the dictionary because this is the default behavior for an animation. If you don’t specify [animation setRemovedOnCompletion:NO] explicitly, the animation will be removed.

Changing Direction While In Flight

When the view receives a click the basic animation is re-created and added to the layer for the key “follow” again. The fromValue field needs set again, but if you click another spot on the view before the animation finishes, what value do you set it to? If you set it to the previous value, it will simply animate from the previous location which won’t look right. You want it to change direction rather than starting from the previous start location.

If instead of using a basic animation in our example code we had simply opted to use the animator proxy, calling -setPosition on the layer would do this for us. The problem is that we want to be able to demonstrate the -animationDidStop delegate which we can’t do with the animator proxy. So how do we provide this functionality of changing direction in flight?

Look no further than the presentationLayer which is accessible through our layer object. We can obtain the last location of our “position” parameter by first getting the presentationLayer and getting the position from it instead. We’ve modified our -mouseDown event to reflect this change.

- (void)mouseDown:(NSEvent*)event;
{
  CABasicAnimation *animation = 
          [CABasicAnimation animationWithKeyPath:@"position"];
  [animation setDelegate:self];

  // Get the presentationLayer
  CALayer *p = (CALayer*)[objectLayer presentationLayer];
  CGPoint position = [p position];
  NSValue *prevVal = [NSValue valueWithPoint:
                      NSPointFromCGPoint(position)];

  [animation setFromValue:prevVal];
    
  [animation setToValue:[NSValue valueWithPoint:[event locationInWindow]]];
  [animation setDuration:2.0];
  [animation setRemovedOnCompletion:NO];
    
  [objectLayer addAnimation:animation forKey:@"follow"];
}

Now, when we click in our view, the animation changes directions from its current location in the animation. The presentationLayer provides you with current values of whatever field you are currently animating. If a value is not being animated, you will get the same value as what is in the main layer.

Conclusion

It’s very handy to have a way to know when an animation is completed. You simply set your app delegate to be your animation’s delegate and when the animation completes, -animationDidStop:finished gets called conveying through the finished flag whether the animation was interrupted or not. To get our in flight values when the animation hasn’t completed, we simply look at the presentationLayer. These features of Core Animation are very powerful. Given the choice I would like to see the presentationLayer values become KVO compliant as this would let you monitor changes in real time, but being able to read the current value at any given time is a great capability in its own right. It’s very cool technology. Until next time.

xcode.png
Follow Me Demo Project

Comments

Interesting post!

Anybody else getting XCode errors on this project? I’m using XCode 3.0, which is apparently older than the XCode this project was created in.

Jakub Suder says:

Just one question: why do you use NSMakePoint on [event locationInWindow]? Wouldn’t it be enough to just write: NSPoint point = [event locationInWindow] ?

To Kristof: maybe he uses XCode 3.1 (it’s available for download in Apple’s developer center)…

Matt Long says:

@Jakub Suder

I think I was planning to change the point location while I was putting the project together and then ended up not doing so. It’s an artifact. I’ll fix it. Thanks for pointing that out.

-Matt