Core Animation 'flip' animation - cocoa

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.

Related

NSWindow flip animation (easy and universal)

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:
Create some new NSWindowController objects with NIBs in your project
Call [FLIPANIMATOR flipToWindowNibName:#"SecondWindowController" direction:ALFlipDirectionRight]; to flip to second window, or [FLIPANIMATOR flipToWindowNibName:#"FirstWindowController" direction:ALFlipDirectionLeft]; to return back
Link QuarzCore.framework to your project
That's all!

Problems animating an NSImageView/NSView with or without constraints

I'm trying to do some very simple view animation in a window (xib) that is using AutoLayout and I'm getting some very unpredictable results. Obviously the auto layout engine is doing some things under the hood and I don't know how to fix it.
To start with, my window has a grid of 3x3 NSImageViews that are set to NSImageScaleProportionallyUpOrDown. After setting the content compression to 10, the views do the right thing when the window is resized...everything stays in the right place and in the correct proportions. A nice 3x3 grid of images.
When the user clicks on a view, I want the view to takeover the entire window and fade out the other views. Then, when touched again, animate back to it's original position.
To keep things something simple, constraint-wise, I decided to leave all the NSImageViews in place and create another one on top of the touched view and use it to animate. That's the problem one.
I've tried animating that view 2 ways: by animating the constraints, and by not using constraints on that view and just animate it's frame. Either way, I get crazy results.
Here's the code:
Using Constraints
- (void) moveToFullView:(NSInteger)which
{
self.soloIndex = which;
NSImageView *tmpView = self.imageviews[which];
// create a view directly on top of the touched view
self.soloView = [[NSImageView alloc] initWithFrame:tmpView.frame];
[self.soloView.translatesAutoresizingMaskIntoConstraints = NO;
self.soloView.image = tmpView.image;
self.soloView.imageScaling = NSImageScaleProportionallyUpOrDown;
// without this, the window changes size in moveBackFromFullView
[self.soloView setContentCompressionResistancePriority:10 forOrientation:NSLayoutConstraintOrientationHorizontal];
[self.soloView setContentCompressionResistancePriority:10 forOrientation:NSLayoutConstraintOrientationVertical];
[self.view addSubview:self.soloView];
// create constraints with animatable constants
CGFloat leftSpace = tmpView.frame.origin.x;
CGFloat topSpace = tmpView.frame.origin.y;
CGFloat width = tmpView.frame.size.width - self.view.bounds.size.width;
CGFloat height = tmpView.frame.size.height - self.view.bounds.size.height;
self.soloView.leftConstraint = [NSLayoutConstraint constraintWithItem:self.soloView
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:leftSpace];
self.soloView.topConstraint = [NSLayoutConstraint constraintWithItem:self.soloView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:topSpace];
self.soloView.widthConstraint = [NSLayoutConstraint constraintWithItem:self.soloView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeWidth
multiplier:1.0
constant:width];
self.soloView.heightConstraint = [NSLayoutConstraint constraintWithItem:self.soloView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeHeight
multiplier:1.0
constant:height];
[self.view addConstraint:self.soloView.leftConstraint];
[self.view addConstraint:self.soloView.topConstraint];
[self.view addConstraint:self.soloView.widthConstraint];
[self.view addConstraint:self.soloView.heightConstraint];
[self.view layoutSubtreeIfNeeded];
// animate the view to occupy the entire self.view
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
context.allowsImplicitAnimation = YES;
context.duration = 0.75;
// fade out the other views for now
for(NSImageView *p in self.imageviews){
p.alphaValue = 0;
}
// these values should make the soloView in the same place and size as the self.view
self.soloView.leftConstraint.constant = 0;
self.soloView.topConstraint.constant = 0;
self.soloView.widthConstraint.constant = 0;
self.soloView.heightConstraint.constant = 0;
[self.view layoutSubtreeIfNeeded];
} completionHandler:nil];
}
- (void) moveBackFromFullView
{
// this is where the solo view should animate back to
NSRect destR = [self.imageviews[self.soloIndex] frame];
CGFloat leftSpace = destR.origin.x;
CGFloat topSpace = destR.origin.y;
CGFloat width = destR.size.width - self.view.bounds.size.width;
CGFloat height = destR.size.height - self.view.bounds.size.height;
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
context.allowsImplicitAnimation = YES;
context.duration = 0.75;
for(NSImageView *p in self.imageviews){
p.alphaValue = 1.00;
}
self.soloView.leftConstraint.constant = leftSpace;
self.soloView.topConstraint.constant = topSpace;
self.soloView.widthConstraint.constant = width;
self.soloView.heightConstraint.constant = height;
[self.view layoutSubtreeIfNeeded];
} completionHandler:^{
[self.soloView removeFromSuperview];
self.soloView = nil;
}];
}
Using the above constraint-based code, the soloView's left and top move correctly to full view and back, but when going to full view, the soloView's width and height don't animate and seem to just stay at the destination size. But...it goes back correctly...width and height too.
Using Frames
- (void) moveToFullView:(NSInteger)which
{
self.soloIndex = which;
NSImageView *tmpView = self.imageviews[which];
self.soloView = [[NSImageView alloc] initWithFrame:tmpView.frame];
[self.soloView.translatesAutoresizingMaskIntoConstraints = NO;
self.soloView.imageScaling = NSImageScaleProportionallyUpOrDown;
self.soloView.image = tmpView.image;
self.soloView.autoresizingMask = NSViewHeightSizable | NSViewWidthSizable;
[self.view addSubview:self.soloView];
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
context.allowsImplicitAnimation = YES;
context.duration = 0.75;
for(NSImageView *p in self.imageviews){
p.alphaValue = 0;
}
self.soloView.frame = NSMakeRect(0, 0, self.view.bounds.size.width, self.view.bounds.size.height);
} completionHandler:nil];
}
- (void) moveBackFromFullView
{
NSRect destR = [self.imageviews[self.soloIndex] frame];
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
context.allowsImplicitAnimation = YES;
context.duration = 0.95;
for(NSImageView *p in self.imageviews){
p.alphaValue = 1.00;
}
self.soloView.frame = destR;
} completionHandler:^{
[self.soloView removeFromSuperview];
self.soloView = nil;
}];
}
Using the above frame-based code, the soloView animates properly to and from full view, with the correct width and height. But there's something VERY strange when going back...there's another ghost view that animates from the origin to the same destination. I have NO idea how there's another animating view. I even checked the number of subviews of self.view and it's 10, but I see 11....the 9 grid views, the soloview and this ghost view. And yes, I have self.view.wantsLayer = YES when the main view is created.
I must be going crazy. Neither code works. I hate working with constraints for such a simple task. I'd rather use the frame based code...it's day and night much simpler, but I have no idea why there's another view moving back.
I have more experience with UIView animation than NSView animation, so maybe I'm missing something fundamental.
ANY help would be very much appreciated.

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.

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;
}

Widget "flip" behavior in Core Animation/Cocoa

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.

Resources