2009-12-08 68 views
0

我有一個通知,當某些屬性更改時,在我的模型中觸發。因此,特定視圖對象中的選擇器捕獲通知以相應地更改視圖的位置。用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"]; 
} 

如何使這種方法部隊多次調用任何建議動畫一步一步執行而不是一次全部執行?謝謝!!

回答

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 /米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]動畫例程。

每個[無論]動畫例程執行適當的動畫,將setAnimationDidStopSelector設置爲animationDidStop:finished:context :.當每個動畫完成時,animationDidStop:finished:context:方法(在檢查是否應立即停止所有動畫之後)將執行隊列中的下一個動畫,方法是將下一個字典拖出隊列並解釋其數據以調用適當做[隨時]動畫方法。如果隊列中沒有動畫,則該例程將animatingPlayer設置爲NO併發布通知,以便其他對象可以知道播放器何時適當地停止了其當前動畫的運行。

就是這樣。可能有一個更簡單的方法(?),但這對我來說工作得很好。如果您有興趣查看實際結果,請查看我在App Store中的Mazin應用。

謝謝。

2

我想你可能想要在這裏做的是設置一個需要連續發生的動畫隊列,並設置動畫委託,以便您將收到animationDidStop:finished:消息。這樣,當一個動畫完成時,您可以在隊列中設置下一個動畫。

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;