Forcing Consecutive Animations with CABasicAnimation
I have a notification that fires in my model when certain properties change. As a result, a selector in a particular view object catc开发者_运维知识库hes the notifications to change the position of the view accordingly.
The notifications causes a view on the window to move in a particular direction (always vertically or horizontally and always by a standard step size on the window). It's possible for the user-actions to cause several notifications to fire one after the other. For example, 3 notifications could be sent to move the view down three steps, then two more notifications could be sent to move the view to the right two steps.
The problem is that as I execute the animations, they don't happen consecutively. So, in the previous example, though I want the view to move slowly down three spaces and then move over two spaces as a result of the notifications, instead it ends up moving diagonally to the new position.
Here is the code for my two selectors (note that placePlayer sets the position of the view according to current information in the model):
- (void)moveEventHandler: (NSNotification *) notification
{
[self placePlayer];
CABasicAnimation* moveAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
moveAnimation.duration = 3;
moveAnimation.fillMode = kCAFillModeForwards; // probably not necessary
moveAnimation.removedOnCompletion = NO; // probably not necessary
[[self layer] addAnimation:moveAnimation forKey:@"animatePosition"];
}
Any suggestions on how to make multiple calls to this methods force animation to execute step-by-step rather than all at once? Thanks!!
I think what you might want to do here is set up a queue of animations that need to happen consecutively and set the animations delegate so that you will receive the animationDidStop:finished: message. This way when one animation is completed you can set off the next one in the queue.
The solution I implemented does indeed use a queue. Here's a pretty complete description:
This is all done in a view class called PlayerView. In the header I include the following:
#import "NSMutableArray+QueueAdditions.h"
@interface PlayerView : UIImageView {
Player* representedPlayer; // The model object represented by the view
NSMutableArray* actionQueue; // An array used as a queue for the actions
bool animatingPlayer; // Notes if the player is in the middle of an animation
bool stoppingAnimation; // Notes if all animations should be stopped (e.g., for re-setting the game)
CGFloat actionDuration; // A convenient way for me to change the duration of all animations
// ... Removed other variables in the class (sound effects, etc) not needed for this example
}
// Notifications
+ (NSString*) AnimationsDidStopNotification;
@property (nonatomic, retain) Player* representedPlayer;
@property (nonatomic, retain, readonly) NSMutableArray* actionQueue;
@property (nonatomic, assign) CGFloat actionDuration;
@property (nonatomic, assign) bool animatingPlayer;
@property (nonatomic, assign) bool stoppingAnimation;
// ... Removed other properties in the class not need for this example
- (void)placePlayer; // puts view where needed (according to the model) without animation
- (void)moveEventHandler:(NSNotification *) notification; // handles events when the player moves
- (void)rotateEventHandler:(NSNotification *) notification; // handles events when the player rotates
// ... Removed other action-related event handles not needed for this example
// These methods actually perform the proper animations
- (void) doMoveAnimation:(CGRect) nextFrame;
- (void) doRotateAnimation:(CGRect)nextFrame inDirection:(enum RotateDirection)rotateDirection;
// ... Removed other action-related methods not needed for this example
// Handles things when each animation stops
- (void) animationDidStop:(NSString*)animationID
finished:(BOOL)finished
context:(void*)context;
// Forces all animations to stop
- (void) stopAnimation;
@end
As an aside, the QueueAdditions category in NSMutableArray+QueueAdditions.h/m looks like this:
@interface NSMutableArray (QueueAdditions)
- (id)popObject;
- (void)pushObject:(id)obj;
@end
@implementation NSMutableArray (QueueAdditions)
- (id)popObject
{
// nil if [self count] == 0
id headObject = [self objectAtIndex:0];
if (headObject != nil) {
[[headObject retain] autorelease]; // so it isn't dealloc'ed on remove
[self removeObjectAtIndex:0];
}
return headObject;
}
- (void)pushObject:(id)obj
{
[self addObject: obj];
}
@end
Next, in the implementation of the PlayerView, I have the following:
#import "PlayerView.h"
#import <QuartzCore/QuartzCore.h>
@implementation PlayerView
@synthesize actionQueue;
@synthesize actionDuration;
@synthesize animatingPlayer;
@synthesize stoppingAnimation;
// ... Removed code not needed for this example (init to set up the view's image, sound effects, actionDuration, etc)
// Name the notification to send when animations stop
+ (NSString*) AnimationsDidStopNotification
{
return @"PlayerViewAnimationsDidStop";
}
// Getter for the representedPlayer property
- (Player*) representedPlayer
{
return representedPlayer;
}
// Setter for the representedPlayer property
- (void)setRepresentedPlayer:(Player *)repPlayer
{
if (representedPlayer != nil)
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[representedPlayer release];
}
if (repPlayer == nil)
{
representedPlayer = nil;
// ... Removed other code not needed in this example
}
else
{
representedPlayer = [repPlayer retain];
if (self.actionQueue == nil)
{
actionQueue = [[NSMutableArray alloc] init];
}
[actionQueue removeAllObjects];
animatingPlayer = NO;
stoppingAnimation = NO;
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(moveEventHandler:)
name:[Player DidMoveNotification]
object:repPlayer ];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(rotateEventHandler:)
name:[Player DidRotateNotification]
object:repPlayer ];
// ... Removed other addObserver actions and code not needed in this example
}
}
// ... Removed code not needed for this example
- (void) placePlayer
{
// Example not helped by specific code... just places the player where the model says it should go without animation
}
// Handle the event noting that the player moved
- (void) moveEventHandler: (NSNotification *) notification
{
// Did not provide the getRectForPlayer:onMazeView code--not needed for the example. But this
// determines where the player should be in the model when this notification is captured
CGRect nextFrame = [PlayerView getRectForPlayer:self.representedPlayer onMazeView:self.mazeView];
// If we are in the middle of an animation, put information for the next animation in a dictionary
// and add that dictionary to the action queue.
// If we're not in the middle of an animation, just do the animation
if (animatingPlayer)
{
NSDictionary* actionInfo = [NSDictionary dictionaryWithObjectsAndKeys:
[NSValue valueWithCGRect:nextFrame], @"nextFrame",
@"move", @"actionType",
@"player", @"actionTarget",
nil];
[actionQueue pushObject:actionInfo];
}
else
{
animatingPlayer = YES; // note that we are now doing an animation
[self doMoveAnimation:nextFrame];
}
}
// Handle the event noting that the player rotated
- (void) rotateEventHandler: (NSNotification *) notification
{
// User info in the notification notes the direction of the rotation in a RotateDirection enum
NSDictionary* userInfo = [notification userInfo];
NSNumber* rotateNumber = [userInfo valueForKey:@"rotateDirection"];
// Did not provide the getRectForPlayer:onMazeView code--not needed for the example. But this
// determines where the player should be in the model when this notification is captured
CGRect nextFrame = [PlayerView getRectForPlayer:self.representedPlayer onMazeView:self.mazeView];
if (animatingPlayer)
{
NSDictionary* actionInfo = [NSDictionary dictionaryWithObjectsAndKeys:
[NSValue valueWithCGRect:nextFrame], @"nextFrame",
@"rotate", @"actionType",
rotateNumber, @"rotateDirectionNumber",
@"player", @"actionTarget",
nil];
[actionQueue pushObject:actionInfo];
}
else
{
enum RotateDirection direction = (enum RotateDirection) [rotateNumber intValue];
animatingPlayer = YES;
[self doRotateAnimation:nextFrame inDirection:direction];
}
}
// ... Removed other action event handlers not needed for this example
// Perform the actual animation for the move action
- (void) doMoveAnimation:(CGRect) nextFrame
{
[UIView beginAnimations:@"Move" context:NULL];
[UIView setAnimationDuration:actionDuration];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:@selector(animationDidStop:finished:context:)];
self.frame = nextFrame;
[UIView commitAnimations];
}
// Perform the actual animation for the rotate action
- (void) doRotateAnimation:(CGRect)nextFrame inDirection:(enum RotateDirection)rotateDirection
{
int iRot = +1;
if (rotateDirection == CounterClockwise)
{
iRot = -1;
}
[UIView beginAnimations:@"Rotate" context:NULL];
[UIView setAnimationDuration:(3*actionDuration)];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:@selector(animationDidStop:finished:context:)];
CGAffineTransform oldTransform = self.transform;
CGAffineTransform transform = CGAffineTransformRotate(oldTransform,(iRot*M_PI/2.0));
self.transform = transform;
self.frame = nextFrame;
[UIView commitAnimations];
}
- (void) animationDidStop:(NSString*)animationID
finished:(BOOL)finished
context:(void *)context
{
// If we're stopping animations, clear the queue, put the player where it needs to go
// and reset stoppingAnimations to NO and note that the player is not animating
if (self.stoppingAnimation)
{
[actionQueue removeAllObjects];
[self placePlayer];
self.stoppingAnimation = NO;
self.animatingPlayer = NO;
}
else if ([actionQueue count] > 0) // there is an action in the queue, execute it
{
NSDictionary* actionInfo = (NSDictionary*)[actionQueue popObject];
NSString* actionTarget = (NSString*)[actionInfo valueForKey:@"actionTarget"];
NSString* actionType = (NSString*)[actionInfo valueForKey:@"actionType"];
// For actions to the player...
if ([actionTarget isEqualToString:@"player"])
{
NSValue* rectValue = (NSValue*)[actionInfo valueForKey:@"nextFrame"];
CGRect nextFrame = [rectValue CGRectValue];
if ([actionType isEqualToString:@"move"])
{
[self doMoveAnimation:nextFrame];
}
else if ([actionType isEqualToString:@"rotate"])
{
NSNumber* rotateNumber = (NSNumber*)[actionInfo valueForKey:@"rotateDirectionNumber"];
enum RotateDirection direction = (enum RotateDirection) [rotateNumber intValue];
[self doRotateAnimation:nextFrame inDirection:direction];
}
// ... Removed code not needed for this example
}
else if ([actionTarget isEqualToString:@"cell"])
{
// ... Removed code not needed for this example
}
}
else // no more actions in the queue, mark the animation as done
{
animatingPlayer = NO;
[[NSNotificationCenter defaultCenter]
postNotificationName:[PlayerView AnimationsDidStopNotification]
object:self
userInfo:[NSDictionary dictionaryWithObjectsAndKeys: nil]];
}
}
// Make animations stop after current animation by setting stopAnimation = YES
- (void) stopAnimation
{
if (self.animatingPlayer)
{
self.stoppingAnimation = YES;
}
}
- (void)dealloc {
if (representedPlayer != nil)
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
[representedPlayer release];
[actionQueue release];
// …Removed other code not needed for example
[super dealloc];
}
@end
Explanation:
The view subscribes to appropriate notifications from the model object (player). When it captures a notification, it checks to see if it is already doing an animation (using the animatingPlayer property). If so, it takes the information from the notification (noting how the player is supposed to be animated), puts that information into a dictionary, and adds that dictionary to the animation queue. If there is no animation currently going, the method will set animatingPlayer to true and call an appropriate do[Whatever]Animation routine.
Each do[Whatever]Animation routine performs the proper animations, setting a setAnimationDidStopSelector to animationDidStop:finished:context:. When each animation finishes, the animationDidStop:finished:context: method (after checking whether all animations should be stopped immediately) will perform the next animation in the queue by pulling the next dictionary out of the queue and interpreting its data in order to call the appropriate do[Whatever]Animation method. If there are no animations in the queue, that routine sets animatingPlayer to NO and posts a notification so other objects can know when the player has appropriately stopped its current run of animations.
That's about it. There may be a simpler method (?) but this worked pretty well for me. Check out my Mazin app in the App Store if you're interested in seeing the actual results.
Thanks.
You should think about providing multiple points along the animation path in an array as shown below.
The example below specifies multiple points along the y-axis but you can also specify a bezier path that you want your animation to follow.
The main difference between basic animation and key-frame animation is that Key-frame allows you to specify multiple points along the path.
CAKeyframeAnimation *downMoveAnimation;
downMoveAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.translation.y"];
downMoveAnimation.duration = 12;
downMoveAnimation.repeatCount = 1;
downMoveAnimation.values = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:20],
[NSNumber numberWithFloat:220],
[NSNumber numberWithFloat:290], nil];
downMoveAnimation.keyTimes = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0],
[NSNumber numberWithFloat:0.5],
[NSNumber numberWithFloat:1.0], nil];
downMoveAnimation.timingFunctions = [NSArray arrayWithObjects:
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn],
// from keyframe 1 to keyframe 2
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut], nil];
// from keyframe 2 to keyframe 3
downMoveAnimation.removedOnCompletion = NO;
downMoveAnimation.fillMode = kCAFillModeForwards;
精彩评论