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.
Related
I wrote some animation code in OS X 10.6, but the animations aren't working properly when I recompile the app for 10.9.
I've got an array of buttons, for each of them my goal is to have it shrink, then return to normal size, then shrink, and then return to normal size (to "flex" a couple of times).
NSMutableArray *animations = [NSMutableArray arrayWithCapacity:buttons.count];
NSMutableArray *allAnimations = [NSMutableArray arrayWithCapacity:buttons.count*4];
for(NSButton *button in buttons) {
// store the original frame and generate the smaller frame
NSRect originalFrame = button.frame;
NSRect smallFrame = [self smallRectForRect:originalFrame scale:0.9];
// build dictionaries which describe the animations from normal-to-small and vice versa
NSMutableDictionary *normalToSmall = [NSMutableDictionary dictionary];
[normalToSmall setObject:button forKey:NSViewAnimationTargetKey];
[normalToSmall setObject:[NSValue valueWithRect:originalFrame] forKey:NSViewAnimationStartFrameKey];
[normalToSmall setObject:[NSValue valueWithRect:smallFrame] forKey:NSViewAnimationEndFrameKey];
NSMutableDictionary *smallToNormal = [NSMutableDictionary dictionary];
[smallToNormal setObject:button forKey:NSViewAnimationTargetKey];
[smallToNormal setObject:[NSValue valueWithRect:smallFrame] forKey:NSViewAnimationStartFrameKey];
[smallToNormal setObject:[NSValue valueWithRect:originalFrame] forKey:NSViewAnimationEndFrameKey];
// create, and chain together, the shrink, reset, shrink, reset animation chain
NSViewAnimation *animation1 = [self createAnimationWithDictionary:normalToSmall duration:duration startingAfterAnimation:nil];
NSViewAnimation *animation2 = [self createAnimationWithDictionary:smallToNormal duration:duration startingAfterAnimation:animation1];
NSViewAnimation *animation3 = [self createAnimationWithDictionary:normalToSmall duration:duration startingAfterAnimation:animation2];
NSViewAnimation *animation4 = [self createAnimationWithDictionary:smallToNormal duration:duration startingAfterAnimation:animation3];
// store all of the animations in an array just in case ARC wants to release them prematurely.
[allAnimations addObjectsFromArray:#[animation1, animation2, animation3, animation4]];
// Store the first animation, so that we can start each of the chains after we're done creating them.
[animations addObject:animation1];
}
// start the first animation in each chain
for (NSViewAnimation *animation in animations) {
[animation startAnimation];
}
Here is my function used to create each of the animations:
- (NSViewAnimation*) createAnimationWithDictionary:(NSDictionary*)animationDictionary duration:(float)duration startingAfterAnimation:(NSViewAnimation*)previousAnimation {
NSViewAnimation *animation = [[NSViewAnimation alloc] initWithViewAnimations:[NSArray arrayWithObject:animationDictionary]];
[animation setDuration:duration];
[animation setDelegate:self];
if (previousAnimation != nil) {
[animation startWhenAnimation:previousAnimation reachesProgress:1.0];
}
return animation;
}
I set my window controller as the delegate so I could watch the animations start and end and it appears that only the first animation in each chain starts and ends. The subsequent animations which were configured to -startWhenAnimation:reachesProgress:1.0 do not appear to start at all.
Why won't the subsequent animations start running?
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]
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;
}
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.
I'm trying to make a Card class that duplicates the behavior of Dashboard widgets in that you can put controls or images or whatever on two sides of the card and flip between them.
Layer backed views have a transform property, but altering that doesn't do what I would expect it to do (rotating the layer around the y axis folds it off to the left side).
I was pointed to some undocumented features and an .h file named cgsprivate.h, but I'm wondering if there is an official way to do this? This software would have to be shipped and I'd hate to see it fail later because the Apple guys pull it in 10.6.
Anyone have any idea how to do this? It's so weird to me that a simple widget thing would be so hard to do in Core Animation.
Thanks in advance!
EDIT: I can accomplish this behavior with images that are on layers, but I don't know how to get more advanced controls/views/whatever on the layers. The card example uses images.
Mike Lee has an implementation of the flip effect for which he has released some sample code. (Unfortunately, this is no longer available online, but Drew McCormack built off of that in his own implementation.) It appears that he grabs the layers for the "background" and "foreground" views to be swapped, uses a CATransform3D to rotate the two views in the animation, and then swaps the views once the animation has completed.
By using the layers from the views, you avoid needing to cache into a bitmap, since that's what the layers are doing anyways. In any case, his view controller looks to be a good drop-in solution for what you want.
Using Core Animation like e.James outlined...Note, this is using garbage collection and a hosted layer:
#import "AnimationWindows.h"
#interface AnimationFlipWindow (PrivateMethods)
NSRect RectToScreen(NSRect aRect, NSView *aView);
NSRect RectFromScreen(NSRect aRect, NSView *aView);
NSRect RectFromViewToView(NSRect aRect, NSView *fromView, NSView *toView);
#end
#pragma mark -
#implementation AnimationFlipWindow
#synthesize flipForward = _flipForward;
- (id) init {
if ( self = [super init] ) {
_flipForward = YES;
}
return self;
}
- (void) finalize {
// Hint to GC for cleanup
[[NSGarbageCollector defaultCollector] collectIfNeeded];
[super finalize];
}
- (void) flip:(NSWindow *)activeWindow
toBack:(NSWindow *)targetWindow {
CGFloat duration = 1.0f * (activeWindow.currentEvent.modifierFlags & NSShiftKeyMask ? 10.0 : 1.0);
CGFloat zDistance = 1500.0f;
NSView *activeView = [activeWindow.contentView superview];
NSView *targetView = [targetWindow.contentView superview];
// Create an animation window
CGFloat maxWidth = MAX(NSWidth(activeWindow.frame), NSWidth(targetWindow.frame)) + 500;
CGFloat maxHeight = MAX(NSHeight(activeWindow.frame), NSHeight(targetWindow.frame)) + 500;
CGRect animationFrame = CGRectMake(NSMidX(activeWindow.frame) - (maxWidth / 2),
NSMidY(activeWindow.frame) - (maxHeight / 2),
maxWidth,
maxHeight);
mAnimationWindow = [NSWindow initForAnimation:NSRectFromCGRect(animationFrame)];
// Add a touch of perspective
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0 / zDistance;
[mAnimationWindow.contentView layer].sublayerTransform = transform;
// Relocate target window near active window
CGRect targetFrame = CGRectMake(NSMidX(activeWindow.frame) - (NSWidth(targetWindow.frame) / 2 ),
NSMaxY(activeWindow.frame) - NSHeight(targetWindow.frame),
NSWidth(targetWindow.frame),
NSHeight(targetWindow.frame));
[targetWindow setFrame:NSRectFromCGRect(targetFrame) display:NO];
mTargetWindow = targetWindow;
// New Active/Target Layers
[CATransaction begin];
CALayer *activeWindowLayer = [activeView layerFromWindow];
CALayer *targetWindowLayer = [targetView layerFromWindow];
[CATransaction commit];
activeWindowLayer.frame = NSRectToCGRect(RectFromViewToView(activeView.frame, activeView, [mAnimationWindow contentView]));
targetWindowLayer.frame = NSRectToCGRect(RectFromViewToView(targetView.frame, targetView, [mAnimationWindow contentView]));
[CATransaction begin];
[[mAnimationWindow.contentView layer] addSublayer:activeWindowLayer];
[CATransaction commit];
[mAnimationWindow orderFront:nil];
[CATransaction begin];
[[mAnimationWindow.contentView layer] addSublayer:targetWindowLayer];
[CATransaction commit];
// Animate our new layers
[CATransaction begin];
CAAnimation *activeAnim = [CAAnimation animationWithDuration:(duration * 0.5) flip:YES forward:_flipForward];
CAAnimation *targetAnim = [CAAnimation animationWithDuration:(duration * 0.5) flip:NO forward:_flipForward];
[CATransaction commit];
targetAnim.delegate = self;
[activeWindow orderOut:nil];
[CATransaction begin];
[activeWindowLayer addAnimation:activeAnim forKey:#"flip"];
[targetWindowLayer addAnimation:targetAnim forKey:#"flip"];
[CATransaction commit];
}
- (void) animationDidStop:(CAAnimation *)animation finished:(BOOL)flag {
if (flag) {
[mTargetWindow makeKeyAndOrderFront:nil];
[mAnimationWindow orderOut:nil];
mTargetWindow = nil;
mAnimationWindow = nil;
}
}
#pragma mark PrivateMethods:
NSRect RectToScreen(NSRect aRect, NSView *aView) {
aRect = [aView convertRect:aRect toView:nil];
aRect.origin = [aView.window convertBaseToScreen:aRect.origin];
return aRect;
}
NSRect RectFromScreen(NSRect aRect, NSView *aView) {
aRect.origin = [aView.window convertScreenToBase:aRect.origin];
aRect = [aView convertRect:aRect fromView:nil];
return aRect;
}
NSRect RectFromViewToView(NSRect aRect, NSView *fromView, NSView *toView) {
aRect = RectToScreen(aRect, fromView);
aRect = RectFromScreen(aRect, toView);
return aRect;
}
#end
#pragma mark -
#pragma mark CategoryMethods:
#implementation CAAnimation (AnimationFlipWindow)
+ (CAAnimation *) animationWithDuration:(CGFloat)time flip:(BOOL)bFlip forward:(BOOL)forwardFlip{
CABasicAnimation *flipAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
CGFloat startValue, endValue;
if ( forwardFlip ) {
startValue = bFlip ? 0.0f : -M_PI;
endValue = bFlip ? M_PI : 0.0f;
} else {
startValue = bFlip ? 0.0f : M_PI;
endValue = bFlip ? -M_PI : 0.0f;
}
flipAnimation.fromValue = [NSNumber numberWithDouble:startValue];
flipAnimation.toValue = [NSNumber numberWithDouble:endValue];
CABasicAnimation *shrinkAnimation = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
shrinkAnimation.toValue = [NSNumber numberWithFloat:1.3f];
shrinkAnimation.duration = time * 0.5;
shrinkAnimation.autoreverses = YES;
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
animationGroup.animations = [NSArray arrayWithObjects:flipAnimation, shrinkAnimation, nil];
animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animationGroup.duration = time;
animationGroup.fillMode = kCAFillModeForwards;
animationGroup.removedOnCompletion = NO;
return animationGroup;
}
#end
#pragma mark -
#implementation NSWindow (AnimationFlipWindow)
+ (NSWindow *) initForAnimation:(NSRect)aFrame {
NSWindow *window = [[NSWindow alloc] initWithContentRect:aFrame
styleMask:NSBorderlessWindowMask
backing:NSBackingStoreBuffered
defer:NO];
[window setOpaque:NO];
[window setHasShadow:NO];
[window setBackgroundColor:[NSColor clearColor]];
[window.contentView setWantsLayer:YES];
return window;
}
#end
#pragma mark -
#implementation NSView (AnimationFlipWindow)
- (CALayer *) layerFromWindow {
NSBitmapImageRep *image = [self bitmapImageRepForCachingDisplayInRect:self.bounds];
[self cacheDisplayInRect:self.bounds toBitmapImageRep:image];
CALayer *layer = [CALayer layer];
layer.contents = (id)image.CGImage;
layer.doubleSided = NO;
// Shadow settings based upon Mac OS X 10.6
[layer setShadowOpacity:0.5f];
[layer setShadowOffset:CGSizeMake(0,-10)];
[layer setShadowRadius:15.0f];
return layer;
}
#end
The header file:
#interface AnimationFlipWindow : NSObject {
BOOL _flipForward;
NSWindow *mAnimationWindow;
NSWindow *mTargetWindow;
}
// Direction of flip animation (property)
#property (readwrite, getter=isFlipForward) BOOL flipForward;
- (void) flip:(NSWindow *)activeWindow
toBack:(NSWindow *)targetWindow;
#end
#pragma mark -
#pragma mark CategoryMethods:
#interface CAAnimation (AnimationFlipWindow)
+ (CAAnimation *) animationWithDuration:(CGFloat)time
flip:(BOOL)bFlip // Flip for each side
forward:(BOOL)forwardFlip; // Direction of flip
#end
#interface NSWindow (AnimationFlipWindow)
+ (NSWindow *) initForAnimation:(NSRect)aFrame;
#end
#interface NSView (AnimationFlipWindow)
- (CALayer *) layerFromWindow;
#end
EDIT: This will animate to flip from one window to another window. You can apply the same principals to a view.
It's overkill for your purposes (as it contains a largely-complete board and card game reference app), but check out this sample from ADC. The card games included with it do that flip effect quite nicely.
If you are able to do this with images, perhaps you can keep all of your controls in an NSView object (as usual), and then render the NSView into a bitmap image using cacheDisplayInRect:toBitmapImageRep: just prior to executing the flip effect. The steps would be:
Render the NSView to a bitmap
Display that bitmap in a layer suitable for the flip effect
Hide the NSView and expose the image layer
Perform the flip effect
I know this is late but Apple has an example project here that may be of help to anyone still stumbling upon this question.
https://developer.apple.com/library/mac/#samplecode/ImageTransition/Introduction/Intro.html#//apple_ref/doc/uid/DTS40010277
There's a complete open source implementation of this by the guys at Mizage.
You can check it out here: https://github.com/mizage/Flip-Animation
Probably not the case in 2008 when this question was asked, but this is pretty easy these days:
[UIView animateWithDuration:0.5 animations:^{
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromRight forView:self.iconView cache:YES];
/* changes to the view made here will be reflected on the flipped to side */
}];
Note: Apparently, this only works on iOS.