The delayperunit property in my code below keeps the sprite on screen for 2.5 seconds before it changes. What I actually want to do is display the sprite for 0.5 seconds so that there is 2.0 second break (no animation displayed) before the next one appears for another 0.5 seconds and so on. How can I achieve this?
// Create the intro image
CGSize screenSize = [CCDirector sharedDirector].winSize;
CCSprite *introImage = [CCSprite spriteWithFile:#"intro1.png"];
[introImage setPosition:ccp(screenSize.width/2, screenSize.height/2)];
[self addChild:introImage];
// Create the intro animation, and load it from intro1 to intro7.png
CCAnimation *introAnimation = [CCAnimation animation];
[introAnimation delayPerUnit:2.5f];
for (int frameNumber=0; frameNumber < 8; frameNumber++) {
CCLOG(#"Adding image intro%d.png to the introAnimation.",frameNumber);
[introAnimation addSpriteFrameWithFilename:
[NSString stringWithFormat:#"intro%d.png",frameNumber]];
}
I dont think you can do that with a CCAnimation. You could either embed that in a class (if this will be a recurring theme), or do this in your module that displays these frames:
in .h, some ivars;
NSUInteger _currentFrame;
NSUInteger _maxFrames;
in .m, where you are ready to begin:
_currentFrame=1;
_maxFrames = 8;
[self scheduleOnce:#selector(flashFrame) delay:0.];
and
-(void) flashFrame: {
NSString *spriteName = [NSString stringWithFormat:#"intro%d.png",_currentFrame];
CCSprite *sprite = [CCSprite spriteWithFile:spriteName];
CGSize screenSize = [CCDirector sharedDirector].winSize;
sprite.position=ccp(screenSize.width/2, screenSize.height/2);
id wait = [CCDelayTime actionWithDuration:.5];
id clean = [CCCallBlock actionWithBlock:^{
[sprite removeFromParentAndCleanup:YES];
}];
id seq = [CCSequence actions:wait,clean,nil];
[self addChild sprite];
[sprite runAction:seq];
if(_currentFrame < maxFrame) {
_currentFrame++;
[self scheduleOnce:#selector(flashFrame) delay:2.0];
} else {
// do whatever you wanted to do after this sequence.
}
}
standard disclaimer : not tested, coded from memory, mileage will vary :)
Consider having an action composed by a CCSequence of CCAnimation (the one with 0.2 delay per unit) and add a CCDelayTime of 0.5 secs in between the animations. Then Repeat the action forever. Also, use CCAnimationCache to store your animations if you are using Cocos2d 2.x onwards.
Related
I have a pageLeft and pageRight animation that animates the position and alpha of 2 views, making a total of 4 simultaneous CABasicAnimations:
Move next page into the view
Move current page out of the view
Fade in next page
Fade out current page
Everything works well with the animations and setup. this question is about mouse events after the page animation. I have found that after the animation runs, the nextView does not receive mouseDown: events. If the animation is not run because the shouldNotAnimate block is run below instead, the nextView does receive mouseDown: events.
This means that something about the way I have setup my CABasicAnimations and CATransaction is causing this. I am stumped as to what it could be. Any ideas?
Animation Code:
BOOL forward = currentIndex < index;
if (shouldNotAnimate) {
// handles swapping pages faster than the animation duration
[self.sourceViewContainer setSubviews:[NSArray array]];
[nextView.layer removeAllAnimations];
[nextView setFrame:self.sourceViewContainer.bounds];
[nextView.layer setPosition:self.sourceViewContainer.bounds.origin];
[nextView.layer setOpacity:1.0];
[self.sourceViewContainer addSubview:nextView];
[currentView removeFromSuperview];
[currentView.layer removeAllAnimations];
[currentView setFrame:self.sourceViewContainer.bounds];
[currentView.layer setPosition:self.sourceViewContainer.bounds.origin];
[currentView.layer setOpacity:1.0];
self.animationsRunning = NO;
} else {
self.animationsRunning = YES;
[CATransaction begin];
[CATransaction setCompletionBlock:^{
self.animationsRunning = NO;
}];
// Setup incoming push animation
[nextView setFrame:CGRectMake(self.sourceViewContainer.bounds.size.width * (forward ? 1 : -1), 0, self.sourceViewContainer.bounds.size.width, self.sourceViewContainer.bounds.size.width)];
[self.sourceViewContainer addSubview:nextView];
CABasicAnimation *incomingAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
incomingAnimation.fromValue = [NSValue valueWithPoint:nextView.frame.origin];
incomingAnimation.toValue = [NSValue valueWithPoint:self.sourceViewContainer.bounds.origin];
incomingAnimation.duration = PANEL_PAGE_SWIPE_ANIMATION_DURATION;
incomingAnimation.removedOnCompletion = NO;
incomingAnimation.fillMode = kCAFillModeForwards;
incomingAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
incomingAnimation.completion = ^(BOOL finished) {
[nextView.layer setPosition:self.sourceViewContainer.bounds.origin];
[nextView.layer removeAnimationForKey:#"incoming"];
};
[nextView.layer addAnimation:incomingAnimation forKey:#"incoming"];
// Setup outgoing push animation
CGRect offscreen = CGRectMake(self.sourceViewContainer.bounds.size.width * (forward ? -1 : 1), 0, self.sourceViewContainer.bounds.size.width, self.sourceViewContainer.bounds.size.height);
CABasicAnimation *outgoingAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
outgoingAnimation.fromValue = [NSValue valueWithPoint:self.sourceViewContainer.bounds.origin];
outgoingAnimation.toValue = [NSValue valueWithPoint:offscreen.origin];
outgoingAnimation.duration = PANEL_PAGE_SWIPE_ANIMATION_DURATION;
outgoingAnimation.removedOnCompletion = NO;
outgoingAnimation.fillMode = kCAFillModeForwards;
outgoingAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
outgoingAnimation.completion = ^(BOOL finished) {
[currentView removeFromSuperview];
[currentView.layer setPosition:self.sourceViewContainer.bounds.origin];
[currentView.layer removeAnimationForKey:#"outgoing"];
};
[currentView.layer addAnimation:outgoingAnimation forKey:#"outgoing"];
// Setup incoming alpha animation
CABasicAnimation *fadeInAnimation = [CABasicAnimation animationWithKeyPath:#"opacity"];
fadeInAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
fadeInAnimation.toValue = [NSNumber numberWithFloat:1.0f];
fadeInAnimation.duration = PANEL_PAGE_SWIPE_ANIMATION_DURATION;
fadeInAnimation.removedOnCompletion = NO;
fadeInAnimation.fillMode = kCAFillModeForwards;
fadeInAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
fadeInAnimation.completion = ^(BOOL finished) {
[nextView.layer setOpacity:1.0f];
[nextView.layer removeAnimationForKey:#"fadeIn"];
};
[nextView.layer addAnimation:fadeInAnimation forKey:#"fadeIn"];
// Setup outgoing alpha animation
CABasicAnimation *fadeOutAnimation = [CABasicAnimation animationWithKeyPath:#"opacity"];
fadeOutAnimation.fromValue = [NSNumber numberWithFloat:1.0f];
fadeOutAnimation.toValue = [NSNumber numberWithFloat:0.0f];
fadeOutAnimation.duration = PANEL_PAGE_SWIPE_ANIMATION_DURATION;
fadeOutAnimation.removedOnCompletion = NO;
fadeOutAnimation.fillMode = kCAFillModeForwards;
fadeOutAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
fadeOutAnimation.completion = ^(BOOL finished) {
[currentView removeFromSuperview];
[currentView.layer setOpacity:1.0f];
[currentView.layer removeAnimationForKey:#"fadeOut"];
};
[currentView.layer addAnimation:fadeOutAnimation forKey:#"fadeOut"];
[CATransaction commit];
} // Run animations
Solution
I had to explicitly set the view frame in the completion block, even though I had done this immediately prior to the animation calls.
[CATransaction begin];
[CATransaction setCompletionBlock:^{
self.animationsRunning = NO;
[nextView setFrame:self.sourceViewContainer.bounds];
}];
I think I know what's going on.
CAAnimation objects don't actually change the underlying property that they animation. Instead, it applies the properties to a presentation layer, which is drawn instead of the regular layer. When you create an animation with removedOnCompletion=false, the presentation layer is left active after the animation is completed, but the underlying property still isn't changed. Plus, moving a view's layer doesn't move the view itself, so it won't respond to user interaction at it's new location.
I'd suggest moving your views using UIView block animation (UIView animateWithDuration:completion: and it's relatives) instead of using CAAnimation objects. Those methods actually move the view to it's new position, so that it responds to user interaction at the final location once the animation is complete.
You could do your move and alpha change all in one animation block.
I had an issue with iOS once where the UIMapView because unresponsive after an animation due to a call to the map view. Somehow userInteractionEnable was set to FALSE after the animation finished.
What you could do to test if that is the case is to assert that the value for userInteractionEnable is TRUE.
assert(view.userInteractionEnable != FALSE); // ensure user interaction is enabled
You may find that when you run the app that it breaks on this assertion. If it does, you can simply set it to TRUE and your app will work as you expect.
As for my issue with UIMapView, the problem went away after an iOS version update so it was likely a bug caused by an edge case. I filed a bug about the problem which may have helped raise awareness about the unexpected behavior.
I have a custom view in an NSStatusItem object.
This view displays icons. It's also able to show a progress, but you have to call [self.statusitemview setProgressValue:theValue];
I have a set of icons, and it chooses the right one using this value.
This seems very jerky, because the executed process doesn't send updates all the time.
So I would like to animate this.
I would like to call the animation like you can with other cocoa-controls:
[[self.statusItemView animator] setProgressValue:value];
If that's at all possible
What is the proper way to do this?
I wouldn't want to use an NSTimer.
EDIT
The images are drawn using the drawRect: method
Here's the code:
- (void)drawRect:(NSRect)dirtyRect
{
if (self.isHighlighted) {
[self.statusItem drawStatusBarBackgroundInRect:self.bounds withHighlight:YES];
}
[self drawIcon];
}
- (void)drawIcon {
if (!self.showsProgress) {
[self drawIconWithName:#"statusItem"];
} else {
[self drawProgressIcon];
}
}
- (void)drawProgressIcon {
NSString *pushed = (self.isHighlighted)?#"-pushed":#"";
int iconValue = ((self.progressValue / (float)kStatusItemViewMaxValue) * kStatusItemViewProgressStates);
[self drawIconWithName:[NSString stringWithFormat:#"statusItem%#-%d", pushed, iconValue]];
}
- (void)drawIconWithName:(NSString *)iconName {
if (self.isHighlighted && !self.showsProgress) iconName = [iconName stringByAppendingString:#"-pushed"];
NSImage *icon = [NSImage imageNamed:iconName];
NSRect drawingRect = NSCenterRect(self.bounds, icon);
[icon drawInRect:drawingRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0 respectFlipped:YES hints:nil];
}
- (void)setProgressValue:(int)progressValue {
if (progressValue > kStatusItemViewMaxValue || progressValue < 0) {
#throw [NSException exceptionWithName:#"Invalid Progress Value"
reason:[NSString stringWithFormat:#"The value %d id invalid. Range {0 - %d}", progressValue, kStatusItemViewMaxValue]
userInfo:nil];
}
_progressValue = progressValue;
[self setNeedsDisplay:YES];
}
- (void)setShowsProgress:(BOOL)showsProgress {
if (!showsProgress) self.progressValue = 0;
_showsProgress = showsProgress;
[self setNeedsDisplay:YES];
}
It has to be possible somehow.
Since standard controls from Apple are drawn using the drawRect:, but have smooth animations...
To animate custom properties, you need to make your view conform to the NSAnimatablePropertyContainer protocol.
You can then set up multiple custom properties as animatable (in addition to the properties already supported by NSView), and then you can simply use your views' animator proxy to animate the properties:
yourObject.animator.propertyName = finalPropertyValue;
Apart from making animation very simple, it also allows you to animate multiple objects simultaneously using an NSAnimationContext:
[NSAnimationContext beginGrouping];
firstObject.animator.propertyName = finalPropertyValue1;
secondObject.animator.propertyName = finalPropertyValue2;
[NSAnimationContext endGrouping];
You can also set the duration and supply a completion handler block:
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:0.5];
[[NSAnimationContext currentContext] setCompletionHandler:^{
NSLog(#"animation finished");
}];
firstObject.animator.propertyName = finalPropertyValue1;
secondObject.animator.propertyName = finalPropertyValue2;
[NSAnimationContext endGrouping];
For a standard NSView object, if you want to add animation support to a property in your view, you just need to override the +defaultAnimationForKey: method in your view and return an animation for the property:
//declare the default animations for any keys we want to animate
+ (id)defaultAnimationForKey:(NSString *)key
{
//in this case, we want to add animation for our x and y keys
if ([key isEqualToString:#"x"] || [key isEqualToString:#"y"]) {
return [CABasicAnimation animation];
} else {
// Defer to super's implementation for any keys we don't specifically handle.
return [super defaultAnimationForKey:key];
}
}
I've created a simple sample project that shows how to animate multiple properties of a view simultaneously using the NSAnimatablePropertyContainer protocol.
All your view needs to do to update successfully is make sure that setNeedsDisplay:YES is called when any of the animatable properties are modified. You can then get the values of those properties in your drawRect: method and update the animation based on those values.
I have answered similar question here
You can't animate custom properties with animator, but you can write custom animation if you need, though it's not best idea.
Updated (Custom Animation):
- (void) scrollTick:(NSDictionary*) params
{
static NSTimeInterval timeStart = 0;
if(!timeStart)
timeStart = [NSDate timeIntervalSinceReferenceDate];
NSTimeInterval stTime = timeStart;
NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
NSTimeInterval totalTime = [[params valueForKey:#"duration"] doubleValue];
if(currentTime > timeStart + totalTime)
{
currentTime = timeStart + totalTime;
timeStart = 0;
}
double progress = (currentTime - stTime)/totalTime;
progress = (sin(progress*3.14-3.14/2.0)+1.0)/2.0;
NSClipView* clip = [params valueForKey:#"target"];
float startValue = [[params valueForKey:#"from"] floatValue];
float endValue = [[params valueForKey:#"to"] floatValue];
float newValue = startValue + (endValue - startValue)*progress;
[self setProperty:newValue];
if(timeStart)
[self performSelectorOnMainThread:#selector(scrollTick:) withObject:params waitUntilDone:NO];
}
- (void) setAnimatedProperty:(float)newValue
{
NSDictionary* params = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithFloat:self.property], #"from",
[NSNumber numberWithFloat:newValue],#"to",
[NSNumber numberWithFloat:1.0],#"duration",
nil];
[self performSelectorOnMainThread:#selector(scrollTick:) withObject:params waitUntilDone:NO];
}
So the issue here is when I create one animated projectile, everything is fine. As soon as the user creates a second, the first stops animating.
Alright, here's how I'm setting it up in my init:
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:
#"acorns.plist"];
CCSpriteBatchNode *spriteSheet = [CCSpriteBatchNode
batchNodeWithFile:#"acorns.png"];
[self addChild:spriteSheet];
NSMutableArray *flyingFrames = [NSMutableArray array];
for(int i = 1; i <= 4; ++i) {
[flyingFrames addObject:
[[CCSpriteFrameCache sharedSpriteFrameCache] spriteFrameByName:
[NSString stringWithFormat:#"acorn%d.png", i]]];
}
CCAnimation *flying = [CCAnimation
animationWithFrames:flyingFrames delay:0.5f];
self.flying = [CCRepeatForever actionWithAction:
[CCAnimate actionWithAnimation:flying restoreOriginalFrame:NO]];
Then, in my create bullets method which is called when the user taps the screen, I do:
CCSprite *bullet = [CCSprite spriteWithSpriteFrameName:#"acorn1.png"];
bullet.position = CGPointMake(140.0f, FLOOR_HEIGHT+145.0f);
[bullet runAction:_flying];
[self addChild:bullet z:9];
[bullets addObject:bullet];
So the first time the user taps, everything works fine. The second time, an animated bullet is created, but the existing one stops animating, and so on.
I believe each sprite should have its own actions, ie you cant reuse an action while the action is in progress. Something like:
CCSprite *bullet = [CCSprite spriteWithSpriteFrameName:#"acorn1.png"];
bullet.position = CGPointMake(140.0f, FLOOR_HEIGHT+145.0f);
id _fly=[CCAnimation animationWithFrames:flyingFrames delay:0.5f];
id _flyForever = [[CCRepeatForever _fly];
[bullet runAction:_flyForever];
[self addChild:bullet z:9];
[bullets addObject:buller];
The flyingFrames can be referenced by multiple animations, so you must take care of retaining the array for reuse.
I'd like to take a UITextView and allow the user to enter text into it and then trigger a copy of the contents onto a quartz bitmap context. Does anyone know how I can perform this copy action? Should I override the drawRect method and call [super drawRect] and then take the resulting context and copy it? If so, does anyone have any reference to sample code to copy from one context to another?
Update: from reading the link in the answer below, I put together this much to attempt to copy my UIView contents into a bitmap context, but something is still not right. I get my contents mirrored across the X axis (i.e. upside down). I tried using CGContextScaleCTM() but that seems to have no effect.
I've verified that the created UIImage from the first four lines do properly create a UIImage that isn't strangely rotated/flipped, so there is something I'm doing wrong with the later calls.
// copy contents to bitmap context
UIGraphicsBeginImageContext(mTextView.bounds.size);
[mTextView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage* image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[self setNeedsDisplay];
// render the created image to the bitmap context
CGImageRef cgImage = [image CGImage];
CGContextScaleCTM(mContext, 1.0, -1.0); // doesn't seem to change result
CGContextDrawImage(mContext, CGRectMake(
mTextView.frame.origin.x,
mTextView.frame.origin.y,
[image size].width, [image size].height), cgImage);
Any suggestions?
Here is the code I used to get a UIImage of UIView:
#implementation UIView (Sreenshot)
- (UIImage *)screenshot{
UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, [UIScreen mainScreen].scale);
/* iOS 7 */
BOOL visible = !self.hidden && self.superview;
CGFloat alpha = self.alpha;
BOOL animating = self.layer.animationKeys != nil;
BOOL success = YES;
if ([self respondsToSelector:#selector(drawViewHierarchyInRect:afterScreenUpdates:)]){
//only works when visible
if (!animating && alpha == 1 && visible) {
success = [self drawViewHierarchyInRect:self.bounds afterScreenUpdates:NO];
}else{
self.alpha = 1;
success = [self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES];
self.alpha = alpha;
}
}
if(!success){ /* iOS 6 */
self.alpha = 1;
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
self.alpha = alpha;
}
UIImage* img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return img;
}
#end
You can use in iOS 7 and later:
- (UIView *)snapshotViewAfterScreenUpdates:(BOOL)afterUpdates
I'm looking to use Core Animation to simulate a flip clock animation in a Mac application. Currently I have three CALayer's representing the top and bottom half of the digit, and a third used to represent the flip animation (a solution found in the follow article: Creating an iPad flip-clock with Core Animation.
The animation of the flip layer is broken into two stages: flipping from the top to the middle of the digit, and then from the middle to the bottom. To achieve this, I use a delegate function which is called whenever an animation ends:
- (void)animationDidStop:(CAAnimation *)oldAnimation finished:(BOOL)flag
{
int digitIndex = [[oldAnimation valueForKey:#"digit"] intValue];
int currentValue = [[oldAnimation valueForKey:#"value"] intValue];
NSMutableArray *digit = [digits objectAtIndex:digitIndex];
CALayer *flipLayer = [digit objectAtIndex:tickerFlip];
CALayer *bottomLayer = [digit objectAtIndex:tickerBottom];
if([[oldAnimation valueForKey:#"state"] isEqual:#"top"] && flag) {
NSLog(#"Top animation finished");
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
flipLayer.contents = [bottomImages objectAtIndex:currentValue];
flipLayer.anchorPoint = CGPointMake(0.0, 1.0);
flipLayer.hidden = NO;
[CATransaction commit];
CABasicAnimation *anim = [self generateAnimationForState:#"bottom"];
[anim setValue:[NSString stringWithFormat:#"%d", digitIndex] forKey:#"digit"];
[anim setValue:[NSString stringWithFormat:#"%d", currentValue] forKey:#"value"];
[flipLayer addAnimation:anim forKey:nil];
} else if([[oldAnimation valueForKey:#"state"] isEqual:#"bottom"] && flag) {
NSLog(#"Bottom animation finished");
// Hide our flip layer
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
bottomLayer.contents = [bottomImages objectAtIndex:currentValue];
flipLayer.hidden = YES;
flipLayer.anchorPoint = CGPointMake(0.0, 0.0);
[CATransaction commit];
}
}
This delegate function makes use of a helper function which generates the transform for the flip layer depending upon its state:
- (CABasicAnimation *)generateAnimationForState:(NSString *)state
{
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"transform"];
anim.duration = 0.15;
anim.repeatCount = 1;
// Check which animation we're doing
if([state isEqualToString:#"top"])
{
anim.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(0.0f, 1, 0, 0)];
anim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(M_PI/2, 1, 0, 0)];
}
else
{
anim.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(-M_PI/2, 1, 0, 0)];
anim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(0.0f, 1, 0, 0)];
}
anim.delegate = self;
anim.removedOnCompletion = NO;
// Set our animations state
[anim setValue:state forKey:#"state"];
return anim;
}
This solution works but causes some slight flickering when an animation is in progress. I believe this is due to the transform on my flip layer resetting between the 'top' and 'bottom' animations. It's important to note that after the first stage of the animation completes, I set the flip layers anchor point to the top of the image, ensuring the flip pivots correctly.
Currently I'm unsure if my animation has been setup optimally. I'm new to transformations and Core Animation in general. Can anyone point me in the right direction?
After
anim.removedOnCompletion = NO;
try inserting this code:
anim.fillMode = kCAFillModeForwards;
And let me know. Essentially the code snippet is supposed to prevent the 'reset' that is causing the flicker.