How can I know current animation in CAAnimationGroups? - ios

Now I want to use Core Animation to write some methods for animation just as easy as Cocos2d ,just like ccMove,ccFade,ccRotate.
Everything seems ok, but when I want to implement the animation sequence,I've troubled.The first thing I've thought was CCAnimationGroup,but when animations in groups,I can not kown when the current action complete, so I can't set the property of layer, this lead to the layer back to the original status. Anyone know this ? My code:
UIView *testView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)];
testView.backgroundColor= [UIColor redColor];
testView.center = self.view.center;
//mark1: textview's position is self.view.center
[self.view addSubview:testView];
CAKeyframeAnimation *moveAnimation = [CAKeyframeAnimation
animationWithKeyPath:#"position"];
moveAnimation.duration = 3;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 100, 100);
CGPathAddLineToPoint(path, NULL, 200, 200);
moveAnimation.path = path;
//mark2: I want the testview move to CGPointMake(100,200)
CGPathRelease(path);
CABasicAnimation *angleAnimation =
[CABasicAnimation
animationWithKeyPath:#"transform.rotation"];
angleAnimation.toValue = #(45 * M_PI / 180);
angleAnimation.duration = 3;
angleAnimation.beginTime = 3; //make3: rotate testview 45°
CAAnimationGroup *actionGroup = [CAAnimationGroup animation];
[actionGroup setDuration:6];
[actionGroup setAnimations:#[moveAnimation, angleAnimation]];
[CATransaction begin];
[testView.layer addAnimation:actionGroup forKey:nil];
[CATransaction commit];
You can run it, and you'll see the textview's position and rotate is not I want.
Can someone help me?

I've solved it.In fact,I don't need to know when one of the actions in group complete.Just set the animation's fillMode and removedOnCompletion, so that the the layer would not back to original status.
UIView *testView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)];
testView.backgroundColor= [UIColor redColor];
testView.center = self.view.center;
[self.view addSubview:testView]; //mark1: textview's position is self.view.center
CAKeyframeAnimation *moveAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
moveAnimation.duration = 3;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 100, 100);
CGPathAddLineToPoint(path, NULL, 200, 200);
moveAnimation.path = path;
moveAnimation.fillMode = kCAFillModeForwards;
moveAnimation.removedOnCompletion = NO;
CGPathRelease(path); //mark2: I want the testview move to CGPointMake(100,200)
CABasicAnimation *angleAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
angleAnimation.toValue = #(45 * M_PI / 180);
angleAnimation.duration = 3;
angleAnimation.beginTime = 3; //make3: rotate testview 45°
angleAnimation.fillMode = kCAFillModeForwards;
angleAnimation.removedOnCompletion = NO;
CAAnimationGroup *actionGroup = [CAAnimationGroup animation];
[actionGroup setDuration:6];
[actionGroup setAnimations:#[moveAnimation, angleAnimation]];
actionGroup.fillMode = kCAFillModeForwards;
actionGroup.removedOnCompletion = NO;
[CATransaction begin];
[testView.layer addAnimation:actionGroup forKey:nil];
[CATransaction commit];
In code above, I set all the action's fillmode to kCAFillModeForwards, and removedOnCompletion = NO.This just work. And I also find that scaleAnimation and fadeAnimation should set their fillmode to kCAFillModeBoth so that the layer cannot back to original property status.
You can also check the code on my github.https://github.com/Vienta/SSAnimation Thank you!

CAAnimationGroup is subclass of CAAnimation, so you can have a delegate of CAAnimationGroup and receive notification when animation stoped.
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag

Related

How to remove strange delay between two sequential CAKeyframeAnimation animations?

iOs-coders!
This code to animate the little red square to drawing big sign "8" on the UIView. First an upper ring (animSequence1 = 5 sec), then right away a lower ring (animSequence2 = another 5 sec).
No delay needed!
But I stucked on strange delay (about 1 sec) between two sequential animations. Whence did this delay come from?!?
Code:
- (void)drawRect:(CGRect)drawRect {
CGContextRef context = UIGraphicsGetCurrentContext();
drawRect = CGRectMake(0, 0, 320, 320);
CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor;);
CGContextFillRect(context, drawRect);
// Little red square
CALayer *layerTest;
layerTest = [CALayer layer];
layerTest.bounds = CGRectMake(0, 0, 20, 20);
layerTest.anchorPoint = CGPointMake(0, 0);
layerTest.backgroundColor = [UIColor redColor].CGColor;
[[self layer] addSublayer:layerTest];
// Upper ring
CAKeyframeAnimation *animSequence1;
animSequence1 = [CAKeyframeAnimation animationWithKeyPath:#"position"];
animSequence1.fillMode = kCAFillModeForwards;
animSequence1.removedOnCompletion = NO;
animSequence1.autoreverses = NO;
animSequence1.repeatCount = 0;
animSequence1.duration = 5.0;
animSequence1.beginTime = 0.0;
animSequence1.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(200, 120) radius:80 startAngle:DEG_TO_RAD(90) endAngle:DEG_TO_RAD(450) clockwise:YES].CGPath;
animSequence1.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
// Lower ring
CAKeyframeAnimation *animSequence2;
animSequence2 = [CAKeyframeAnimation animationWithKeyPath:#"position"];
animSequence2.fillMode = kCAFillModeForwards;
animSequence2.removedOnCompletion = NO;
animSequence2.autoreverses = NO;
animSequence2.repeatCount = 0;
animSequence2.duration = 5.0;
animSequence2.beginTime = 5.0;
animSequence2.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(200, 280) radius:80 startAngle:DEG_TO_RAD(-90) endAngle:DEG_TO_RAD(-450) clockwise:NO].CGPath;
animSequence2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
// A sequence of animations
CAAnimationGroup *animGroup;
layerTest.position = CGPointMake(200, 200);
animGroup = [CAAnimationGroup animation];
animGroup.duration = 10.0;
animGroup.animations = [NSArray arrayWithObjects:animSequence1, animSequence2, nil];
[layerTest addAnimation:animGroup forKey:nil];
}
Also I tried make it without CAAnimationGroup.
Set up [animSequence1 setDelegate:self]; for first CAKeyframeAnimation (animSequence1), and then start second CAKeyframeAnimation (animSequence2) by (void)animationDidStop.
It worked same, but this strange delay (about 1 sec) between two animations no dissapear!
QUESTION: How to remove strange delay between two sequential CAKeyframeAnimation animations? No delay needed!
Don’t create layers or animations in -drawRect:—you don’t have a whole lot of control over when it gets called, so you’re going to end up with multiple layerTests added to your view, all trying to animate separately. Create the layer / animation in an initializer, or in response to some user interaction.
I would also advise getting rid of your fillMode and removedOnCompletion settings on animSequence1; once animSequence2 starts, you’re effectively asking CA to run two non-additive animations once on the same property. Not sure whether the behavior for that is well-defined, but it’s another place the weirdness could be coming from.

How to change the rectangle shape to a circle shape with animation

I have a label:
self.logInLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 300, 100)];
self.logInLabel.backgroundColor = [UIColor clearColor];
self.logInLabel.backgroundColor = Color_Circle_Outer;
self.logInLabel.textColor = Color_Circle_Outer;
self.logInLabel.textAlignment = NSTextAlignmentCenter;
self.logInLabel.lineBreakMode = NSLineBreakByWordWrapping;
self.logInLabel.numberOfLines = 1;
self.logInLabel.font = [UIFont boldSystemFontOfSize:12*IPAD];
self.logInLabel.text = #"Log In";
[self addSubview:self.logInLabel];
self.logInLabel.layer.cornerRadius = self.frame.size.height/2.0;
self.logInLabel.layer.borderColor = Color_Circle_Outer.CGColor;
self.logInLabel.layer.borderWidth = 2*IPAD;
self.logInLabel.layer.masksToBounds = YES;
I want to change this shape to a Circle. I have tried:
[UIView animateWithDuration:AnimationTime animations:^{
self.logInLabel.layer.bounds = CGRectMake(0, 0, self.frame.size.height, self.frame.size.height);
}];
But the shape changed immediately without animation, then the position change to center with animation. Then I tried with this:
[UIView animateWithDuration:AnimationTime animations:^{
self.logInLabel.bounds = CGRectMake(0, 0, self.logInLabel.frame.size.height, self.logInLabel.frame.size.height);
}];
I have very ugly animation effect.
I do not care if this view is a label, I just want to implement changing a rectangle shape to a circle shape with an animation.
Thank you.
It works only CABasicAnimation not the UIView animateWithDuration
UIView *logInLabel = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
logInLabel.backgroundColor = [UIColor blackColor];
logInLabel.layer.masksToBounds = YES;
[self.view addSubview:logInLabel];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"cornerRadius"];
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
animation.fromValue = [NSNumber numberWithFloat:0.0f];
animation.toValue = [NSNumber numberWithFloat:100.0f];
animation.duration = 5.0;
[logInLabel.layer addAnimation:animation forKey:#"cornerRadius"];
[logInLabel.layer setCornerRadius:0.0];
From your code remove the line :
self.logInLabel.layer.cornerRadius = self.logInLabel.frame.size.height / 2.0;
and put it in an animation block like this
You can set cornerRadius of your UILabel to get a circular view.
[UIView animateWithDuration:AnimationTime animations:^{
self.logInLabel.layer.cornerRadius = self.logInLabel.frame.size.height / 2.0f;
}];
However, the animation will not look very good and the final frame will not be an exact circle because the height and width of your UILabel are not in 1:1 proportion.
If you still don't get the animation try to check the value of AnimationTime variable.
Look that might help you
[UIView animateWithDuration:0.4 animations:^{
CGPoint center = self.logInLabel.center;
self.logInLabel.layer.cornerRadius = self.logInLabel.frame.size.height/2;
self.logInLabel.frame = CGRectMake(0, 0, self.logInLabel.frame.size.height, self.logInLabel.frame.size.height);
self.logInLabel.center = center;
}];

CAEmitterLayer - kCAEmitterLayerCircle, circular path?

I'm trying to recreate this effect (see image below), but with a circular path as I have a round button.
Here's the source.
https://github.com/uiue/ParticleButton/blob/master/ParticleButton/EmitterView.m
The modifications I've made seem to affect the method that the particle is animated, not the path.
I haven't researched the technology employed in depth, however I don't believe it's possible?
fireEmitter = (CAEmitterLayer *)self.layer;
//fireEmitter.renderMode = kCAEmitterLayerAdditive;
//fireEmitter.emitterShape = kCAEmitterLayerLine;
// CGPoint pt = [[touches anyObject] locationInView:self];
float multiplier = 0.25f;
fireEmitter.emitterPosition = self.center;
fireEmitter.emitterMode = kCAEmitterLayerOutline;
fireEmitter.emitterShape = kCAEmitterLayerCircle;
fireEmitter.renderMode = kCAEmitterLayerAdditive;
fireEmitter.emitterSize = CGSizeMake(100 * multiplier, 0);
CustomButton.m
-(void)drawRect:(CGRect)rect
{
//绘制路径
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, self.frame.size.width, self.frame.size.height));
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
animation.duration = 3;
animation.timingFunction = [CAMediaTimingFunction functionWithName:#"easeInEaseOut"];
animation.repeatCount = MAXFLOAT;
// animation.path = path;
animation.path = [UIBezierPath bezierPathWithOvalInRect:rect].CGPath;
[emitterView.layer addAnimation:animation forKey:#"test"];
}
Create a custom button in circle shape.
CustomButton *button = [[CustomButton alloc] initWithFrame:CGRectMake(100, 100, 300, 300)];
[self.view addSubview:button];
button.center = self.view.center;
button.layer.cornerRadius = button.frame.size.width/2.f;
button.clipsToBounds = YES;
You 're just setting the emitter properties there (irrelevant with the animation of its position). The part where the animation is created and added to the button is missing from your question (but is present in the sample project).
There in the drawRect: method the animation is added as follows:
-(void)drawRect:(CGRect)rect
{
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, self.frame.size.width, self.frame.size.height));
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
animation.duration = 3;
animation.timingFunction = [CAMediaTimingFunction functionWithName:#"easeInEaseOut"];
animation.repeatCount = MAXFLOAT;
animation.path = path; // That's what defines the path of the animation
[emitterView.layer addAnimation:animation forKey:#"test"];
}
Which essentially says, take this path (right now a rectangle with the dimensions of the button) and animate the emitterViews (the particles) position along this path for 3 seconds (and repeat forever).
So, a simple change in animation.path to something like this for example could define a circular path for emitter to follow:
animation.path = [UIBezierPath bezierPathWithOvalInRect:rect].CGPath;
(Essentially an oval with the dimensions of the button - which might be what you want, or not - but you get the idea...)
Note: I'm not suggesting that drawRect: is the best place to create and add the animation (it's just based on the sample that you have provided). You could create and add the animation any way you like provided that you have a path matching your button's circular shape.

Why when I set a low duration value for my CABasicAnimation does it jump?

Sample Project: http://cl.ly/1W3V3b0D2001
I'm using CABasicAnimation to create a progress indicator that is like a pie chart. Similar to the iOS 7 app download animation:
The animation is set up as follows:
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
CGFloat radius = CGRectGetWidth(self.frame) / 2;
CGFloat inset = 1;
CAShapeLayer *ring = [CAShapeLayer layer];
ring.path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, inset, inset)
cornerRadius:radius-inset].CGPath;
ring.fillColor = [UIColor clearColor].CGColor;
ring.strokeColor = [UIColor whiteColor].CGColor;
ring.lineWidth = 2;
self.innerPie = [CAShapeLayer layer];
inset = radius/2;
self.innerPie.path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, inset, inset)
cornerRadius:radius-inset].CGPath;
self.innerPie.fillColor = [UIColor clearColor].CGColor;
self.innerPie.strokeColor = [UIColor whiteColor].CGColor;
self.innerPie.lineWidth = (radius-inset)*2;
self.innerPie.strokeStart = 0;
self.innerPie.strokeEnd = 0;
[self.layer addSublayer:ring];
[self.layer addSublayer:self.innerPie];
self.progress = 0.0;
}
The animation is triggered by setting the progress of the view:
- (void)setProgress:(CGFloat)progress animated:(BOOL)animated {
self.progress = progress;
if (animated) {
CGFloat totalDurationForFullCircleAnimation = 0.25;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
self.innerPie.strokeEnd = progress;
pathAnimation.delegate = self;
pathAnimation.fromValue = #([self.innerPie.presentationLayer strokeEnd]);
pathAnimation.toValue = #(progress);
pathAnimation.duration = totalDurationForFullCircleAnimation * ([pathAnimation.toValue floatValue] - [pathAnimation.fromValue floatValue]);
[self.innerPie addAnimation:pathAnimation forKey:#"strokeEndAnimation"];
}
else {
[CATransaction setDisableActions:YES];
[CATransaction begin];
self.innerPie.strokeEnd = progress;
[CATransaction commit];
}
}
However, in cases where I set the progress to something small, such as 0.25, there's a jump in the animation. It goes a little forward clockwise, jumps back, then keeps going forward as normal. It's worth nothing that this does not happen if the duration or progress is set higher.
How do I stop the jump? This code works well in every case except when the progress is very low. What am I doing wrong?
Ah! We should have seen this earlier. The problem is this line in your if (animated) block:
self.innerPie.strokeEnd = progress;
Since innerPie is a Core Animation layer, this causes an implicit animation (most property changes do). This animation is fighting with your own animation. You can prevent this from happening by disabling implicit animation while setting the strokeEnd:
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.innerPie.strokeEnd = progress;
[CATransaction commit];
(Note how setDisableActions: is within the begin/commit.)
Alternatively, you can remove your own animation and just use the automatic one, using something like setAnimationDuration: to change its length.
Original Suggestions:
My guess is that your drawRect: is being called, which resets your strokeEnd value. Those layers should probably not be set up in drawRect: anyway. Try moving that setup to an init method or didMoveToWindow: or similar.
If that's not effective, I would suggest adding log statements to track the value of progress and [self.innerPie.presentationLayer strokeEnd] each time the method is called; perhaps they're not doing what you expect.
Why are you using drawRect:? There should be no drawRect: method in your class.
You should not be using drawRect if you are also using self.layer. Use one or the other, never both.
Change it to something like:
- (id)initWithFrame:(CGRect)frame
{
if (!(self = [super initWithFrame:frame])
return nil;
CGFloat radius = CGRectGetWidth(self.frame) / 2;
CGFloat inset = 1;
CAShapeLayer *ring = [CAShapeLayer layer];
ring.path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, inset, inset)
cornerRadius:radius-inset].CGPath;
ring.fillColor = [UIColor clearColor].CGColor;
ring.strokeColor = [UIColor whiteColor].CGColor;
ring.lineWidth = 2;
self.innerPie = [CAShapeLayer layer];
inset = radius/2;
self.innerPie.path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, inset, inset)
cornerRadius:radius-inset].CGPath;
self.innerPie.fillColor = [UIColor clearColor].CGColor;
self.innerPie.strokeColor = [UIColor whiteColor].CGColor;
self.innerPie.lineWidth = (radius-inset)*2;
self.innerPie.strokeStart = 0;
self.innerPie.strokeEnd = 0;
[self.layer addSublayer:ring];
[self.layer addSublayer:self.innerPie];
self.progress = 0.0;
return self;
}
When travelling within same distance, the smaller the duration, the higher the speed. When you set the duration too small, the speed will be very high, and that's why it looks like jumping.

iOS CAKeyFrameAnimation Scaling Flickers at animation end

In another test of Key Frame animation I am combining moving a UIImageView (called theImage) along a bezier path and scaling larger it as it moves, resulting in a 2x larger image at the end of the path. My initial code to do this has these elements in it to kick off the animation:
UIImageView* theImage = ....
float scaleFactor = 2.0;
....
theImage.center = destination;
theImage.transform = CGAffineTransformMakeScale(1.0,1.0);
CABasicAnimation *resizeAnimation = [CABasicAnimation animationWithKeyPath:#"bounds.size"];
[resizeAnimation setToValue:[NSValue valueWithCGSize:CGSizeMake(theImage.image.size.height*scaleFactor, theImage.image.size.width*scaleFactor)]];
resizeAnimation.fillMode = kCAFillModeBackwards;
resizeAnimation.removedOnCompletion = NO;
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pathAnimation.path = [jdPath path].CGPath;
pathAnimation.fillMode = kCAFillModeBackwards;
pathAnimation.removedOnCompletion = NO;
CAAnimationGroup* group = [CAAnimationGroup animation];
group.animations = [NSArray arrayWithObjects:pathAnimation, resizeAnimation, nil];
group.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
group.removedOnCompletion = NO;
group.duration = duration;
group.delegate = self;
[theImage.layer addAnimation:group forKey:#"animateImage"];
Then, when the animation completes I want to retain the image at the larger size, so I implement:
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
{
theImage.transform = CGAffineTransformMakeScale(scaleFactor,scaleFactor);
}
This all works .. sort of. The problem is that at the end of the animation theImage flickers for a brief moment - just enough to make it look bad. I am guessing that this is the transition at the end of the animation where I set the transform to the new size.
In experimenting with this I tried a slightly different form of the above, but still got the same flicker:
CAKeyframeAnimation *resizeAnimation = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
NSValue* startSizeKey = [NSValue valueWithCATransform3D:CATransform3DScale (theImage.layer.transform, 1.0, 1.0, 1.0)];
NSValue* endSizeKey = [NSValue valueWithCATransform3D:CATransform3DScale (theImage.layer.transform, scaleFactor, scaleFactor, 1.0)];
NSArray* sizeKeys = [NSArray arrayWithObjects:startSizeKey, endSizeKey, nil];
[resizeAnimation setValues:sizeKeys];
....
theImage.transform = CGAffineTransformMakeScale(scaleFactor,scaleFactor);
But when I ended the animation at the same size as the original, there was NO flicker:
CAKeyframeAnimation *resizeAnimation = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
NSValue* startSizeKey = [NSValue valueWithCATransform3D:CATransform3DScale (theImage.layer.transform, 1.0, 1.0, 1.0)];
NSValue* middleSizeKey = [NSValue valueWithCATransform3D:CATransform3DScale (theImage.layer.transform, scaleFactor, scaleFactor, 1.0)];
NSValue* endSizeKey = [NSValue valueWithCATransform3D:CATransform3DScale (theImage.layer.transform, 1.0, 1.0, 1.0)];
NSArray* sizeKeys = [NSArray arrayWithObjects:startSizeKey, middleSizeKey, endSizeKey, nil];
[resizeAnimation setValues:sizeKeys];
....
theImage.transform = CGAffineTransformMakeScale(1.0,1.0);
So my big question is how can I animate this image without the flicker, and end up with a different size at the end of the animation?
Edit March 2nd
My initial tests were with scaling the image up. I just tried scaling it down (IE scaleFactor = 0.4) and the flickering was a lot more visible, and a lot more obvious as to what I am seeing. This was the sequence of events:
Original sized image is painted on the screen at the starting location.
As the image moves along the path it shrinks smoothly.
The fully shrunk image arrives at the end of the path.
The image is then painted at its original size.
The image is finally painted at its shrunken size.
So it seems to be step 4 that is the flickering that I am seeing.
Edit March 22
I have just uploaded to GitHub a demo project that shows off the moving of an object along a bezier path. The code can be found at PathMove
I also wrote about it in my blog at Moving objects along a bezier path in iOS
It can be tricky to animate a view's layer using Core Animation. There are several things that make it confusing:
Setting an animation on a layer doesn't change the layer's properties. Instead, it changes the properties of a “presentation layer” that replaces the original “model layer” on the screen as long as the animation is applied.
Changing a layer's property normally adds an implicit animation to the layer, with the property name as the animation's key. So if you want to explicitly animate a property, you usually want to set the property to its final value, then add an animation whose key is the property name, to override the implicit animation.
A view normally disables implicit animations on its layer. It also mucks around with its layer's properties in other somewhat mysterious ways.
Also, it's confusing that you animate the view's bounds to scale it up, but then switch to a scale transformation at the end.
I think the easiest way to do what you want is to use the UIView animation methods as much as possible, and only bring in Core Animation for the keyframe animation. You can add the keyframe animation to the view's layer after you've let UIView add its own animation, and your keyframe animation will override the animation added by UIView.
This worked for me:
- (IBAction)animate:(id)sender {
UIImageView* theImage = self.imageView;
CGFloat scaleFactor = 2;
NSTimeInterval duration = 1;
UIBezierPath *path = [self animationPathFromStartingPoint:theImage.center];
CGPoint destination = [path currentPoint];
[UIView animateWithDuration:duration animations:^{
// UIView will add animations for both of these changes.
theImage.transform = CGAffineTransformMakeScale(scaleFactor, scaleFactor);
theImage.center = destination;
// Prepare my own keypath animation for the layer position.
// The layer position is the same as the view center.
CAKeyframeAnimation *positionAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
positionAnimation.path = path.CGPath;
// Copy properties from UIView's animation.
CAAnimation *autoAnimation = [theImage.layer animationForKey:#"position"];
positionAnimation.duration = autoAnimation.duration;
positionAnimation.fillMode = autoAnimation.fillMode;
// Replace UIView's animation with my animation.
[theImage.layer addAnimation:positionAnimation forKey:positionAnimation.keyPath];
}];
}
CAAnimations will flicker at the end if the terminal state was assigned in such a way that it itself created an implicit animation. Keep in mind CAAnimations are temporary adjustments of an object properties for the purposes of visualizing transition. When the animation done, if the layer's state is still the original starting state, that is what is going to be displayed ever so temporarily until you set the final layer state, which you do in your animationDidStop: method.
Furthermore, your animation is adjusting the bounds.size property of your layer, so you should similarly set your final state rather than using the transform adjustment as your final state. You could also use the transform property as the animating property in the animation instead of bounds.size.
To remedy this, immediately after assigning the animation, change the layer's permeant state to your desired terminal state so that when the animation completes there will be no flicker, but do so in such a manner to no trigger an implicit animation before the animation begins. Specifically, in your case you should do this at the end of your animation set up:
UIImageView* theImage = ....
float scaleFactor = 2.0;
....
theImage.center = destination;
theImage.transform = CGAffineTransformMakeScale(1.0,1.0);
CGSize finalSize = CGSizeMake(theImage.image.size.height*scaleFactor, theImage.image.size.width*scaleFactor);
CABasicAnimation *resizeAnimation = [CABasicAnimation animationWithKeyPath:#"bounds.size"];
[resizeAnimation setToValue:[NSValue valueWithCGSize:finalSize]];
resizeAnimation.fillMode = kCAFillModeBackwards;
resizeAnimation.removedOnCompletion = NO;
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pathAnimation.path = [jdPath path].CGPath;
pathAnimation.fillMode = kCAFillModeBackwards;
pathAnimation.removedOnCompletion = NO;
CAAnimationGroup* group = [CAAnimationGroup animation];
group.animations = [NSArray arrayWithObjects:pathAnimation, resizeAnimation, nil];
group.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
group.removedOnCompletion = NO;
group.duration = duration;
group.delegate = self;
[theImage.layer addAnimation:group forKey:#"animateImage"];
[CATransaction begin];
[CATransaction setDisableActions:YES];
theImage.bounds = CGRectMake( theImage.bounds.origin.x, theImage.bounds.origin.y, finalSize.width, finalSize.height );
[CATransaction commit];
and then remove the transform adjustment in your animationDidStop: method.
I was experimenting with some CAAnimations this week and was noticing that there was a flickering at the end of my animations. In particular, I would animation from a circle to a square, while changing the fillColor as well.
Each CAAnimation has a property called removedOnCompletion which defaults to YES. This means that the animation will disappear (i.e. transitions, scales, rotations, etc.) when the animation completes and you'll be left with the original layer.
Since you already have set your removedOnCompletion properties to NO, I would suggest trying to shift your execution of your animations to use CATransactions, instead of delegates and animationDidStop...
[CATransaction begin];
[CATransaction setDisableActions:YES];
[CATransaction setCompletionBlock: ^{ theImage.transform = ...}];
// ... CAAnimation Stuff ... //
[CATransaction commit];
You put the transaction's completion block call before you create your animations, as per:
http://zearfoss.wordpress.com/2011/02/24/core-animation-catransaction-protip/
The following is from one of my methods:
[CATransaction begin];
CABasicAnimation *animation = ...;
animation.fromValue = ...;
animation.toValue = ...;
[CATransaction setCompletionBlock:^ { self.shadowRadius = _shadowRadius; }];
[self addAnimation:animation forKey:#"animateShadowOpacity"];
[CATransaction commit];
And, I constructed this animation and it works fine for me with no glitches at the end:
The setup and trigger are custom methods I have in a window, and i trigger the animation on mousedown.
UIImageView *imgView;
UIBezierPath *animationPath;
-(void)setup {
canvas = (C4View *)self.view;
imgView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"img256.png"]];
imgView.frame = CGRectMake(0, 0, 128, 128);
imgView.center = CGPointMake(384, 128);
[canvas addSubview:imgView];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[UIImageView animateWithDuration:2.0f animations:^{
[CATransaction begin];
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pathAnimation.duration = 2.0f;
pathAnimation.calculationMode = kCAAnimationPaced;
animationPath = [UIBezierPath bezierPath];
[animationPath moveToPoint:imgView.center];
[animationPath addLineToPoint:CGPointMake(128, 512)];
[animationPath addLineToPoint:CGPointMake(384, 896)];
pathAnimation.path = animationPath.CGPath;
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
[imgView.layer addAnimation:pathAnimation forKey:#"animatePosition"];
[CATransaction commit];
CGFloat scaleFactor = 2.0f;
CGRect newFrame = imgView.frame;
newFrame.size.width *= scaleFactor;
newFrame.size.height *= scaleFactor;
newFrame.origin = CGPointMake(256, 0);
imgView.frame = newFrame;
imgView.transform = CGAffineTransformRotate(imgView.transform,90.0*M_PI/180);
}];
}

Resources