Change animation time for properties of a CALayer - cocoa

I have a CALayer to animate a change in its image contents. Now, how can I change how long it takes for this animation to take place?

You can just call:
[CATransaction setAnimationDuration:durationSecs]
in -layoutSublayers or anywhere else that you modify the layers and expect them to implicitly animate. This will effect the current implicit transaction and any sub-transactions within this one.

A different way to do this:
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:2.5f] forKey:kCATransactionAnimationDuration];
//Perform CALayer actions, such as changing the layer contents, position, whatever.
aCALayerObject.contents = [self newCALayerContents];
[CATransaction commit];
That code would animate the change of the CALayer's contents over 2.5 seconds. You can also use this to disable all animations completely. Like this:
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];

It's more or less simple. You have an ivar CALayer *yourLayer. Then you set the delegate and implement the delegate method -(id<CAAction>)actionForLayer:forKey:
- (void)awakeFromNib {
yourLayer.delegate = self;
yourLayer.name = #"yourLayer";
}
- (id <CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
if([layer.name isEqualToString yourLayer.name]) { // Check for right layer
CABasicAnimation *ani = [CABasicAnimation animationWithKeyPath:event]; // Default Animation for 'event'
ani.duration = .5; // Your custom animation duration
return ani;
} else return nil; // Default Animation
}

Related

Animating and removing an NSImageView object failed?

My app situation:
I am now having an app displaying a dynamic picture. And I want to capture its current image and save it to disc.
My goal:
I am working on an animated visual effect: add an image view on top, and then animates it from its origin frame into CGRecrZero. Finally, remove this image view from vie hierarchy after animation is done.
Now problem:
First time triggerring the action, it seems work. But when trigger it at the second time, the image view does not animate and stay atop of all subviews.
Codes of the view controller(with ARC):
Firstly, this is the IBAction to trigger the snapshot:
- (IBAction)actionSaveSnapshot:(id)sender
{
... // Save the image file. This works everytime.
NSThread *tempThread = [[NSThread alloc] initWithTarget:self
selector:#selector(threadSimulateScreenCapture:)
object:nil];
if (tempThread)
{
[tempThread start];
tempThread = nil; // Simply release it
}
}
And here is the thread:
- (void)threadSimulateScreenCapture:(id)arg
{#autoreleasepool {
NSImageView __strong *subImageView;
subImageView = [[NSImageView alloc] init];
NSRect subRect = [_imageView frame]; // the dynamic image view which I want to snapshot with
const CGFloat animationTime = 0.4;
[subImageView setWantsLayer:YES];
[subImageView setImageScaling:NSImageScaleAxesIndependently];
[subImageView setImage:[_imageView image]];
dispatch_async(dispatch_get_main_queue(), ^{
[CATransaction begin];
[self.view addSubview:subImageView];
[subImageView setFrame:subRect];
[subImageView setNeedsDisplay:YES];
[CATransaction commit];
});
[NSThread sleepForTimeInterval:0.01];
dispatch_async(dispatch_get_main_queue(), ^{
[CATransaction begin];
[CATransaction setAnimationDuration:animationTime];
[subImageView.layer setFrame:CGRectZero];
[CATransaction commit];
});
[NSThread sleepForTimeInterval:(NSTimeInterval)animationTime]; // wait for end of animation
dispatch_async(dispatch_get_main_queue(), ^{
[subImageView removeFromSuperview];
});
NSLog(#"SubView Count: %#\n%#", self.view.subviews, subImageView); // MARK. described below
subImageView = nil; // release the image
}} // ENDS: thread and aotureleasepool
Every time the program runs to the "MARK" line and print the subview array, it is clear thar the subImageView is not removed from superview then. And every time the thread ends, a "deleted thread with uncommitted CATransaction ...." raised.
What wrong did I do?
I may solved my own problem: At the line with [subImageView setFrame:subRect];, insert another line with: [subImageView.layer setFrame:subRect];
Meanwhile, add another line before the "addSubview:" method: [self.view setWantsLayer:YES];.
It seemed work. But I do not know why. Why I should set both view and layer's frame?

NSView Does Not Respond To Mouse Events After CABasicAnimation

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.

Why fadeout effect for view did't work?

I add to the scrollview my view with content as a documenView and use this code to fadeout my documentView:
- (IBAction)deleteAllRules:(id)sender {
NSViewAnimation * deleteListOfRulesViewAnimation;
NSRect viewRect;
NSMutableDictionary* animDict;
// Create the attributes dictionary
animDict = [NSMutableDictionary dictionaryWithCapacity:3];
viewRect = listOfRulesView.frame;
// Specify which view to modify.
[animDict setObject:listOfRulesView forKey:NSViewAnimationTargetKey];
// Specify the starting position of the view.
[animDict setObject:[NSValue valueWithRect:viewRect] forKey:NSViewAnimationStartFrameKey];
// Shrink the view from its current size to nothing.
NSRect viewZeroSize = listOfRulesView.frame;
viewZeroSize.size.width = 0;
viewZeroSize.size.height = 0;
[animDict setObject:[NSValue valueWithRect:viewZeroSize] forKey:NSViewAnimationEndFrameKey];
// Set this view to fade out
[animDict setObject:NSViewAnimationFadeOutEffect forKey:NSViewAnimationEffectKey];
// Create the view animation object.
deleteListOfRulesViewAnimation = [[NSViewAnimation alloc] initWithViewAnimations:[NSArray
arrayWithObjects:animDict, nil]];
// Set some additional attributes for the animation.
[deleteListOfRulesViewAnimation setDuration:1.5]; // One and a half seconds.
[deleteListOfRulesViewAnimation setAnimationCurve:NSAnimationEaseIn];
[deleteListOfRulesViewAnimation setDelegate:self];
// Run the animation.
[deleteListOfRulesViewAnimation startAnimation];
// The animation has finished, so go ahead and release it.
[deleteListOfRulesViewAnimation release];
}
but view hide with out fadeout effect. Why?
It's recommended to use Core Animation for 10.5+ code.
The simplest way to use Core Animation is to use the animator proxy. For example you can do the fade out effect by animating the alpha value of the view :
[[myView animator] setAlpaValue:0.0f]
You can change the animation duration using:
[[NSAnimationContext currentContext] setDuration:0.4f]

CALayer: callback when animation ends?

I have been running into some issues with animating multiple CALayers at the same time, and was hoping someone could point me in the right direction.
My app contains an array of CALayer. The position of each layer is set to (previousLayer.position.y + previousLayer.bounds.height), which basically lays them out similar to a table. I then have a method that, every-time it is called, adds a new layer to the stack and sets its Y position is set to 0. The Y positions of all other layers in the array are then offset by the height of the new layer (essentially pushing all old layers down).
What I am having problems with is preventing the adding of new layers until the previous animation has completed. Is there a way to tell when an implicit animation has finished? Or alternatively, if I use CABasicAnimation and animationDidFinish, is there a way to tell which object finished animating when animationDidFinish is called?
You can set arbitrary values for keys on your animation object. What this means is that you can associate your layer that you're animating with the animation and then query it in -animationDidStop:finished: You create your animation this way:
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"position"];
// set other fields...
[anim setValue:layerToAnimate forKey:#"layer"];
// Start the animation
[layerToAnimate addAnimation:anim forKey:nil];
Then check for that value when the animation stops:
- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)flag
{
CALayer *animatedLayer = [animation valueForKey:#"layer"];
// do something with the layer based on some condition...
// spin off the next animation...
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue
forKey:kCATransactionDisableActions];
[animatedLayer setPosition:position];
[CATransaction commit];
}
This is explicit animation, but it should give you what you need.
You can try surrounding your code in a CATransaction. Here's how that would look in Swift 3:
CATransaction.begin()
CATransaction.setCompletionBlock({
// run after the animations
})
// animtations
CATransaction.commit()
It turns out that rather than adding the CABasicAnimation directly to the CALayer, I had to add it to the layer's 'actions' dictionary... This leaves the layer at it's final position after the animation ends, but still calls the 'animationDidFinish' method.
-(void)startAnimation {
if(!animating){
animating = YES;
for(int i=0; i<[tweets count]; i++) {
//get the layer
CETweetLayer *currentLayer = [tweets objectAtIndex:i];
//setup the orgin and target y coordinates
float targetY = currentLayer.position.y + offset;
//setup the animation
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"position"];
anim.delegate = self;
currentLayer.actions = [NSDictionary dictionaryWithObject:anim forKey:#"position"];
currentLayer.position = CGPointMake(self.bounds.size.width/2, targetY);
}
}
}
And then...
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
animating = NO;
}

Core Animation 'flip' animation

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.

Resources