I'm generating CALayer when user taps the screen. Then I'm translating that layer to certain position using Animation. Then I'm removing it using this code in animationDidStop:
[mylayer removeFromSuperLayer];
Here everything is working fine, but when I tap again before the previous animation stops, my current layer is not removed from the superlayer. How do I removed it under these circumstances?
If you are creating a new layer each time, then the delegate method will only be able to remove the current one (i.e. the older one will be lost)
You could try using CATransaction begin/commit pairs around your animations and adding completion block, this way you can pass the layers reference for each animation
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[myLayer removeFromSuperlayer];
}];
//your existing animation code
[CATransaction commit];
Related
I have an odd problem in one of my chain animations. I have block object for each of the blocks on my screen (26 of them), and when the user presses them I perform a flip. It works great, but I'm adding a scale animation before I do these to make them about 50% bigger on the screen. So, my sequence is:
enlarge
delay (let user see the large image)
spin-out 90% (removes block from view) - using transform.rotation.y
change image & spin-in
shrink.
I have setup the window view controller to be a delegate of these blocks, such that it can pass a counter to the blocks so I can position the right sublayer on the top (using setZposition).
It all works great, except when I have 2 blocks on positioned above/below each other on the screen, such that the enlarge will cause them to overlap, and then when the spin-out animation starts, it immediately has the right side of the block pop behind the block below it. I've tried changing the animation to transform.rotation.x and get the same behavior when the blocks are side-to-side.
I'm not sure if it's an iOS bug of if I'm just not doing something correct. Any suggestions are greatly appreciated. Here is the spin-out method:
- (void)spinOut:(id)sender
{
NSTimeInterval animationTime=0.85;
[UIView animateWithDuration:animationTime
delay:0
options: UIViewAnimationOptionCurveLinear
animations:^{
// setup the animation to spin the current view out
[CATransaction begin];
[CATransaction setDisableActions:YES];
CABasicAnimation *spinOut = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
[spinOut setDelegate:self];
[spinOut setDuration:animationTime];
CAMediaTimingFunction *tf = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[spinOut setTimingFunction:tf];
[spinOut setFromValue:[NSNumber numberWithFloat:M_PI * 0.0]];
[spinOut setToValue:[NSNumber numberWithFloat:M_PI * 0.5]];
[spinOut setRemovedOnCompletion:NO];
[spinOut setFillMode:kCAFillModeForwards];
// setup variables used to roll in the next view on animation completion.
[pageView.layer addAnimation:spinOut forKey:kMyVeryOwnABCsSpinOutKey];
[CATransaction commit];
}
completion:^(BOOL finished){
[self setFlipOutAnimationTimer:[NSTimer scheduledTimerWithTimeInterval:animationTime target:self selector:#selector(spinIn:) userInfo:nil repeats:NO]];
}];
}
OK - this is an old question, but I kindof figured out this problem. Since I was flipping these images around the Y axis, I was able to change the animated image's Zposition. In my startAnimation method, I added:
float pageLevelLayerPosition;
// logic to set the zposition variable 100 more than anything nearby
_originalImageViewZPosition = bigImageView.layer.zPosition;
[bigImageView.layer setZPosition:pageLevelLayerPosition];
and then when in the animationDidStop, I added the relevant line of code to reset it back to 0 where I was removing the animation. I had some code to manage the variable so that I was assured that the number was greater than anything nearby.
I guess if I ever get into 3-D animation, then I'll need to control this much more closely, but that is what was causing my problem.
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.
I set a few CALayer transform and bounds modifications, within a CATransaction. However, regardless the method I use (key-value, setAnimationDuration) there is no animation, the changes are done, but immediately without transition.
Do you have any idea why?
Thanks!
/* CALayer*layer=[CALayer layer];
layer.bounds =AnUIImageView.bounds;
layer.contents=AnUIImageView.layer.contents;
[AnotherUIImageView.layer addSublayer:layer];
CGPoint thecentre=AnUIImageView.center;
CALayer* layerInTarget=[AnotherUIImageView.layer.sublayers lastObject];
[layerInTarget setPosition:[self.view convertPoint:thecentre toView:AnotherUIImageView]];
AnUIImageView.layer.hidden=YES;
*/ // the code above works, i show it to be complete
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:2.0f]
forKey:kCATransactionAnimationDuration];
layerInTarget.position=[self.view convertPoint:AnotherUIImageView.center toView:AnotherUIImageView];
layerInTarget.transform=CATransform3DMakeScale(0.6,0.6,0.6);
[CATransaction commit];
CALayers that are associated with a UIView (as in, they're accessed via view.layer) do not participate in implicit animations, regardless of how you configure your CATransaction. You either need to use explicit animations (using the appropriate subclass of CAAnimation) or you need to use UIView animations.
Is it possible to perform animations with Core Animation using blocks on the Mac similarly to how one can do it on iOS? I'd like to be able to set up completion blocks at the end of an animation to remove views, etc. I know that this can be achieved with delegates, but obviously the whole point of blocks with animations is to avoid that pain.
CATransaction + (void)setCompletionBlock:(void (^)(void))block
The block object called when animations for this transaction group are completed.
[CATransaction begin];
[CATransaction setAnimationDuration:5.0];
[CATransaction setCompletionBlock:^{
// this will be done when animation has completed
}];
//do some things to your layers
[CATransaction commit];
Twitter is offering TwUI that uses Core Animation with Blocks. It might help you.
https://github.com/twitter/twui
https://github.com/twitter/twui/blob/master/lib/UIKit/TUIView+Animation.m
#interface TUIViewAnimation : NSObject <CAAction>
{
/* snip */
void (^animationCompletionBlock)(BOOL finished);
Sometimes. NSAnimationContext and NSAnimationGroup both have completionHandler properties to which you can assign blocks, but many others don't.
I am using a CALayer to display a path via drawLayer:inContext delegate method, which resides in the view controller of the view that the layer belongs to. Each time the user moves their finger on the screen the path is updated and the layer is redrawn. However, the drawing doesn't keep up with the touches: there is always a slight lag in displaying the last two points of the path. It also flickers, but only while displaying the last two-three points again. If I just do the drawing in the view's drawRect, it works fine and the drawing is definitely fast enough.
Does anyone know why it behaves like this? I suspect it is something to do with the layer buffering, but I couldn't find any documentation about it.
[UIView new] is simply shorthand for [[UIView alloc] init].
Give the following before setNeedsDisplay method::
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:someDelay] forKey:kCATransactionAnimationDuration];
[aLayer setNeedsDisplay];
[CATransaction commit];
You might have better luck using a layer hosting view.
Instead of using the drawLayer:inContext: method, setup the view you want and add a CALayer to it:
UIView *layerHosting = [[UIView alloc] initWithFrame:frame];
[layerHosting setLayer:[[CALayer new] autorelease]];
Hope that helps!