How to make flip animation for OS X application windows without complex coding?
Finally, I did it. I have created object that work with NSWindowController objects instead of NSWidows.
ALWindowFlipAnimator.h
#import <Foundation/Foundation.h>
//............................................................................................................
// Shorten macroes:
#define FLIPANIMATOR [ALWindowFlipAnimator sharedWindowFlipAnimator]
//............................................................................................................
// Window flip direction:
typedef NS_ENUM(NSUInteger, ALFlipDirection)
{
ALFlipDirectionLeft,
ALFlipDirectionRight,
ALFlipDirectionUp,
ALFlipDirectionDown
};
@interface ALWindowFlipAnimator : NSObject
+(ALWindowFlipAnimator *)sharedWindowFlipAnimator;
-(void)flipToWindowNibName:(NSString *)nibName direction:(ALFlipDirection)direction;
@end
ALWindowFlipAnimator.m
#import "ALWindowFlipAnimator.h"
#import <QuartzCore/QuartzCore.h>
@implementation ALWindowFlipAnimator
{
NSWindowController *_currentWindowController; // Current window controller
NSWindowController *_nextWindowController; // Next window controller to flip to
NSWindow *_animationWindow; // Window where flip animation plays
}
//============================================================================================================
// Initialize flip window controller
//============================================================================================================
-(id)init
{
self = [super init];
if (self)
{
_currentWindowController = nil;
_nextWindowController = nil;
_animationWindow = nil;
}
return self;
}
//============================================================================================================
// Create shared flip window manager
//============================================================================================================
+(ALWindowFlipAnimator *)sharedWindowFlipAnimator
{
static ALWindowFlipAnimator *wfa = nil;
if (!wfa) wfa = [[ALWindowFlipAnimator alloc] init];
return wfa;
}
//============================================================================================================
// Flip to window with selected NIB file name from the current one
//============================================================================================================
-(void)flipToWindowNibName:(NSString *)nibName direction:(ALFlipDirection)direction
{
if (!_currentWindowController || ![[_currentWindowController window] isVisible])
{
// No current window controller or window is closed
_currentWindowController = [[NSClassFromString(nibName) alloc] initWithWindowNibName:nibName];
[_currentWindowController showWindow:self];
}
else
{
if ([[_currentWindowController className] isEqualToString:nibName])
// Bring current window to front
[[_currentWindowController window] makeKeyAndOrderFront:self];
else
{
// Flip to new window
_nextWindowController = [[NSClassFromString(nibName) alloc] initWithWindowNibName:nibName];
[self flipToNextWindowControllerDirection:direction];
}
}
}
#pragma mark - Flip animation
#define DEF_DURATION 2.0 // Animation duration
#define DEF_SCALE 1.2 // Scaling factor for animation window (_animationWindow)
//============================================================================================================
// Start window flipping animation
//============================================================================================================
-(void)flipToNextWindowControllerDirection:(ALFlipDirection)direction
{
NSWindow *currentWindow = [_currentWindowController window];
NSWindow *nextWindow = [_nextWindowController window];
NSView *currentWindowView = [currentWindow.contentView superview];
NSView *nextWindowView = [nextWindow.contentView superview];
// Create window for animation
CGFloat maxWidth = MAX(currentWindow.frame.size.width, nextWindow.frame.size.width);
CGFloat maxHeight = MAX(currentWindow.frame.size.height, nextWindow.frame.size.height);
CGFloat xscale = DEF_SCALE * 2.0;
maxWidth += maxWidth * xscale;
maxHeight += maxHeight * xscale;
CGRect animationFrame = CGRectMake(NSMidX(currentWindow.frame) - (maxWidth / 2),
NSMidY(currentWindow.frame) - (maxHeight / 2),
maxWidth, maxHeight);
_animationWindow = [[NSWindow alloc] initWithContentRect:NSRectFromCGRect(animationFrame)
styleMask:NSBorderlessWindowMask
backing:NSBackingStoreBuffered
defer:NO];
[_animationWindow setOpaque:NO];
[_animationWindow setHasShadow:NO];
[_animationWindow setBackgroundColor:[NSColor clearColor]];
[_animationWindow.contentView setWantsLayer:YES];
[_animationWindow setLevel:NSScreenSaverWindowLevel];
// Move next window closer to the current one
CGRect nextFrame = CGRectMake(NSMidX(currentWindow.frame) - (NSWidth(nextWindow.frame) / 2 ),
NSMaxY(currentWindow.frame) - NSHeight(nextWindow.frame),
NSWidth(nextWindow.frame), NSHeight(nextWindow.frame));
[nextWindow setFrame:NSRectFromCGRect(nextFrame) display:NO];
// Make snapshots of current and next windows
[CATransaction begin];
CALayer *currentWindowSnapshot = [self snapshotToImageLayerFromView:currentWindowView];
CALayer *nextWindowSnapshot = [self snapshotToImageLayerFromView:nextWindowView];
[CATransaction commit];
currentWindowSnapshot.frame = [self rect:currentWindowView.frame
fromView:currentWindowView
toView:[_animationWindow contentView]];
nextWindowSnapshot.frame = [self rect:nextWindowView.frame
fromView:nextWindowView
toView:[_animationWindow contentView]];
// Create 3D transform matrix to snapshots
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -(1.0 / 1500.0);
currentWindowSnapshot.transform = transform;
nextWindowSnapshot.transform = transform;
// Add snapshots to animation window
[CATransaction begin];
[[_animationWindow.contentView layer] addSublayer:currentWindowSnapshot];
[[_animationWindow.contentView layer] addSublayer:nextWindowSnapshot];
[CATransaction commit];
[_animationWindow makeKeyAndOrderFront:nil];
// Animation for snapshots
[CATransaction begin];
CAAnimation *currentSnapshotAnimation = [self animationWithDuration:(DEF_DURATION * 0.5) flip:YES direction:direction];
CAAnimation *nextSnapshotAnimation = [self animationWithDuration:(DEF_DURATION * 0.5) flip:NO direction:direction];
[CATransaction commit];
// Start animation
nextSnapshotAnimation.delegate = self;
[currentWindow orderOut:nil];
[CATransaction begin];
[currentWindowSnapshot addAnimation:currentSnapshotAnimation forKey:@"flipAnimation"];
[nextWindowSnapshot addAnimation:nextSnapshotAnimation forKey:@"flipAnimation"];
[CATransaction commit];
}
//============================================================================================================
// Convert rectangle from one view coordinates to another
//============================================================================================================
-(CGRect)rect:(NSRect)rect fromView:(NSView *)fromView toView:(NSView *)toView
{
rect = [fromView convertRect:rect toView:nil];
rect = [fromView.window convertRectToScreen:rect];
rect = [toView.window convertRectFromScreen:rect];
rect = [toView convertRect:rect fromView:nil];
return NSRectToCGRect(rect);
}
//============================================================================================================
// Get snapshot of selected view as layer with bitmap image
//============================================================================================================
-(CALayer *)snapshotToImageLayerFromView:(NSView*)view
{
// Make view snapshot
NSBitmapImageRep *snapshot = [view bitmapImageRepForCachingDisplayInRect:view.bounds];
[view cacheDisplayInRect:view.bounds toBitmapImageRep:snapshot];
// Convert snapshot to layer
CALayer *layer = [CALayer layer];
layer.contents = (id)snapshot.CGImage;
layer.doubleSided = NO;
// Add shadow of window to snapshot
[layer setShadowOpacity:0.5];
[layer setShadowOffset:CGSizeMake(0.0, -10.0)];
[layer setShadowRadius:15.0];
return layer;
}
//============================================================================================================
// Create animation
//============================================================================================================
-(CAAnimation *)animationWithDuration:(CGFloat)time flip:(BOOL)flip direction:(ALFlipDirection)direction
{
// Set flip direction
NSString *keyPath = @"transform.rotation.y";
if (direction == ALFlipDirectionUp || direction == ALFlipDirectionDown) keyPath = @"transform.rotation.x";
CABasicAnimation *flipAnimation = [CABasicAnimation animationWithKeyPath:keyPath];
CGFloat startValue = flip ? 0.0 : -M_PI;
CGFloat endValue = flip ? M_PI : 0.0;
if (direction == ALFlipDirectionLeft || direction == ALFlipDirectionUp)
{
startValue = flip ? 0.0 : M_PI;
endValue = flip ? -M_PI : 0.0;
}
flipAnimation.fromValue = [NSNumber numberWithDouble:startValue];
flipAnimation.toValue = [NSNumber numberWithDouble:endValue];
CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
scaleAnimation.toValue = [NSNumber numberWithFloat:DEF_SCALE];
scaleAnimation.duration = time * 0.5;
scaleAnimation.autoreverses = YES;
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
animationGroup.animations = [NSArray arrayWithObjects:flipAnimation, scaleAnimation, nil];
animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animationGroup.duration = time;
animationGroup.fillMode = kCAFillModeForwards;
animationGroup.removedOnCompletion = NO;
return animationGroup;
}
//============================================================================================================
// Flip animation did finish
//============================================================================================================
-(void)animationDidStop:(CAAnimation *)animation finished:(BOOL)flag
{
[[_nextWindowController window] makeKeyAndOrderFront:nil];
[_animationWindow orderOut:nil];
_animationWindow = nil;
_currentWindowController = _nextWindowController;
}
@end
How to use:
[FLIPANIMATOR flipToWindowNibName:@"SecondWindowController" direction:ALFlipDirectionRight];
to flip to second window, or [FLIPANIMATOR flipToWindowNibName:@"FirstWindowController" direction:ALFlipDirectionLeft];
to return back