使用CABasicAnimation强制连续动画

时间:2009-12-08 05:10:57

标签: objective-c core-animation cabasicanimation

当某些属性发生变化时,我会在模型中触发通知。因此,特定视图对象中的选择器会捕获通知以相应地更改视图的位置。

通知会导致窗口上的视图沿特定方向移动(始终垂直或水平移动,并始终按窗口上的标准步长移动)。用户操作可能会导致多个通知一个接一个地触发。例如,可以发送3个通知以将视图向下移动三个步骤,然后可以发送另外两个通知以将视图移动到正确的两个步骤。

问题在于,当我执行动画时,它们不会连续发生。因此,在前面的示例中,虽然我希望视图在三个空格中缓慢移动,然后在通知的情况下移动两个空格,而不是最终沿对角线移动到新位置。

以下是我的两个选择器的代码(请注意,placePlayer根据模型中的当前信息设置视图的位置):

- (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"];
}

有关如何对此方法进行多次调用的任何建议都会强制动画逐步执行而不是一次执行?谢谢!

3 个答案:

答案 0 :(得分:2)

我认为你可能想要做的是设置一个动画队列,需要连续发生并设置动画委托,这样你就会收到animationDidStop:finished:消息。这样,当一个动画完成后,您可以设置队列中的下一个动画。

答案 1 :(得分:1)

我实施的解决方案确实使用了队列。这是一个非常完整的描述:

这一切都在名为PlayerView的视图类中完成。在标题中我包含以下内容:

#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

另外,NSMutableArray + QueueAdditions.h / m中的QueueAdditions类别如下所示:

@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

接下来,在PlayerView的实现中,我有以下内容:

#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

说明:

视图订阅来自模型对象(播放器)的适当通知。当它捕获通知时,它会检查它是否已经在做动画(使用animatingPlayer属性)。如果是这样,它将从通知中获取信息(注意播放器应该如何设置动画),将该信息放入字典中,并将该字典添加到动画队列中。如果当前没有动画,该方法会将animatingPlayer设置为true并调用适当的do [Whatever]动画例程。

每个[Whatever]动画例程执行正确的动画,将setAnimationDidStopSelector设置为animationDidStop:finished:context:。当每个动画结束时,animationDidStop:finished:context:方法(在检查是否应该立即停止所有动画之后)将通过将下一个字典拉出队列并解释其数据来执行队列中的下一个动画以便调用适当做[Whatever]动画方法。如果队列中没有动画,则该例程将animatingPlayer设置为NO并发布通知,以便其他对象可以知道玩家何时适当地停止了当前的动画运行。

就是这样。可能有一个更简单的方法(?),但这对我来说非常好。如果您有兴趣查看实际结果,请在App Store中查看我的Mazin应用程序。

感谢。

答案 2 :(得分:0)

您应该考虑在数组中沿动画路径提供多个点,如下所示。

下面的示例指定沿y轴的多个点,但您也可以指定希望动画遵循的贝塞尔曲线路径。

基本动画和关键帧动画之间的主要区别在于,关键帧允许您沿路径指定多个点。

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;