Smooth Animation on CAKeyframeAnimation for CATransform3D animations - objective-c

I've created my own transition animation between views. I animate two properties, the position and the transform, to provide a cube-like transition between views. The frame uses CABasicAnimation while the transform uses a "2-stage" CAKeyframeAnimation. Everything works fine except for one small detail I can't seem to figure out. In my transition I apply a CATransform3DScale on the middle key frame to create a zoom-in/zoom-out effect. That works fine except the animation looks slightly jerky. It's animating the between the key frames in a linear fashion, and I would like to smooth that out. Now CAKeyframeAnimation has a way to do that using calculationMode, but it doesn't seem to work for transforms. I've tried setting it to kCAAnimationCubic and kCAAnimationCubicPaced with no effect.
Here is the code that animates one view's transform (a similar block of code animates the other view):
CAKeyframeAnimation *aTransform = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
CATransform3D transform1 = RotateOnX(toView, M_PI_4);
transform1 = CATransform3DScale(transform1, RotationalZoom, RotationalZoom, RotationalZoom);
[aTransform setValues:Array([NSValue valueWithCATransform3D:RotateOnX(toView, M_PI_2)],
[NSValue valueWithCATransform3D:transform1],
[NSValue valueWithCATransform3D:RotateOnX(toView, 0)])];
[toView.layer addAnimation:aTransform forKey:#"transform"];
Note: RotateOnX(UIView *, CGFloat) is a block that returns a transform for a view rotated on X by the desired Radians.
As you can see I only set a scaling transform on the middle key frame. Also, the rotation of the view is perfectly smooth, it's only the scaling that appears to 'jerk' as it changes direction.
Does anyone have any ideas on how to smooth out the scaling/zooming?

Try the timingFunctions property. Since you have 3 keyframes, you need 2 timing functions: one for the animation from keyframe 0 to keyframe 1, and another for the animation from keyframe 1 to keyframe 2.
aTransform.timingFunctions = [NSArray arrayWithObjects:
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInOut],
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInOut],
nil];

Related

CABasicAnimation - view should position on the last frame

Hello i want to do a CABasicAnimation rotation where my view rotates 440 degree. After the animatin i don´t want to reset the view to the old position. It should be on the same position like on the last frame of the animation.
CABasicAnimation *rotate = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
rotate.toValue = [NSNumber numberWithFloat:degrees / 180.0 * M_PI];
rotate.duration = 4.4;
[rotate setFillMode:kCAFillModeForwards];
UIView *animationView = [self getBGContainerViewForSender:sender];
[animationView.layer addAnimation:rotate forKey:#"myRotationAnimation"];</i>
Can anybody tell me how to position the view on the last frame?
When you create an animation and add it to a layer, you are not changing any of the values in your object. So in your case, the transform property of animationView will not change. Therefore, when the animation ends, the view will snap back to the old position. To solve this, you have to do two things:
Before you add the animation, set the transform property of the views layer to the value you want when the animation is done. However, this will add an implicit animation to the layer, so you need to kill the implicit animation. To do that:
The value of forKey: in the add animation method must be the name of the animation, in this case "transform". This name will replace the implicit animation for transform with your explicit animation.
So add:
animationView.layer.transform = CA3DTransformMakeRotate(....);
and change the add animation call to
[animationView.layer addAnimation:rotate
forKey:#"transform"];
This is explained in WWDC 2010, Core Animation in Practice Part 1, at about 39:20.
Before initating the animation make a transaction like this:
[CATransaction begin];
[CATransaction setValue:kCFBooleanTrue forKey:kCATransactionDisableActions];
CATransform3D current = animationView.layer.transform;
animationView.layer.transform = CATransform3DRotate(current, numberWithFloat:degrees / 180.0 * M_PI, 0, 1.0, 0);
[CATransaction commit];
/* YOUR ORIGINAL CODE COMES HERE*/

How to synchronously animate a UIView and a CALayer

To illustrate this question, I gisted a very small Xcode project on Github (two classes, 11kb download). Have a look at the gist here or use git clone git#gist.github.com:93982af3b65d2151672e.git.
Please consider the following scenario. A custom view of mine, called 'container view', contains two little squares. Illustrated in this screenshot:
The blue square is a 22x22 pt UIView instance and the red square is a 22x22 pt CALayer instance. For purposes of this question I want the two squares to 'stick' to the bottom right corner of the outer view, while I animate the frame of that outer view.
I change the container view's frame within a UIView's class method animateWithDuration:delay:options:animations:completion:, with a non-default easing parameter of type UIViewAnimationCurveEaseOut.
[UIView animateWithDuration:1.0 delay:0
options:UIViewAnimationCurveEaseOut
animations:^{ _containerView.frame = randomFrame; }
completion:nil];
Note: Both the blue UIView's and the red CALayer's frame are set in the overridden setFrame: method of the container view, but results would have been the same if I had set the blue UIView's autoresizingMask property to UIView flexible left and top margins.
In the resulting animation, the blue square 'sticks' to the corner just the way I intended it, but the red square completely disregards both timing and easing of the animation. I assume this is because of the implicit animation feature of Core Animation, a feature that has helped me in many occasions before.
Here are a few screenshots in the animation sequence that illustrate the asynchronicity of the two squares:
On to the question: Is there a way to synchronize the two frame changes so that the red CALayer and the blue UIView both move with the same animation duration and easing curve, sticking to each other as if they were one view?
P.S. Of course the required visual result of the two squares sticking together could be achieved in any number of ways, for example by having both layers become either CALayers or UIViews, but the project that the real issue is in has a very legit cause for the one to be a CALayer and the other to be a UIView.
I'm assuming that you got the end position correct and that after the animation that the view and the layer are aligned. That has nothing to do with the animation, just geometry.
When you change the property of a CALayer that isn't the layer of a view (like in your case) it will implicitly animate to its new value. To customize this animation you could use an explicit animation, like a basic animation. Changing the frame in a basic animation would look something like this.
CABasicAnimation *myAnimation = [CABasicAnimation animationWithKeyPath:#"frame"];
[myAnimation setToValue:[NSValue valueWithCGRect:myNewFrame]];
[myAnimation setFromValue:[NSValue valueWithCGRect:[myLayer frame]]];
[myAnimation setDuration:1.0];
[myAnimation setTimingFunction:[CAMediaTimingFuntion functionWithName: kCAMediaTimingFunctionEaseOut]];
[myLayer setFrame:myNewFrame];
[myLayer addAnimation:myAnimation forKey:#"someKeyForMyAnimation"];
If the timing function and the duration of both animations are the same then they should stay aligned.
Also note that explicit animations doesn't change the value and hat you have to both add the animation and set the new value (like in the sample above)
Yes, there are a number of ways to achieve the same effect. One example is having the view and the layer be subviews of the same subview (that in turn is a subview of the outer frame).
Edit
You can't easily group the UIView-animation with an explicit animation. Neither can you use an animation group (since you are applying them to different layers) but yo can use a CATransaction:
[CATransaction begin];
[CATransaction setAnimationDuration:1.0];
[CATransaction setAnimationTimingFunction:[CAMediaTimingFuntion functionWithName: kCAMediaTimingFunctionEaseOut]];
// Layer animation
CABasicAnimation *myAnimation = [CABasicAnimation animationWithKeyPath:#"frame"];
[myAnimation setToValue:[NSValue valueWithCGRect:myNewFrame]];
[myAnimation setFromValue:[NSValue valueWithCGRect:[myLayer frame]]];
[myLayer setFrame:myNewFrame];
[myLayer addAnimation:myAnimation forKey:#"someKeyForMyAnimation"];
// Outer animation
CABasicAnimation *outerAnimation = [CABasicAnimation animationWithKeyPath:#"frame"];
[outerAnimation setToValue:[NSValue valueWithCGRect:myNewOuterFrame]];
[outerAnimation setFromValue:[NSValue valueWithCGRect:[outerView frame]]];
[[outerView layer] setFrame:myNewOuterFrame];
[[outerView layer] addAnimation:outerAnimation forKey:#"someKeyForMyOuterAnimation"];
[CATransaction commit];
If you want David Rönnqvist's snippet in Swift 3.0, here you have it:
CATransaction.begin()
CATransaction.setAnimationDuration(1.0)
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut))
// Layer animation
let myAnimation = CABasicAnimation(keyPath: "frame");
myAnimation.toValue = NSValue(cgRect: myNewFrame)
myAnimation.fromValue = NSValue(cgRect: myLayer.frame)
myLayer.frame = myNewFrame
myLayer.add(myAnimation, forKey: "someKeyForMyAnimation")
// Outer animation
let outerAnimation = CABasicAnimation(keyPath: "frame")
outerAnimation.toValue = NSValue(cgRect: myNewOuterFrame)
outerAnimation.fromValue = NSValue(cgRect: outerView.frame)
outerView.layer.frame = myNewOuterFrame
outerView.layer.add(outerAnimation, forKey: "someKeyForMyOuterAnimation")
CATransaction.commit()

How do I achieve continuous rotation of an NSImage inside NSImageView?

FUTURE VIEWERS:
I have managed to finish this rotation animation and code with description can be found on tho question. NSImage rotation in NSView is not working
Before you proceed please up vote Duncan C 's answer. As I manage to achieve this rotation from his answer.
I have an image like this,
I want to keep rotating this sync icon, On a different thread. Now I tried using Quartz composer and add the animation to QCView but it is has very crazy effect and very slow too.
Question :
How do I rotate this image continuously with very less processing expense?
Effort
I read CoreAnimation, Quartz2D documentation but I failed to find the way to make it work. The only thing I know so far is, I have to use
CALayer
CAImageRef
AffineTransform
NSAnimationContext
Now, I am not expecting code, but an understanding with pseudo code will be great!
Getting an object to rotate more than 180 degrees is actually a little bit tricky. The problem is that you specify a transformation matrix for the ending rotation, and the system decides to rotate in the other direction.
What I've done is to create a CABasicAnimation of less than 180 degrees, set up to be additive , and with a repeat count. Each step in the animation animates the object more.
The following code is taken from an iOS application, but the technique is identical in Mac OS.
CABasicAnimation* rotate = [CABasicAnimation animationWithKeyPath: #"transform.rotation.z"];
rotate.removedOnCompletion = FALSE;
rotate.fillMode = kCAFillModeForwards;
//Do a series of 5 quarter turns for a total of a 1.25 turns
//(2PI is a full turn, so pi/2 is a quarter turn)
[rotate setToValue: [NSNumber numberWithFloat: -M_PI / 2]];
rotate.repeatCount = 11;
rotate.duration = duration/2;
rotate.beginTime = start;
rotate.cumulative = TRUE;
rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
CAAnimation objects operate on layers, so for Mac OS, you'll need to set the "wants layer" property in interface builder, and then add the animation to your view's layer.
To make your view rotate forever, you'd set repeat count to some very large number like 1e100.
Once you've created your animation, you'd add it to your view's layer with code something like this:
[myView.layer addAnimation: rotate forKey: #"rotateAnimation"];
That's about all there is to it.
Update:
I've recently learned of another way to handle rotations of greater than 180 degrees, or continuous rotations.
There is a special object called a CAValueFunction that lets you apply a change to your layer's transform using an arbitrary value, including values that specify multiple full rotations.
You create a CABasicAnimation of your layer's transform property, but then instead of providing a transform, the value you supply is an NSNumber that gives the new rotation angle. If you provide a new angle like 20pi, your layer will rotate 10 full rotations (2pi/rotation). The code looks like this:
//Create a CABasicAnimation object to manage our rotation.
CABasicAnimation *rotation = [CABasicAnimation animationWithKeyPath:#"transform"];
rotation.duration = 10.0;
CGFLOAT angle = 20*M_PI;
//Set the ending value of the rotation to the new angle.
rotation.toValue = #(angle);
//Have the rotation use linear timing.
rotation.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
/*
This is the magic bit. We add a CAValueFunction that tells the CAAnimation we are
modifying the transform's rotation around the Z axis.
Without this, we would supply a transform as the fromValue and toValue, and
for rotations > a half-turn, we could not control the rotation direction.
By using a value function, we can specify arbitrary rotation amounts and
directions and even rotations greater than 360 degrees.
*/
rotation.valueFunction =
[CAValueFunction functionWithName: kCAValueFunctionRotateZ];
/*
Set the layer's transform to it's final state before submitting the animation, so
it is in it's final state once the animation completes.
*/
imageViewToAnimate.layer.transform =
CATransform3DRotate(imageViewToAnimate.layer.transform, angle, 0, 0, 1.0);
[imageViewToAnimate.layer addAnimation:rotation forKey:#"transform.rotation.z"];
(I Extracted the code above from a working example application, and took out some things that weren't directly related to the subject. You can see this code in use in the project KeyframeViewAnimations (link) on github. The code that does the rotation is in a method called `handleRotate'

How to animate and rotate an image?

Hi i am working on an iPhone application and i have to animate and rotate an image, Image is attached here
now purpose of this image is that whenever any tap some where on iPad it will animate and will point towards that place.
Please help me to figure it out. Thanx
Try this -
CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
animation.fromValue = [NSNumber numberWithFloat:0.0f];
animation.toValue = [NSNumber numberWithFloat: 2*M_PI];
animation.duration = 10.0f;
animation.repeatCount = INFINITY;
[self.yourImage.layer addAnimation:animation forKey:#"SpinAnimation"];
check this tutorial. It will really help you.
To animate images view, you'll need to use CoreAnimation.
All you have to do is making UIImageView object with your image inside and then you'll manipulate directly the layer to animate.
There is 2 kind of animation with core animation :
Implicit animation (normally they are activate by default) : Everytime you change some setting on the layer (like position with [imageView.layer setPosition:CGPointMake...]) there should be an implicit animation with default parameters (Duration, AnimationCurve etc.)
Explicit animation : If you want to create more complex animations, you should use explicit animation, it will allow you to do complex things like moving, rotating and scaling some layer in the same time.
I encourage you to check the core animation programming guide

Tracking a Core Animation animation

I have two circles which move around the screen. The circles are both UIViews which contain other UIViews. The area outside each circle is transparent.
I have written a function to create a CGPath which connects the two circles with a quadrilateral shape. I fill this path in a transparent CALayer which spans the entire screen. Since the layer is behind the two circular UIViews, it appears to connect them.
Finally, the two UIViews are animated using Core Animation. The position and size of both circles change during this animation.
So far the only method that I have had any success with is to interrupt the animation at regular intervals using an NSTimer, then recompute and draw the beam based on the location of the circle's presentationLayer. However, the quadrilateral lags behind the circles when the animation speeds up.
Is there a better way to accomplish this using Core Animation? Or should I avoid Core Animation and implement my own animation using an NSTimer?
I faced a similar problem. I used layers instead of views for the animation. You could try something like this.
Draw each element as a CALayer and include them as sublayers for your container UIVIew's layer. UIViews are easier to animate, but you will have less control. Notice that for any view you can get it's layer with [view layer];
Create a custom sublayer for your quadrilateral. This layer should have a property or several of properties you want to animate for this layer. Let's call this property "customprop". Because it is a custom layer, you want to redraw on each frame of the animation. For the properties you plan to animate, your custom layer class should return YES needsDisplayForKey:. That way you ensure -(void)drawInContext:(CGContextRef)theContext gets called on every frame.
Put all animations (both circles and the quad) in the same transaction;
For the circles you can probably use CALayers and set the content, if it is an image, the standard way:
layer.contents = [UIImage imageNamed:#"circle_image.png"].CGImage;
Now, for the quad layer, subclass CALayer and implement this way:
- (void)drawInContext:(CGContextRef)theContext{
//Custom draw code here
}
+ (BOOL)needsDisplayForKey:(NSString *)key{
if ([key isEqualToString:#"customprop"])
return YES;
return [super needsDisplayForKey:key];
}
The transaction would look like:
[CATransaction begin];
CABasicAnimation *theAnimation=[CABasicAnimation animationWithKeyPath:#"customprop"];
theAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(1000, 1000)];
theAnimation.duration=1.0;
theAnimation.repeatCount=4;
theAnimation.autoreverses=YES;
theAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
theAnimation.delegate = self;
[lay addAnimation:theAnimation forKey:#"selecting"];
[CATransaction setValue:[NSNumber numberWithFloat:10.0f]
forKey:kCATransactionAnimationDuration];
circ1.position=CGPointMake(1000, 1000);
circ2.position=CGPointMake(1000, 1000);
[CATransaction commit];
Now all the draw routines will happen at the same time. Make sure your drawInContext: implementation is fast. Otherwise the animation will lag.
After adding each sublayer to the UIViews's layer, rememeber to call [layer setNeedsDisplay]. It does not get called automatically.
I know this is a bit complicated. However, the resulting animations are better than using a NSTimer and redrawing on each call.
If you need to find the current visible state of the layers, you can call -presentationLayer on the CALayer in question, and this will give you a layer that approximates the one used for rendering. Note I said approximates - it's not guaranteed to be fully accurate. However it may be good enough for your purposes.