CAEmitterLayer - kCAEmitterLayerCircle, circular path? - ios

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.

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 can I know current animation in CAAnimationGroups?

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

Group animation on multiple objects

I'm trying to animate many bubbles coming out of a machine. I'm using a basic animation to scale the bubbles from a small bubble to a large one and a keyframe animation to send the bubble in a curve across the screen. These are combined into a group animation.
I have read through just about all I can to try to get this to work, but I have the following two problems.
I can't get the scale animation to work together with the keyframe
animation when I try to animate each bubble.
The animation happens to them all at the same time. I'd like to see them animated one
after the other, but they all animate together.
here is the animation code I have.
-(void)EmitBubbles:(UIButton*)bubblename :(CGRect)RectPassed
{
bubblename.hidden = NO;
// Set position and size of each bubble to be at head of bubble machine and size (0,0)
CGPoint point = (RectPassed.origin);
CGRect ButtonFrame;
ButtonFrame = bubblename.bounds;
ButtonFrame.size = CGSizeMake(0, 0);
bubblename.bounds = ButtonFrame;
CGRect OldButtonBounds = bubblename.bounds;
CGRect NewButtonBounds = OldButtonBounds;
NewButtonBounds.size = RectPassed.size;
CGFloat animationDuration = 0.8f;
// Set up scaling
CABasicAnimation *resizeAnimation = [CABasicAnimation animationWithKeyPath:#"bounds.size"];
resizeAnimation.fillMode = kCAFillModeForwards;
resizeAnimation.duration = animationDuration;
resizeAnimation.fromValue = [NSValue valueWithCGSize:OldButtonBounds.size];
resizeAnimation.toValue = [NSValue valueWithCGSize:NewButtonBounds.size];
// Set Actual bubble size after animation
bubblename.bounds = NewButtonBounds;
// Setup Path
CGPoint MidPoint = CGPointMake(500,50);
CGPoint EndPoint = CGPointMake(RectPassed.origin.x, RectPassed.origin.y);
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pathAnimation.calculationMode = kCAAnimationPaced;
CGMutablePathRef curvedPath = CGPathCreateMutable();
CGPathMoveToPoint(curvedPath, nil, bubblename.bounds.origin.x, bubblename.bounds.origin.y);
CGPoint NewLoc = CGPointMake(745, 200);
CGPathMoveToPoint(curvedPath, NULL, NewLoc.x,NewLoc.y);
CGPathAddCurveToPoint(curvedPath, NULL, 745,200,MidPoint.x,MidPoint.y, EndPoint.x, EndPoint.y);
pathAnimation.path = curvedPath;
// Set Bubble action position after animation
bubblename.layer.position = point;
// Add scale and path to animation group
CAAnimationGroup *group = [CAAnimationGroup animation];
group.removedOnCompletion = YES;
[group setAnimations:[NSArray arrayWithObjects:pathAnimation, resizeAnimation, nil]];
group.duration = animationDuration;
[bubblename.layer addAnimation:group forKey:#"nil"]; //savingAnimation
CGPathRelease(curvedPath);
}
I call the animation for each bubble using
[self EmitBubbles:btnbubble1 :CGRectMake(200, 500, 84, 88)];
[self EmitBubbles:btnbubble2 :CGRectMake(200, 500, 84, 88)];
I have 20 bubbles on screen. The size of the rect passed is the final size I want the bubbles to be.
Can I can get each bubble to animate separately using a scale and keyframe path, without having to write the above code for each bubble?
Thanks
OK. I think I did it.After hours of looking around Stackoverflow and other places I found that to bhave the objects animate one after the other. I had to calculate the time according to the app time. I used.
group.beginTime = CACurrentMediaTime() + AnimationDelay
To stop the animation momentarily appearing at its final position before continuing with the animation. I had to use
group.fillMode = kCAFillModeBackwards;

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

How to do a curve/arc animation with CAAnimation?

I have an user interface where an item get deleted, I would like to mimic the "move to folder" effect in iOS mail. The effect where the little letter icon is "thrown" into the folder. Mine will get dumped in a bin instead.
I tried implementing it using a CAAnimation on the layer. As far as I can read in the documentations I should be able to set a byValue and a toValue and CAAnimation should interpolate the values. I am looking to do a little curve, so the item goes through a point a bit above and to the left of the items start position.
CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:#"position"];
[animation setDuration:2.0f];
[animation setRemovedOnCompletion:NO];
[animation setFillMode:kCAFillModeForwards];
[animation setTimingFunction:[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut]];
[animation setFromValue:[NSValue valueWithCGPoint:fromPoint]];
[animation setByValue:[NSValue valueWithCGPoint:byPoint]];
[animation setToValue:[NSValue valueWithCGPoint:CGPointMake(512.0f, 800.0f)]];
[animation setRepeatCount:1.0];
I played around with this for some time, but it seems to me that Apple means linear interpolation.
Adding the byValue does not calculate a nice arc or curve and animate the item through it.
How would I go about doing such an animation?
Thanks for any help given.
Using UIBezierPath
(Don't forget to link and then import QuartzCore, if you're using iOS 6 or prior)
Example code
You could use an animation that will follow a path, conveniently enough, CAKeyframeAnimation supports a CGPath, which can be obtained from an UIBezierPath. Swift 3
func animate(view : UIView, fromPoint start : CGPoint, toPoint end: CGPoint)
{
// The animation
let animation = CAKeyframeAnimation(keyPath: "position")
// Animation's path
let path = UIBezierPath()
// Move the "cursor" to the start
path.move(to: start)
// Calculate the control points
let c1 = CGPoint(x: start.x + 64, y: start.y)
let c2 = CGPoint(x: end.x, y: end.y - 128)
// Draw a curve towards the end, using control points
path.addCurve(to: end, controlPoint1: c1, controlPoint2: c2)
// Use this path as the animation's path (casted to CGPath)
animation.path = path.cgPath;
// The other animations properties
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
animation.duration = 1.0
animation.timingFunction = CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseIn)
// Apply it
view.layer.add(animation, forKey:"trash")
}
Understanding UIBezierPath
Bezier paths (or Bezier Curves, to be accurate) work exactly like the ones you'd find in photoshop, fireworks, sketch... They have two "control points", one for each vertex. For example, the animation I just made:
Works the bezier path like that. See the documentation on the specifics, but it's basically two points that "pull" the arc towards a certain direction.
Drawing a path
One cool feature about UIBezierPath, is that you can draw them on screen with CAShapeLayer, thus, helping you visualise the path that it will follow.
// Drawing the path
let *layer = CAShapeLayer()
layer.path = path.cgPath
layer.strokeColor = UIColor.black.cgColor
layer.lineWidth = 1.0
layer.fillColor = nil
self.view.layer.addSublayer(layer)
Improving the original example
The idea of calculating your own bezier path, is that you can make the completely dynamic, thus, the animation can change the curve it's going to do, based on multiple factors, instead of just hard-coding as I did in the example, for instance, the control points could be calculated as follows:
// Calculate the control points
let factor : CGFloat = 0.5
let deltaX : CGFloat = end.x - start.x
let deltaY : CGFloat = end.y - start.y
let c1 = CGPoint(x: start.x + deltaX * factor, y: start.y)
let c2 = CGPoint(x: end.x , y: end.y - deltaY * factor)
This last bit of code makes it so that the points are like the previous figure, but in a variable amount, respect to the triangle that the points form, multiplied by a factor which would be the equivalent of a "tension" value.
You are absolutely correct that animating the position with a CABasicAnimation causes it to go in a straight line. There is another class called CAKeyframeAnimation for doing more advanced animations.
An array of values
Instead of toValue, fromValue and byValue for basic animations you can either use an array of values or a complete path to determine the values along the way. If you want to animate the position first to the side and then down you can pass an array of 3 positions (start, intermediate, end).
CGPoint startPoint = myView.layer.position;
CGPoint endPoint = CGPointMake(512.0f, 800.0f); // or any point
CGPoint midPoint = CGPointMake(endPoint.x, startPoint.y);
CAKeyframeAnimation *move = [CAKeyframeAnimation animationWithKeyPath:#"position"];
move.values = #[[NSValue valueWithCGPoint:startPoint],
[NSValue valueWithCGPoint:midPoint],
[NSValue valueWithCGPoint:endPoint]];
move.duration = 2.0f;
myView.layer.position = endPoint; // instead of removeOnCompletion
[myView.layer addAnimation:move forKey:#"move the view"];
If you do this you will notice that the view moves from the start point in a straight line to the mid point and in another straight line to the end point. The part that is missing to make it arc from start to end via the mid point is to change the calculationMode of the animation.
move.calculationMode = kCAAnimationCubic;
You can control it arc by changing the tensionValues, continuityValues and biasValues properties. If you want finer control you can define your own path instead of the values array.
A path to follow
You can create any path and specify that the property should follow that. Here I'm using a simple arc
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL,
startPoint.x, startPoint.y);
CGPathAddCurveToPoint(path, NULL,
controlPoint1.x, controlPoint1.y,
controlPoint2.x, controlPoint2.y,
endPoint.x, endPoint.y);
CAKeyframeAnimation *move = [CAKeyframeAnimation animationWithKeyPath:#"position"];
move.path = path;
move.duration = 2.0f;
myView.layer.position = endPoint; // instead of removeOnCompletion
[myView.layer addAnimation:move forKey:#"move the view"];
Try this it will solve your problem definitely, I have used this in my project:
UILabel *label = [[[UILabel alloc] initWithFrame:CGRectMake(10, 126, 320, 24)] autorelease];
label.text = #"Animate image into trash button";
label.textAlignment = UITextAlignmentCenter;
[label sizeToFit];
[scrollView addSubview:label];
UIImageView *icon = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:#"carmodel.png"]] autorelease];
icon.center = CGPointMake(290, 150);
icon.tag = ButtonActionsBehaviorTypeAnimateTrash;
[scrollView addSubview:icon];
UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
button.center = CGPointMake(40, 200);
button.tag = ButtonActionsBehaviorTypeAnimateTrash;
[button setTitle:#"Delete Icon" forState:UIControlStateNormal];
[button sizeToFit];
[button addTarget:self action:#selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
[scrollView addSubview:button];
[scrollView bringSubviewToFront:icon];
- (void)buttonClicked:(id)sender {
UIView *senderView = (UIView*)sender;
if (![senderView isKindOfClass:[UIView class]])
return;
switch (senderView.tag) {
case ButtonActionsBehaviorTypeExpand: {
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"transform"];
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
anim.duration = 0.125;
anim.repeatCount = 1;
anim.autoreverses = YES;
anim.removedOnCompletion = YES;
anim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.2, 1.2, 1.0)];
[senderView.layer addAnimation:anim forKey:nil];
break;
}
case ButtonActionsBehaviorTypeAnimateTrash: {
UIView *icon = nil;
for (UIView *theview in senderView.superview.subviews) {
if (theview.tag != ButtonActionsBehaviorTypeAnimateTrash)
continue;
if ([theview isKindOfClass:[UIImageView class]]) {
icon = theview;
break;
}
}
if (!icon)
return;
UIBezierPath *movePath = [UIBezierPath bezierPath];
[movePath moveToPoint:icon.center];
[movePath addQuadCurveToPoint:senderView.center
controlPoint:CGPointMake(senderView.center.x, icon.center.y)];
CAKeyframeAnimation *moveAnim = [CAKeyframeAnimation animationWithKeyPath:#"position"];
moveAnim.path = movePath.CGPath;
moveAnim.removedOnCompletion = YES;
CABasicAnimation *scaleAnim = [CABasicAnimation animationWithKeyPath:#"transform"];
scaleAnim.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
scaleAnim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.1, 0.1, 1.0)];
scaleAnim.removedOnCompletion = YES;
CABasicAnimation *opacityAnim = [CABasicAnimation animationWithKeyPath:#"alpha"];
opacityAnim.fromValue = [NSNumber numberWithFloat:1.0];
opacityAnim.toValue = [NSNumber numberWithFloat:0.1];
opacityAnim.removedOnCompletion = YES;
CAAnimationGroup *animGroup = [CAAnimationGroup animation];
animGroup.animations = [NSArray arrayWithObjects:moveAnim, scaleAnim, opacityAnim, nil];
animGroup.duration = 0.5;
[icon.layer addAnimation:animGroup forKey:nil];
break;
}
}
}
I found out how to do it. It's really possible to animate X and Y separately. If you animate them over the same time (2.0 seconds below) and set a different timing function, it will make it look like it moves in an arc instead of a straight line from start to finish values. To adjust the arc you'd need to play around with setting a different timing function. Not sure if CAAnimation supports any "sexy" timing functions however.
const CFTimeInterval DURATION = 2.0f;
CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:#"position.y"];
[animation setDuration:DURATION];
[animation setRemovedOnCompletion:NO];
[animation setFillMode:kCAFillModeForwards];
[animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]];
[animation setFromValue:[NSNumber numberWithDouble:400.0]];
[animation setToValue:[NSNumber numberWithDouble:0.0]];
[animation setRepeatCount:1.0];
[animation setDelegate:self];
[myview.layer addAnimation:animation forKey:#"animatePositionY"];
animation = [CABasicAnimation animationWithKeyPath:#"position.x"];
[animation setDuration:DURATION];
[animation setRemovedOnCompletion:NO];
[animation setFillMode:kCAFillModeForwards];
[animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
[animation setFromValue:[NSNumber numberWithDouble:300.0]];
[animation setToValue:[NSNumber numberWithDouble:0.0]];
[animation setRepeatCount:1.0];
[animation setDelegate:self];
[myview.layer addAnimation:animation forKey:#"animatePositionX"];
Edit:
Should be possible to change timing function by using https://developer.apple.com/library/ios/#documentation/Cocoa/Reference/CAMediaTimingFunction_class/Introduction/Introduction.html (CAMediaTimingFunction inited by functionWithControlPoints:::: )
It's a "cubic Bezier curve". I'm sure Google has answers there. http://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves :-)
I have had a bit similar question several days ago and I implemented it with timer, as Brad says, but not NSTimer. CADisplayLink - that is the timer which should be used for this purpose, as it is synchronized with the frameRate of the application and provides smoother and more natural animation. You can look at my implementation of it in my answer here. This technique really gives much more control on animation than CAAnimation, and is not much more complicated.
CAAnimation can't draw anything since it doesn't even redraw the view. It only moves, deforms and fades what is already drawn.

Resources