I'm making a navigation controller class for Mac OS X.
I want to replace the current view with a kCATransitionPush animation.
Like in this post:
Core Animation Tutorial - Wizard Dialog With Transitions
The CATransition is set up like this:
CATransition *transition = [CATransition animation];
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromLeft;
[self.view setAnimations:#{ #"subviews" : transition }];
However, when I replace the views, there is a fade animation, which is being automatically added.
[NSAnimationContext beginGrouping];
{
[[self.view animator] replaceSubview:_currentViewController.view with:newViewController.view];
}
[NSAnimationContext endGrouping];
How can I do a push animation without the fading?
The best way I've found to get smooth Core Animation transitions that works regardless of whether the view supports CA or not is to do the following:
Create an image of the view you are trying to animate, using NSView -cacheDisplayInRect:toBitmapImageRep or a similar method
Put that image in an NSImageView
Layer back the image view, which will draw fine without glitches when layer backed
Add the image view as a subview over the view that you are trying to transition
Animate the image view frame using NSView's animator proxy
Remove the image view once the animation has completed
I suspect you're running into implicit animations - Core Animation will automatically animate layer property changes that happen outside of your own transactions.
There's a good summary of several methods for disabling these implicit animations in these two questions:
How to disable CALayer implicit animations?
Disabling implicit animations in -[CALayer setNeedsDisplayInRect:]
...and you can read more about implicit transactions in the Core Animation docs
I think the transition from left transition includes a built-in fade. The IOS push transitions do.
If you don't want that, you might have to roll your own push transition using Core Animation. This would be easy in iOS with UIView animations. Sadly, there is not an equivalent in Mac OS. I wish Apple would go back and add view animations to Mac OS. I get spoiled using them in iOS, and then miss them when I work on Mac applications.
Add below code
CATransition *transition = [CATransition animation];
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromLeft;
[transition setDuration:0.5];
[self.view.layer addAnimation:transition forKey:kCATransition];
Related
To illustrate this question, I gisted a very small Xcode project on Github (two classes, 11kb download). Have a look at the gist here or use git clone git#gist.github.com:93982af3b65d2151672e.git.
Please consider the following scenario. A custom view of mine, called 'container view', contains two little squares. Illustrated in this screenshot:
The blue square is a 22x22 pt UIView instance and the red square is a 22x22 pt CALayer instance. For purposes of this question I want the two squares to 'stick' to the bottom right corner of the outer view, while I animate the frame of that outer view.
I change the container view's frame within a UIView's class method animateWithDuration:delay:options:animations:completion:, with a non-default easing parameter of type UIViewAnimationCurveEaseOut.
[UIView animateWithDuration:1.0 delay:0
options:UIViewAnimationCurveEaseOut
animations:^{ _containerView.frame = randomFrame; }
completion:nil];
Note: Both the blue UIView's and the red CALayer's frame are set in the overridden setFrame: method of the container view, but results would have been the same if I had set the blue UIView's autoresizingMask property to UIView flexible left and top margins.
In the resulting animation, the blue square 'sticks' to the corner just the way I intended it, but the red square completely disregards both timing and easing of the animation. I assume this is because of the implicit animation feature of Core Animation, a feature that has helped me in many occasions before.
Here are a few screenshots in the animation sequence that illustrate the asynchronicity of the two squares:
On to the question: Is there a way to synchronize the two frame changes so that the red CALayer and the blue UIView both move with the same animation duration and easing curve, sticking to each other as if they were one view?
P.S. Of course the required visual result of the two squares sticking together could be achieved in any number of ways, for example by having both layers become either CALayers or UIViews, but the project that the real issue is in has a very legit cause for the one to be a CALayer and the other to be a UIView.
I'm assuming that you got the end position correct and that after the animation that the view and the layer are aligned. That has nothing to do with the animation, just geometry.
When you change the property of a CALayer that isn't the layer of a view (like in your case) it will implicitly animate to its new value. To customize this animation you could use an explicit animation, like a basic animation. Changing the frame in a basic animation would look something like this.
CABasicAnimation *myAnimation = [CABasicAnimation animationWithKeyPath:#"frame"];
[myAnimation setToValue:[NSValue valueWithCGRect:myNewFrame]];
[myAnimation setFromValue:[NSValue valueWithCGRect:[myLayer frame]]];
[myAnimation setDuration:1.0];
[myAnimation setTimingFunction:[CAMediaTimingFuntion functionWithName: kCAMediaTimingFunctionEaseOut]];
[myLayer setFrame:myNewFrame];
[myLayer addAnimation:myAnimation forKey:#"someKeyForMyAnimation"];
If the timing function and the duration of both animations are the same then they should stay aligned.
Also note that explicit animations doesn't change the value and hat you have to both add the animation and set the new value (like in the sample above)
Yes, there are a number of ways to achieve the same effect. One example is having the view and the layer be subviews of the same subview (that in turn is a subview of the outer frame).
Edit
You can't easily group the UIView-animation with an explicit animation. Neither can you use an animation group (since you are applying them to different layers) but yo can use a CATransaction:
[CATransaction begin];
[CATransaction setAnimationDuration:1.0];
[CATransaction setAnimationTimingFunction:[CAMediaTimingFuntion functionWithName: kCAMediaTimingFunctionEaseOut]];
// Layer animation
CABasicAnimation *myAnimation = [CABasicAnimation animationWithKeyPath:#"frame"];
[myAnimation setToValue:[NSValue valueWithCGRect:myNewFrame]];
[myAnimation setFromValue:[NSValue valueWithCGRect:[myLayer frame]]];
[myLayer setFrame:myNewFrame];
[myLayer addAnimation:myAnimation forKey:#"someKeyForMyAnimation"];
// Outer animation
CABasicAnimation *outerAnimation = [CABasicAnimation animationWithKeyPath:#"frame"];
[outerAnimation setToValue:[NSValue valueWithCGRect:myNewOuterFrame]];
[outerAnimation setFromValue:[NSValue valueWithCGRect:[outerView frame]]];
[[outerView layer] setFrame:myNewOuterFrame];
[[outerView layer] addAnimation:outerAnimation forKey:#"someKeyForMyOuterAnimation"];
[CATransaction commit];
If you want David Rönnqvist's snippet in Swift 3.0, here you have it:
CATransaction.begin()
CATransaction.setAnimationDuration(1.0)
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut))
// Layer animation
let myAnimation = CABasicAnimation(keyPath: "frame");
myAnimation.toValue = NSValue(cgRect: myNewFrame)
myAnimation.fromValue = NSValue(cgRect: myLayer.frame)
myLayer.frame = myNewFrame
myLayer.add(myAnimation, forKey: "someKeyForMyAnimation")
// Outer animation
let outerAnimation = CABasicAnimation(keyPath: "frame")
outerAnimation.toValue = NSValue(cgRect: myNewOuterFrame)
outerAnimation.fromValue = NSValue(cgRect: outerView.frame)
outerView.layer.frame = myNewOuterFrame
outerView.layer.add(outerAnimation, forKey: "someKeyForMyOuterAnimation")
CATransaction.commit()
My app uses CALayer to draw views. More precisely, it uses the drawLayer:inContext: method on a sublayer of a UIView's top layer. This is a nice way to get the 'implicit' animation of consecutive drawLayer:inContext: drawings to fade into each other over time. The fading animations happen fairly fast, maybe in 0.25 seconds, but to change its duration, simply implement another delegate method called actionForLayer:forKey:. In this perfectly working example implementation here the default duration is stretched to 2.0 seconds:
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:event];
animation.duration = 2.0;
return animation;
// or return nil for the default duration.
}
On to the issue at hand.
If you call [sublayer setNeedsDisplay] faster than the fades have time to complete, with each new fade you'll see a sudden jump. From the looks of it, the fade that's in progress is cancelled and it's final state is used as the starting point of the new fade. This might not be very surprising, but the visual result is rather unwanted.
Consider the scenario of a ten second fade from black to white, with another fade, to black, triggered five seconds after the start. The animation will start fading from black to white, but when it's at a 'half way gray' it jumps to full white before fading to black again.
Is there a way to prevent this from happening? Can I get the layer to fade from the gray back down to black? Is there a CALayer drawing equivalent of saying UIViewAnimationOptionBeginFromCurrentState (used in UIView animations)?
Cheers.
A layer's animation is only a visual representation of what the layer should look like as it animates. In CA when you animate from one state to another, the entire state of the layer changes immediately. A presentation layer is created and displays the animation, and when the animation completes the actual layer is left in place at the end.
So, my guess is that when you want to transition from one state to another, and the current animation hasn't completed yet, you have to capture the current state of the animation and then use this as the starting point for your next animation.
The problem lies in not being able to modify a layer's current animation.
In the following post I capture the current state of an animation, set that as the current state for the layer and use that as the beginning value from which to animate. The post applies this technique to the speed / duration of an animation, but can also be applied to your scenario.
https://stackoverflow.com/a/9544674/1218605
I'm a little stumped on this one too.
Did you forget to specify the fillMode kCAFillModeForwards. There's more info about that in the reference docs.
For example, I got this to work without any snapping, although I'm not changing the duration.
#implementation FadingLayer
- (void)fadeOut {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"backgroundColor"];
animation.fillMode = kCAFillModeForwards;
animation.fromValue = (id)[UIColor redColor].CGColor;
animation.toValue = (id)[UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.0].CGColor;
animation.removedOnCompletion = FALSE;
animation.delegate = self;
[self addAnimation:animation
forKey:#"test"];
}
- (void)fadeIn {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"backgroundColor"];
animation.fillMode = kCAFillModeForwards;
animation.fromValue = (id)[UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.0].CGColor;
animation.toValue = (id)[UIColor redColor].CGColor;
animation.removedOnCompletion = FALSE;
animation.delegate = self;
[self addAnimation:animation
forKey:#"test"];
}
#end
You'll probably want to animate a custom property however.
Hope this helps :/
I wanted to accomplish the same thing with a zoom animation of a layer tree. I have a zoom in/out key-equivalent where the user can zoom the layer tree accordingly. However, if the user presses the zoom key-equivalent in rapid succession, there would be a temporary snap-back to the values prior to the onset of the animation, since the previous animation hadn't yet completed.
At the end of the animation code, performing a sole [CATransaction commit] forced any pending transactions to be committed to the layer model before the start of the next animation, and solved the problem.
The documentation says:
+ commit
Commit all changes made during the current transaction.
Declaration
+ (void)commit
Special Considerations
Raises an exception if no current transaction exists.
However, testing this with many [CATransaction commit] messages in succession doesn't actually raise an exception. I've used this same technique to squelch warnings of the form:
CoreAnimation: warning, deleted thread with uncommitted
CATransaction;
in an NSOperation whose thread of execution finishes before layer animations do. It could be that Apple changed this behaviour in recent OS releases to a no-op (which would be much saner) if no current transaction exists, without updating the documentation.
This is my problem:
The background of the progress indicator doesn't appear to be redrawing, and it's certainely not transparent. I'm using core animation to animate the image in the background; when I don't use core animation it looks fine. This is the code I am using:
[[NSAnimationContext currentContext] setDuration:0.25];
[[ViewImage animator] setAlphaValue:0.5f ];
[[statusText animator] setAlphaValue:0.1f ];
[progressIndicator usesThreadedAnimation ];
The progress indicator doesn't use core animation. I have also tried removing [progressIndicator usesThreadedAnimation]; which doesn't help.
-usesThreadedAnimation is the getter for the property. You want -setUsesThreadedAnimation: to set the property.
Also, for the transparency issue, I believe you need to switch on layers for at least the progress indicator if not the parent view as well. That should fix the transparency issue.
I'm trying to bring the ripple effect seen in the dashboard application to iphone. My idea is whenever i place a button in the layout view there should be ripple effect around the button.
However, I`m able to bring the ripple effect for the whole view but I needs the effect only around the button. I don know where I went wrong. I tried the following code.
LayoutButton *tempLayoutButton=[[LayoutButton alloc] initWithObject:object];
tempLayoutButton.center=copyImage.center;
[layoutView addSubview:tempLayoutButton];
CALayer *templayer=[CALayer layer];
tempLayoutButton.layer.masksToBounds=NO;
templayer.frame=CGRectMake(0,0,50,50);
[tempLayoutButton.layer addSublayer:templayer];
CATransition *animation = [CATransition animation];
animation.delegate = self;
animation.duration = 1.0f;
animation.timingFunction = UIViewAnimationCurveEaseInOut;
animation.type = #"rippleEffect";
[templayer addAnimation:animation forKey:#"animation"];
[tempLayoutButton release];
[tempLayoutButton.layer addSublayer:templayer];
If I replace templayer with LayoutView layer I can see the riple effect from the whole view. Can anyone tell me the solution
Thanks in advance
I found the answer in one of apples mailing list. The animation is apple`s own animation and no one can use that. And there is no guarantee for the animation to appear properly in the screen. If we need the same type of animation we need to manipulate the layers and get the animation
In the past I've been successfully able to fade in an NSWindow using the following code;
if (![statusWindow isVisible])
{
statusWindow.alphaValue = 0.0;
[statusWindow.animator setAlphaValue:1.0];
}
CAAnimation *anim = [CABasicAnimation animation];
[anim setDelegate:self];
[statusWindow setAnimations:[NSDictionary dictionaryWithObject:anim forKey:#"alphaValue"]];
[statusWindow makeKeyAndOrderFront:self];
For my current project I'm trying to make a flash similar to the one in Photo Booth. I've created a white NSPanel and was planning to set my NSWindow's content to the panel, and quickly set it back.
Is it possible to set the contentView of an NSWindow using a nice fade effect?
P.S - If there is an easier way you know of how to achieve the flash, please tell me!
Thanks in advance,
Ricky.
Why use another window? It looks like you're trying to use CoreAnimation already so why not just add a white CALayer to your existing view and animate its opacity?