CAKeyFrameAnimation of transform property on CALayer not working as expected - ios

I have been working at this for a while and searched SO thoroughly for a solution but to no avail. Here is what I am trying to do.
I have a UIScrollView on which the user can zoom and pan for 5 seconds. I have a separate CALayer which is not layered on top of the UIScrollView. I want to scale and translate this CALayer's contents to reflect the zoom and pans occurring on the UIScrollView. I want to achieve this via key frame animation CAKeyFrameAnimation. When I put this into code, the zoom occurs as expected but the position of the content is offset incorrectly.
Here is how I do it in code. Assume that UIScrollView delegate passes zoom scale and content offset to the following method:
// Remember all zoom params for late recreation via a CAKeyFrameAnimation
- (void) didZoomOrScroll:(float)zoomScale
contentOffset:(CGPoint)scrollViewContentOffset {
CATransform3D scale = CATransform3DMakeScale(zoomScale, zoomScale, 1);
CATransform3D translate = CATransform3DMakeTranslation(-scrollViewContentOffset.x,
-scrollViewContentOffset.y, 0);
CATransform3D concat = CATransform3DConcat(scale, translate);
// _zoomScrollTransforms and _zoomScrollTimes below are of type NSMutableArray
[_zoomScrollTransforms addObject:[NSValue valueWithCATransform3D:concat]];
// secondsElapsed below keeps track of time
[_zoomScrollTimes addObject:[NSNumber numberWithFloat:secondsElapsed]];
}
// Construct layer animation
- (void) constructLayerWithAnimation:(CALayer *)layer {
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
animation.duration = 5.0;
animation.values = _zoomScrollTransforms;
// Adjust key frame times to contain fractional durations
NSMutableArray *keyTimes = [[NSMutableArray alloc] init];
for( int i = 0; i < [_zoomScrollTimes count]; i++ ) {
NSNumber *number = (NSNumber *)[_zoomScrollTimes objectAtIndex:i];
NSNumber *fractionalDuration = [NSNumber numberWithFloat:[number floatValue]/animation.duration];
[keyTimes addObject:fractionalDuration];
}
animation.keyTimes = keyTimes;
animation.removedOnCompletion = YES;
animation.beginTime = 0;
animation.calculationMode = kCAAnimationDiscrete;
animation.fillMode = kCAFillModeForwards;
[layer addAnimation:animation forKey:nil];
}
When the above layer is animated, the content is zoomed properly but it is positioned incorrectly. The content appears to be x and y shifted more than I expected and as a result doesn't exactly retrace the zoom/pans done by the user on the UIScrollView.
Any ideas what I may be doing wrong?
Answer
Ok, i figured out what I was doing wrong. It had to do with the anchor point for the CALayer. Transforms on CALayers are always applied to the anchor point. For CALayers, the anchor point is (0.5, 0.5) by default. So scaling and translations were being conducted along the center point. UIScrollViews, on the other hand, gives offsets from the top left corner of the view. Basically you can think of the anchor point for UIScrollView, for the purposes of thinking about the CGPoint offset value, as being (0.0, 0.0).
So the solution is to set the anchor point of CALayer to (0.0, 0.0). And then everything works as expected.
There are other resources that present this info in a nicer way. See this other question on Stackoverflow that is similar. Also see this article in Apple's documentation that discusses position, anchorpoint and general layer in geometry in great detail.

You are concatenating translation matrix with scaled matrix. The final value of displacement offset will be Offset(X, Y) = (scaleX * Tx, scaleY * Ty).
If you want to move the UIView with (Tx, Ty) offset, than concatenate the translate and scale matrices as below:
CATransform3D scale = CATransform3DMakeScale(zoomScale, zoomScale, 1);
CATransform3D translate = CATransform3DMakeTranslation(-scrollViewContentOffset.x,
-scrollViewContentOffset.y, 0);
CATransform3D concat = CATransform3DConcat(translate, scale);
Try this and let me know if it works.

Related

How can I calculate the duration of an ease-out animation to achieve a desired initial velocity?

Context:
I have a graphic that I rotate continuously while a background operation happens:
CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.toValue = #(M_PI * 2.0);
rotationAnimation.duration = 2;
rotationAnimation.cumulative = YES;
rotationAnimation.repeatCount = HUGE_VALF;
[myImgView.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
At some point this background animation ends, and I stop the animation:
[myImgView.layer removeAllAnimations];
When I do this, though, the rotation stops wherever it is and snaps back to zero rotation. I could just stop it where it is by promoting the presentation value to the model value:
CATransform3D stoppedTransform = myImgView.layer.presentationLayer.transform;
[myImgView.layer removeAllAnimations];
myImgView.layer.transform = stoppedTransform;
This works OK. But ideally I'd like to continue the rotation for the rest of its current circuit, and decelerate and stop smoothly as it approaches and rests at zero rotation. Stop the animation, promote presentation to model, and create a new animation with an ease-out curve to zero. Something like this:
CATransform3D stoppedTransform = myImgView.layer.presentationLayer.transform;
[myImgView.layer removeAllAnimations];
myImgView.layer.transform = stoppedTransform;
CGFloat currRotation = [[myImgView.layer valueForKeyPath:#"transform.rotation.z"] floatValue];
CABasicAnimation *finishAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
finishAnimation.fromValue = #(currRotation);
finishAnimation.toValue = #(0);
finishAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
finishAnimation.duration = 2; //this is what I need to calculate correctly
[myImgView.layer setValue:#(0) forKeyPath:#"transform.rotation.z"];
[myImgView.layer addAnimation:finishAnimation forKey:#"transform.rotation.z"];
My question:
How do I calculate the duration of my finishing animation with its ease-out curve so that its initial velocity matches the velocity of the existing rotation (currently pi radians per second) so there's no visible hiccup? Or is there another way to achieve the desired effect?
I have tried using CASpringAnimation and using its initialVelocity property, but it seems to combine that with the velocity imparted by the spring pulling on the mass, which is governed by mass, stiffness, and damping, and its units are unclear. And I can get close on the ease-out duration by calculating the duration as if it's linear and bumping it 20% or 30% or so.
Side note: there is some minor complexity here, removed to simplify the question, around the toValue of finishAnimation because of how the layer reports its current rotation value. If it's more than pi radians (180 degrees), it reports as negative. So it goes like this:
CGFloat rotationRadians = currRotation / M_PI;
if (rotationRadians < 0)
{
finishAnimation.duration = (-1 * rotationRadians); //negative means more than halfway through, or more than pi radians.
finishAnimation.toValue = #(0);
}
else
{
finishAnimation.duration = (2 - rotationRadians); //positive means less than halfway through, or less than pi radians
finishAnimation.toValue = #(M_PI * 2);
}

Rotate CAShapeLayer around it center without moving postion

I want to rotate a CAShapeLayer with objective c around it center point without moving it around. The CAShapeLayer contain UIBezierPath point of rect. I'm not able to rotate the CAShapeLayer becouse i dont know how. Please show me how tp rotate around it center without moving it postion.
Here is some code that does that:
//Create a CABasicAnimation object to manage our rotation.
CABasicAnimation *rotation = [CABasicAnimation animationWithKeyPath:#"transform"];
totalAnimationTime = rotation_count;
rotation.duration = totalAnimationTime;
//Start the animation at the previous value of angle
rotation.fromValue = #(angle);
//Add change (which will be a change of +/- 2pi*rotation_count
angle += change;
//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);
//Now actually add the animation to the layer.
[imageViewToAnimate.layer addAnimation:rotation forKey:#"transform.rotation.z"];
(That code is taken (and simplified) from my github project KeyframeViewAnimations)
In my project I'm rotating the layer of a UIImageView, but the same approach will work for any CALayer type.

Rotate Button Or Image From One Point To another

I want to create a rotating Button Which can be rotate from my given points
I tried this but it gives angles and i want to give points
self.theImageView.transform=CGAffineTransformMakeRotation (angle);
angle=30;
I also tried this but it has same problem
CABasicAnimation *halfTurn;
halfTurn = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
halfTurn.fromValue = [NSNumber numberWithFloat:0];
halfTurn.toValue = [NSNumber numberWithFloat:((90*M_PI)/360)];
halfTurn.duration = 1;
halfTurn.repeatCount = false;
[[button layer] addAnimation:halfTurn forKey:#"180"];
can any one suggest be or give code snippiest
thanks in advance
If you want to want to give two points and rotate your view around those two points, there is a catch. You have to see to it that those two points are equidistant from the anchor of rotation. You can calculate the arc-angle of such a rotation and set it accordingly.
You can find the length of the arc here:
http://math.about.com/od/formulas/ss/surfaceareavol_9.htm
You can find the formula for finding the arc angle, in this link: http://www.regentsprep.org/Regents/math/algtrig/ATM1/arclengthlesson.htm
Edit
I am now given a premise that there are 7 buttons arranged circularly around an anchor point. What you do now, is maintain an instance level variable called currentAngle and initialize it to 0.
CGFloat nextAngle = currentAngle + (360/7);
CABasicAnimation *halfTurn;
halfTurn = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
halfTurn.fromValue = [NSNumber numberWithFloat:((currentAngle*M_PI)/360)];
halfTurn.toValue = [NSNumber numberWithFloat:((nextAngle*M_PI)/360)];
halfTurn.duration = 1;
halfTurn.repeatCount = false;
[[button layer] addAnimation:halfTurn forKey:#"clock_wise_rotation"];
currentAngle = nextAngle;
The above code is for clockwise rotation only. If you want to perform anticlockwise animation, you have to subtract 360/7 and perform the animation.

CAAnimation with an transform rotation over an point

I'm trying to animate the rotation of a layer of a view over an arbitrary point. The start position of the animation will be a 90º rotation from the end and final position. This view occupies all the screen except the status bar.
I'm trying to use the following code (with a translation, followed by a rotation and an anti translation) but, however starting and ending in it, the animation isn't centered on the point, but wobbles around it.
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform"];
animation.duration = 1;
animation.additive = YES;
animation.removedOnCompletion = YES;
animation.fillMode = kCAFillModeRemoved;
//The point that should be used as the center of rotation
CGPoint cursorCenter=self.angleCenter;
//The center of the view
CGPoint linesCenter=lines.center;
//The transformation: translate->rotate->anti-translate
CATransform3D transform = CATransform3DIdentity;
transform = CATransform3DTranslate(transform, cursorCenter.x-linesCenter.x, cursorCenter.y-linesCenter.y, 0.0);
transform = CATransform3DRotate(transform, degreesToRadians(-90), 0.0, 0.0, -1.0);
transform = CATransform3DTranslate(transform, linesCenter.x-cursorCenter.x, linesCenter.y-cursorCenter.y, 0.0);
animation.fromValue =[NSValue valueWithCATransform3D:transform];
animation.toValue =[NSValue valueWithCATransform3D:CATransform3DIdentity];
[lines.layer addAnimation:animation forKey:#"90rotation"];
What I'm doing wrong?
You don't have to make translations to rotate over a aspecific point. Just change the anchorPoint of the layer to adjust center of the rotation.
The origin of the transform is the value of the center property, or
the layer’s anchorPoint property if it was changed.
About anchor point and how to specify it:
You specify the value for this property using the unit coordinate
space. The default value of this property is (0.5, 0.5), which
represents the center of the layer’s bounds rectangle. All geometric
manipulations to the view occur about the specified point. For
example, applying a rotation transform to a layer with the default
anchor point causes the layer to rotate around its center. Changing
the anchor point to a different location would cause the layer to
rotate around that new point.
Based on CALayer Class Reference and Core Animation Programming Guide

CALayer - CABasicAnimation not scaling around center/anchorPoint

Using the code below I've been trying to get my CALayer to scale around its center - but it scales by left/top (0,0). I saw this question: Anchor Point in CALayer and have tried setting the anchorPoint - but it was [.5, .5] before I set it and setting makes no difference. Also tried setting contentsGravity.
The set up is I have a CAShapeLayer added to self.view.layer I do some drawing in it and then add the CABasicAnimation. The animation runs fine - but it scales toward the top/left - not the center of the view.
Have been tinkering with this for a few hours - it must be something simple but I'm not getting it.
shapeLayer.anchorPoint = CGPointMake(.5,.5);
shapeLayer.contentsGravity = #"center";
printf("anchorPoint %f %f\n", shapeLayer.anchorPoint.x, shapeLayer.anchorPoint.y);
CABasicAnimation *animation =
[CABasicAnimation animationWithKeyPath:#"transform.scale"];
animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.1, 0.1, 1.0)];
[animation setDuration:1.0];
animation.fillMode=kCAFillModeForwards;
animation.removedOnCompletion=NO;
[shapeLayer addAnimation:animation forKey:#"zoom"];
What is the bounds of your layer?
I bet it has zero size; try setting its size to the same size as your shape.
CAShapeLayer draws the shape as kind of an overlay, independent of the contents (and bounds and contentsGravity and etc.) that you'd use in an ordinary CALayer. It can be a bit confusing.
Also make sure that you're adding the animation after the view is created. If you're trying to add animation in the viewDidLoad, it probably won't work. Try adding it to viewDidAppear instead.
I only know Swift, but here's an example:
override func viewDidAppear (animated: Bool) {
addPulsation(yourButton)
}
func addPulsation (button: UIButton) {
let scaleAnimation:CABasicAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.duration = 1.0
scaleAnimation.repeatCount = 100
scaleAnimation.autoreverses = true
scaleAnimation.fromValue = 1.05;
scaleAnimation.toValue = 0.95;
button.layer.addAnimation(scaleAnimation, forKey: "scale")
}
It's an old thread but in case this helps anyone.
This ia a coordinate space problem. You just need to set the drawing space and the space of the the path. This code works for me...
...just put this at the start of the animation creation method (inside a method on the parent view)...
// add animation to remap (without affecting other elements)
let animationLayer = CALayer()
layer?.addSublayer(animationLayer). // (don't use optional for iOS)
// set the layer origin to the center of the view (or any center point for the animation)
animationLayer.bounds.origin .x = -self.bounds.size.width / 2.0
animationLayer.bounds.origin .y = -self.bounds.height / 2.0
// make image layer with circle around the zero point
let imageLayer = CAShapeLayer()
animationLayer.addSublayer(imageLayer)
let shapeBounds = CGRect(x: -bounds.size.width/2.0,
y:-bounds.size.height/2.0,
width: bounds.size.width,
height: bounds.size.height)
imageLayer.path = CGPath(ellipseIn: shapeBounds, transform: nil)
imageLayer.fillColor = fillColour
// **** apply your animations to imageLayer here ****
this code works unchanged on iOS and MacOS - apart from the optional needed on layer for MacOS.
(BTW you need to remove the animation layer after the animation finishes!)
I spent hours to find out how to do it according the center but couldn't find any solutions for OSX. I decided to to move layer using CATransform3DMakeTranslation and combine both transforms.
My code:
NSView *viewToTransform = self.view;
[viewToTransform setWantsLayer:YES];
viewToTransform.layer.sublayerTransform = CATransform3DConcat(CATransform3DMakeScale(-1.0f, -1.0f, 1.0f), CATransform3DMakeTranslation(viewToTransform.bounds.size.width, viewToTransform.bounds.size.height, 0));

Resources