I have looked at every other answer on here (and across the web) to a situation like mine and have yet to remedy the issue. Essentially what is occurring is I am animating an image-containing CALayer across a quadratic bezier curve. The animation works perfectly, but I really need to implement post-animation behavior, and every attempt I have made at doing so has failed. Here is my code:
UIBezierPath *path1 = [UIBezierPath bezierPath];
[path1 moveToPoint:P(67, 301)];
[path1 addQuadCurveToPoint:P(254, 301) controlPoint:P(160, 93)];
CALayer *ball1 = [CALayer layer];
ball1.bounds = CGRectMake(67, 301, 16.0, 16.0);
ball1.position = P(67, 301);
ball1.contents = (id)([UIImage imageNamed:#"turqouiseBallImage.png"].CGImage);
[self.view.layer addSublayer:ball1];
CAKeyframeAnimation *animation1 = [CAKeyframeAnimation animationWithKeyPath:#"position"];
animation1.path = path1.CGPath;
animation1.rotationMode = kCAAnimationRotateAuto;
animation1.repeatCount = 2.0;
animation1.duration = 5.0;
[ball1 addAnimation:animation1 forKey:#"ball1"];
[animation1 setDelegate:self];
I'm trying to set the delegate of the animation to self (my primary view controller; this is all in viewDidLoad), whose code implements the animationDidStop method:
(interface) (only showing relevant code)
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag;
(implementation)
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag; {
NSLog(#"animation did stop");
}
I do not see what I am doing that is causing the method not to be called. From my understanding (I am rather new to iOS programming, granted), the CAKeyFrameAnimation animation1 automatically sends the animationDidStop method to its designated delegate when it ceases to animate, which is precisely why I can't identify an issue.
Every other solution or alternative that I have seen has either not worked or suggested using block animation, for which I could not find a way to animate over a bezier curve.
Set the animation's delegate before calling -[CALayer addAnimation:forKey:].
Note the comment in CALayer.h:
The animation is copied before being added to the layer, so any
subsequent modifications to anim will have no affect unless it is
added to another layer.
Related
I am running into an issue when I create an explicit animation to change the value of a CAShapeLayer's path from an ellipse to a rect.
In my canvas controller I setup a basic CAShapeLayer and add it to the root view's layer:
CAShapeLayer *aLayer;
aLayer = [CAShapeLayer layer];
aLayer.frame = CGRectMake(100, 100, 100, 100);
aLayer.path = CGPathCreateWithEllipseInRect(aLayer.frame, nil);
aLayer.lineWidth = 10.0f;
aLayer.strokeColor = [UIColor blackColor].CGColor;
aLayer.fillColor = [UIColor clearColor].CGColor;
[self.view.layer addSublayer:aLayer];
Then, when I animate the path I get a strange glitch / flicker in the last few frames of the animation when the shape becomes a rect, and in the first few frames when it animates away from being a rect. The animation is set up as follows:
CGPathRef newPath = CGPathCreateWithRect(aLayer.frame, nil);
[CATransaction lock];
[CATransaction begin];
[CATransaction setAnimationDuration:5.0f];
CABasicAnimation *ba = [CABasicAnimation animationWithKeyPath:#"path"];
ba.autoreverses = YES;
ba.fillMode = kCAFillModeForwards;
ba.repeatCount = HUGE_VALF;
ba.fromValue = (id)aLayer.path;
ba.toValue = (__bridge id)newPath;
[aLayer addAnimation:ba forKey:#"animatePath"];
[CATransaction commit];
[CATransaction unlock];
I have tried many different things like locking / unlocking the CATransaction, playing with various fill modes, etc...
Here's an image of the glitch:
http://www.postfl.com/outgoing/renderingglitch.png
A video of what I am experiencing can be found here:
http://vimeo.com/37720876
I received this feedback from the quartz-dev list:
David Duncan wrote:
Animating the path of a shape layer is only guaranteed to work when
you are animating from like to like. A rectangle is a sequence of
lines, while an ellipse is a sequence of arcs (you can see the
sequence generated by using CGPathApply), and as such the animation
between them isn't guaranteed to look very good, or work well at all.
To do this, you basically have to create an analog of a rectangle by
using the same curves that you would use to create an ellipse, but
with parameters that would cause the rendering to look like a
rectangle. This shouldn't be too difficult (and again, you can use
what you get from CGPathApply on the path created with
CGPathAddEllipseInRect as a guide), but will likely require some
tweaking to get right.
Unfortunately this is a limitation of the otherwise awesome animatable path property of CAShapeLayers.
Basically it tries to interpolate between the two paths. It hits trouble when the destination path and start path have a different number of control points - and curves and straight edges will have this problem.
You can try to minimise the effect by drawing your ellipse as 4 curves instead of a single ellipse, but it still isn't quite right. I haven't found a way to go smoothly from curves to polygons.
You may be able to get most of the way there, then transfer to a fade animation for the last part - this won't look as nice, though.
So, I am fairly new to iOS programming, and have inherited a project from a former coworker. We are building an app that contains a gauge UI. When data comes in, we want to smoothly rotate our "layer" (which is a needle image) from the current angle to a new target angle. Here is what we have, which worked well with slow data:
-(void) MoveNeedleToAngle:(float) target
{
static float old_Value = 0.0;
CABasicAnimation *rotateCurrentPressureTick = [CABasicAnimation animationWithKeyPath:#"transform.rotation");
[rotateCurrentPressureTick setDelegate:self];
rotateCurrentPressureTick.fromValue = [NSSNumber numberWithFloat:old_value/57.2958];
rotateCurrentPressureTick.removedOnCompletion=NO;
rotateCurrentPressureTick.fillMode=kCAFillModeForwards;
rotateCurrentPressureTick.toValue=[NSSNumber numberWithFloat:target/57.2958];
rotateCurrentPressureTick.duration=3; // constant 3 second sweep
[imageView_Needle.layer addAnimation:rotateCurrentPressureTick forKey:#"rotateTick"];
old_Value = target;
}
The problem is we have a new data scheme in which new data can come in (and the above method called) faster, before the animation is complete. What's happening I think is that the animation is restarted from the old target to the new target, which makes it very jumpy.
So I was wondering how to modify the above function to add a continuous/restartable behavior, as follows:
Check if the current animation is in progress and
If so, figure out where the current animation angle is, and then
Cancel the current and start a new animation from the current rotation to the new target rotation
Is it possible to build that behavior into the above function?
Thanks. Sorry if the question seems uninformed, I have studied and understand the above objects/methods, but am not an expert.
Yes you can do this using your existing method, if you add this bit of magic:
- (void)removeAnimationsFromView:(UIView*)view {
CALayer *layer = view.layer.presentationLayer;
CGAffineTransform transform = layer.affineTransform;
[layer removeAllAnimations];
view.transform = transform;
}
The presentation layer encapsulates the actual state of the animation. The view itself doesn't carry the animation state properties, basically when you set an animation end state, the view acquires that state as soon as you trigger the animation. It is the presentation layer that you 'see' during the animation.
This method captures the state of the presentation layer at the exact moment you cancel the animation, and then applies that state to the view.
Now you can use this method in your animation method, which will look something like this:
-(void) MoveNeedleToAngle:(float) target{
[self removeAnimationsFromView:imageView_Needle];
id rotation = [imageView_Needle valueForKeyPath:#"layer.transform.rotation.z"];
CGFloat old_value = [rotation floatValue]*57.2958;
// static float old_value = 0.0;
CABasicAnimation *rotateCurrentPressureTick = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
[rotateCurrentPressureTick setDelegate:self];
rotateCurrentPressureTick.fromValue = [NSNumber numberWithFloat:old_value/57.2958];
rotateCurrentPressureTick.removedOnCompletion=NO;
rotateCurrentPressureTick.fillMode=kCAFillModeForwards;
rotateCurrentPressureTick.toValue=[NSNumber numberWithFloat:target/57.2958];
rotateCurrentPressureTick.duration=3; // constant 3 second sweep
[imageView_Needle.layer addAnimation:rotateCurrentPressureTick forKey:#"rotateTick"];
old_value = target;
}
(I have made minimal changes to your method: there are a few coding style changes i would also make, but they are not relevant to your problem)
By the way, I suggest you feed your method in radians, not degrees, that will mean you can remove those 57.2958 constants.
You can get the current rotation from presentation layer and just set the toValue angle. No need to keep old_value
-(void) MoveNeedleToAngle:(float) targetRadians{
CABasicAnimation *animation =[CABasicAnimation animationWithKeyPath:#"transform.rotation"];
animation.duration=5.0;
animation.fillMode=kCAFillModeForwards;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
animation.removedOnCompletion=NO;
animation.fromValue = [NSNumber numberWithFloat: [[layer.presentationLayer valueForKeyPath: #"transform.rotation"] floatValue]];
animation.toValue = [NSNumber numberWithFloat:targetRadians];
// layer.transform= CATransform3DMakeRotation(rads, 0, 0, 1);
[layer addAnimation:animation forKey:#"rotate"];
}
Another way i found (commented line above) is instead of using fromValue and toValue just set the layer transform. This will produce the same animation but the presentationLayer and the model will be in sync.
I'm using CAKeyframeAnimation to move CALayer on a circle trajectory. Sometimes I need to stop animation and to move animation to the point at which the animation stopped. Here the code:
CAKeyframeAnimation* circlePathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
CGMutablePathRef circularPath = the circle path;
circlePathAnimation.path = circularPath;
circlePathAnimation.delegate = self;
circlePathAnimation.duration = 3.0f;
circlePathAnimation.repeatCount = 1;
circlePathAnimation.fillMode = kCAFillModeForwards;
circlePathAnimation.removedOnCompletion = NO;
circlePathAnimation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:.43 :.78 :.69 :.99];
[circlePathAnimation setValue:layer forKey:#"animationLayer"];
[layer addAnimation:circlePathAnimation forKey:#"Rotation"];
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
CALayer *layer = [anim valueForKey:#"animationLayer"];
if (layer) {
CALayer *presentationLayer = layer.presentationLayer;
layer.position = presentationLayer.position;
}
}
But the presentation layer has no changes in position!!! I read that it is no longer reliable from ios 8. So is there any other way I can find the current position of animated layer?
I wasn't aware that the position property of the presentation layer stopped telling you the location of your layer in iOS 8. That's interesting. Hit testing using the hitTest method on the presentation layer still works, I just tried it on an app I wrote a while back.
Pausing and resuming an animation also still works.
For a simple path like a circle path you could check the time of the animation and then calculate the position using trig.
For more complex paths you'd have to know about the path your layer was traversing. Plotting a single cubic or quadratic bezier curve over time is pretty straightforward, but a complex UIBezierPath/CGPath with a mixture of different shapes in it would be a bear to figure out.
In Core Animation Programming guide, there is one paragraph about How to Animate Layer-Backed Views, it says:
If you want to use Core Animation classes to initiate animations, you must issue all of your Core Animation calls from inside a view-based animation block. The UIView class disables layer animations by default but reenables them inside animation blocks. So any changes you make outside of an animation block are not animated.
There are also an example:
[UIView animateWithDuration:1.0 animations:^{
// Change the opacity implicitly.
myView.layer.opacity = 0.0;
// Change the position explicitly.
CABasicAnimation* theAnim = [CABasicAnimation animationWithKeyPath:#"position"];
theAnim.fromValue = [NSValue valueWithCGPoint:myView.layer.position];
theAnim.toValue = [NSValue valueWithCGPoint:myNewPosition];
theAnim.duration = 3.0;
[myView.layer addAnimation:theAnim forKey:#"AnimateFrame"];
}];
In my opinion, it tells that if I don't issue Core Animation calls from inside a view-based animation block, there will no animation.
But it seems that if I add the core animation calls directly without view-based animation block, it works the same.
Have I missed something ?
tl;dr: The documentation only refers to implicit animations. Explicit animations work fine outside of animation blocks.
My paraphrasing of the documentation
The simplified version of that quote from the docs is something like (me paraphrasing it):
UIView have disabled implicit animations except for within animation blocks. If you want to do implicit layer animations you must do them inside an animation block.
What is implicit animations and how do they work?
Implicit animations is what happens when an animatable property of a standalone layer changes. For example, if you create a layer and change it's position it's going to animate to the new position. Many, many layer properties have this behaviour by default.
It happens something like this:
a transaction is started by the system (without us doing anything)
the value of a property is changed
the layer looks for the action for that property
at some point the transaction is committed (without us doing anything)
the action that was found is applied
Notice that there is no mention of animation above, instead there is the word "action". An action in this context refers to an object which implements the CAAction protocol. It's most likely going to be some CAAnimation subclass (like CABasicAnimation, CAKeyframeAnimation or CATransition) but is built to work with anything that conforms to that protocol.
How does it know what "action" to take?
Finding the action for that property happens by calling actionForKey: on the layer. The default implementation of this looks for an action in this order:
This search happens in this order (ref: actionForKey: documentation)
If the layer has a delegate and that delegate implements the Accessing the Layer’s Filters method, the layer calls that method. The delegate must do one of the following:
Return the action object for the given key.
Return nil if it does not handle the action.
Return the NSNull object if it does not handle the action and the search should be terminated.
The layer looks in the layer’s actions dictionary.
The layer looks in the style dictionary for an actions dictionary that contains the key.
The layer calls its defaultActionForKey: method to look for any class-defined actions.
The layer looks for any implicit actions defined by Core Animation.
What is UIView doing?
In the case of layers that are backing views, the view can enable or disable the actions by implementing the delegate method actionForLayer:forKey. For normal cases (outside an animation block) the view disables the implicit animations by returning [NSNull null] which means:
it does not handle the action and the search should be terminated.
However, inside the animation block, the view returns a real action. This can easily be verified by manually invoking actionForLayer:forKey: inside and outside the animation block. It could also have returned nil which would cause the layer to keep looking for an action, eventually ending up with the implicit actions (if any) if it wouldn't find anything before that.
When an action is found and the transaction is committed the action is added to the layer using the regular addAnimation:forKey: mechanism. This can easily be verified by creating a custom layer subclass and logging inside -actionForKey: and -addAnimation:forKey: and then a custom view subclass where you override +layerClass and return the custom layer class. You will see that the stand alone layer instance logs both methods for a regular property change but the backing layer does not add the animation, except when within a animation block.
Why this long explanation of implicit animations?
Now, why did I give this very long explanation of how implicit animations work? Well, it's to show that they use the same methods that you use yourself with explicit animations. Knowing how they work, we can understand what it means when the documentation say: "The UIView class disables layer animations by default but reenables them inside animation blocks".
The reason why explicit animations aren't disabled by what UIView does, is that you are doing all the work yourself: changing the property value, then calling addAnimation:forKey:.
The results in code:
Outside of animation block:
myView.backgroundColor = [UIColor redColor]; // will not animate :(
myLayer.backGroundColor = [[UIColor redColor] CGColor]; // animates :)
myView.layer.backGroundColor = [[UIColor redColor] CGColor]; // will not animate :(
[myView.layer addAnimation:myAnimation forKey:#"myKey"]; // animates :)
Inside of animation block:
myView.backgroundColor = [UIColor redColor]; // animates :)
myLayer.backGroundColor = [[UIColor redColor] CGColor]; // animates :)
myView.layer.backGroundColor = [[UIColor redColor] CGColor]; // animates :)
[myView.layer addAnimation:myAnimation forKey:#"myKey"]; // animates :)
You can see above that explicit animations and implicit animations on standalone layers animate both outside and inside of animation blocks but implicit animations of layer-backed views does only animate inside the animation block.
I will explain it with a simple demonstration (example).
Add below code in your view controller.(don't forget to import QuartzCore)
#implementation ViewController{
UIView *view;
CALayer *layer;
}
- (void)viewDidLoad
{
[super viewDidLoad];
view =[[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
[self.view addSubview:view];
view.backgroundColor =[UIColor greenColor];
layer = [CALayer layer];
layer.frame =CGRectMake(0, 0, 50, 50);
layer.backgroundColor =[UIColor redColor].CGColor;
[view.layer addSublayer:layer];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
layer.frame =CGRectMake(70, 70, 50, 50);
}
See that in toucesBegan method there is no UIVIew animate block.
When you run the application and click on the screen, the opacity of the layer animates.This is because by default these are animatable.By default all the properties of a layer are animatable.
Consider now the case of layer backed views.
Change the code to below
- (void)viewDidLoad
{
[super viewDidLoad];
view =[[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
[self.view addSubview:view];
view.backgroundColor =[UIColor greenColor];
// layer = [CALayer layer];
// layer.frame =CGRectMake(0, 0, 50, 50);
// layer.backgroundColor =[UIColor redColor].CGColor;
// [view.layer addSublayer:layer];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
view.layer.opacity =0.0;
// layer.frame =CGRectMake(70, 70, 50, 50);
}
You might think that this will also animate its view.But it wont happen.Because layer backed views by default are not animatable.
To make those animations happen, you have to explicitly embed the code in UIView animate block.As shown below,
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
[UIView animateWithDuration:2.0 animations:^{
view.layer.opacity =0.0;
}];
// layer.frame =CGRectMake(70, 70, 50, 50);
}
That explains that,
The UIView class (which obviously was layer backed) disables layer animations by default but reenables them inside animation blocks.So any changes you make outside of an animation block are not animated.
Well, I've never used explicit Core Animation inside a view animation block. The documentation it seems to be not clear at all.
Probably the meaning is something like that, it's just a guess:
If you want to animate properties of the view that are linked to the
backed layer you should wrap them into a view animation block. In the
view's layer if you try to change the layer opacity this is not
animated, but if wrap into a view animation block it is.
In your snippet you are directly creating a basic animation thus explicitly creating an animation. Probably the doc just want to point out the differences between views and layers. In the latter animations on most properties are implicit.
You can see the difference if you write something like that:
[UIView animateWithDuration:1.0 animations:^{
// Change the opacity implicitly.
myView.layer.opacity = 0.0;
}];
This will be animated.
myView.layer.opacity = 0.0;
This will not be animated.
// Change the position explicitly.
CABasicAnimation* theAnim = [CABasicAnimation animationWithKeyPath:#"position"];
theAnim.fromValue = [NSValue valueWithCGPoint:myView.layer.position];
theAnim.toValue = [NSValue valueWithCGPoint:myNewPosition];
theAnim.duration = 3.0;
[myView.layer addAnimation:theAnim forKey:#"AnimateFrame"];
This will be animated.
I am trying to animate a CAEmitterLayer's emitterPosition like this:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"emitterPosition.x"] ;
animation.toValue = (id) toValue ;
animation.removedOnCompletion = NO ;
animation.duration = self.translationDuration ;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut] ;
animation.completion = ^(BOOL finished)
{
[self animateToOtherSide] ;
} ;
[_emitterLayer addAnimation:animation forKey:#"emitterPosition"] ;
CGPoint newEmitterPosition = CGPointMake(toValue.floatValue, self.bounds.size.height/2.0) ;
_emitterLayer.emitterPosition = newEmitterPosition ;
Note that animation.completion is declared in a category that just calls the corresponding CAAnimation delegate method.
The problem is that this doesn't animate at all and shows the emitter in its final position. I was under the impression that once you add the animation to the layer, you should change the actual model behind it to its final position so that when the animation completes the model is in its final state; i.e., to prevent the animation from "snapping back" to its original position.
I have tried placing the last two lines in the animation.completion block, and this does indeed animate as expected. However, when the animation finishes some particles are intermittently emitted at the emitter's original position. If you put the system under load (for example, scrolling a tableview while the animation is playing), this happens more often.
Another solution I was thinking about is to not move the emitterPosition at all but just move the CAEmitterLayer itself, although I haven't tried that yet.
Thanks in advance for any help you can provide.
Perhaps emitterPosition.x is not a valid key path for animation. Try using emitterPosition instead (and so you'll have to provide CGPoint values wrapped up in an NSValue).
I just tried this on my own machine and it works fine:
CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:#"emitterPosition"];
ba.fromValue = [NSValue valueWithCGPoint:CGPointMake(30,100)];
ba.toValue = [NSValue valueWithCGPoint:CGPointMake(200,100)];
ba.duration = 6;
ba.autoreverses = YES;
ba.repeatCount = HUGE_VALF;
[emit addAnimation:ba forKey:nil];
Other things to think about:
You can typically use nil as the key in addAnimation:forKey:, unless you're going to need to find this animation later (e.g. to remove it or override it in some way). The key path is the important thing.
Setting removedOnCompletion to NO is almost always wrong and is typically the last refuge of a scoundrel (i.e. due to not understanding how animation works).
If, as you say, setting _emitterLayer.emitterPosition = newEmitterPosition inside the completion block does animate, then when why are you using CABasicAnimation at all? Why not just call UIView animate... and set the emitterPosition in the animations block? If that works, it will kill two birds with one stone, moving the position and animating it too.