Weird Perspective With CATransform3DMakeRotation - iphone

I'm having an odd problem with CATransform3DMakeRotation. When the rotated view is in the center of the superview, it looks like this:
This is how it's supposed to look. However, when it's somewhere else in the superview, say, in the lower left corner, it looks like this:
Note that it is tilted to the right. Same thing happens when it's in the lower right corner, only that it tilts to the left. Is there some way to make it tilt the way it should all the time and in every position? The code to achieve this transformation is as follows:
CALayer *layer = view.layer;
CATransform3D aTransform = CATransform3DIdentity;
float zDistance = 1000;
aTransform.m34 = 1.0 / -zDistance;
self.view.layer.sublayerTransform = aTransform;
CABasicAnimation *rotateAnim = [CABasicAnimation animationWithKeyPath:#"transform"];
rotateAnim.fromValue= [NSValue valueWithCATransform3D:CATransform3DMakeRotation(0, 0, 0, 0)];
rotateAnim.duration=0.12;
rotateAnim.toValue=[NSValue valueWithCATransform3D:CATransform3DMakeRotation(20*M_PI/180, 1, 0, 0)];
rotateAnim.removedOnCompletion = NO;
rotateAnim.fillMode = kCAFillModeForwards;
[layer addAnimation:rotateAnim forKey:#"rotateAnim"];

You've applied perspective at the superview's layer, and that's what you're getting. If you want perspective based on the individual sublayers (without considering the overall perspective), then you have to apply perspective to them individually. For example:
CALayer *layer = view.layer;
CATransform3D aTransform = CATransform3DIdentity;
CGFloat zDistance = 2000.;
aTransform.m34 = 1.0 / -zDistance;
CABasicAnimation *rotateAnim = [CABasicAnimation animationWithKeyPath:#"transform"];
rotateAnim.fromValue= [NSValue valueWithCATransform3D:aTransform];
aTransform = CATransform3DRotate(aTransform, 20*M_PI/180, 1, 0, 0);;
rotateAnim.duration=0.12;
rotateAnim.toValue=[NSValue valueWithCATransform3D:aTransform];
layer.transform = aTransform;
[layer addAnimation:rotateAnim forKey:#"rotateAnim"];
In this example, I apply perspective (m34) in both the fromValue and the toValue of the layer. This then gives you perspective relative to an observer standing in front of the layer itself. Your original code had an observer standing in front of the center of the view, and so you get some skew perspective.
I made some other small changes here. I typically put the observer at 2000 rather than 1000. I find the perspective at 1000 is often more than is desired (unless you're actively going for strong perspective). Depending on your problem, you might want to get rid of perspective entirely. Don't apply it just out of habit.
More importantly, I changed how you were applying the animation at the end. Using removedOnCompletion=NO and kCAFillModeForwards makes things much more complicated than just applying the animation to the model (layer.transform=aTransform) and letting the animation be removed normally. In particular, using removedOnCompletion=NO means that later requests for layer.transform will always return the old value, not the current value. It also makes things complicated if you apply further animations to this layer. This is why the default is to remove the animation when complete.

Related

Stop CABasicAnimation at specific point

I'm using a rotation animation created with CABasicAnimation. It rotates a UIView over 2 seconds. But I need to be able to stop it when the UIView is touched. If I remove the animation the view is in the same position as before the animation started.
Here's my animation code:
float duration = 2.0;
float rotationAngle = rotationDirection * ang * speed * duration;
//rotationAngle = 3*(2*M_PI);//(double)rotationAngle % (double)(2*M_PI) ;
CABasicAnimation* rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.toValue = [NSNumber numberWithFloat: rotationAngle ];
rotationAnimation.duration = duration;
rotationAnimation.cumulative = YES;
rotationAnimation.removedOnCompletion = NO;
rotationAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
rotationAnimation.fillMode = kCAFillModeForwards;
rotationAnimation.delegate = self;
[self.view.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
How can I stop the UIView's rotation right where it is, when it's touched? I know how to manage the touch part, but I can't figure out how to stop the view at the animation's current angle.
Solution:
I solved the problem by getting the angle of the presentation layer, removing the animation and setting the view's transform. Here's the code:
[self.view.layer removeAllAnimations];
CALayer* presentLayer = self.view.layer.presentationLayer;
float currentAngle = [(NSNumber *)[presentLayer valueForKeyPath:#"transform.rotation.z"] floatValue];
self.view.transform = CGAffineTransformMakeRotation(currentAngle);
Good question! For this, it's helpful to know the Core Animation architecture.
If you check out the diagram in the Core Animation Programming Guide that describes the Core Animation Rendering Architecture, you can see that there's three trees.
You have the model tree. That's where you set the values of what you want to happen. Then there's the presentation tree. That's what is pretty much happening as far as the runtime is concerned. Then, finally is the render tree. That's what the user sees.
In your case, you want to query the values of the presentation tree.
It's easy to do. For the view that you have attached the animation, get the layer and for that layer, query the presentationLayer's values. For example:
CATransform3D myTransform = [(CALayer*)[self.view.layer presentationLayer] transform];
There's no way to "pause" an animation mid flow. All you can do is query the values, remove it, and then re-create it again from where you left off.
It's a bit of a pain!
Have a look at some of my other posts where I go into this in a bit more detail, e.g.
Restoring animation where it left off when app resumes from background
Don't forget also that when you add an animation to a view's layer, you aren't actually changing the underlying view's properties. So what happens? We'll you get weird effects where the animation stops and you see the view in it's original position.
That's where you need to use the CAAnimation delegates. Have a look at my answer to this post where I cover this:
CABasicAnimation rotate returns to original position
You need to set the rotation to the rotation of the presentationLayer and then remove the animation from the layer. You can read about the presentation layer in my blog post about Hit testing animating layers.
The code to set the final rotation would be something like:
self.view.layer.transform = [(CALayer*)[self.view.layer presentationLayer] transform];
[self.view.layer removeAnimationForKey:#"rotationAnimation"];

How do I animate CATransform3Ds with a CAKeyframeAnimation?

I've used CAKeyframeAnimations to animate a layer's transform.rotation and transform.translation.x properties, but I'm having trouble animating the transform property implicitly. I have a layer that must animate between two states and CABasicAnimation's default interpolation is totally incorrect and doesn't follow the path I want. CAKeyframeAnimation to the rescue, or so I thought. Any attempt to animate transform using a CAKeyframeAnimation results in the view immediately snapping to the final transform while the other animations run. If I remove the first half of the following function and let my "transform" events use the CABasicAnimation on the bottom, it animates just fine - albeit with incorrectly interpolated transforms along the way.
My layer delegate has implemented the following:
- (id <CAAction>) actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
if ([event isEqualToString:#"transform"])
{
CGSize startSize = ((CALayer *)self.layer.presentationLayer).bounds.size;
CGSize endSize = self.layer.bounds.size;
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:event];
animation.duration = 0.25;
NSMutableArray *values = [NSMutableArray array];
int stepCount = 10;
for (int i = 0; i < stepCount; i++)
{
CGFloat p = i / (float)(stepCount - 1);
CGSize size = [self interpolateBetweenSize:startSize andSize:endSize percentage:p];
CATransform3D transform = [self transformForSize:size];
[values addObject:[NSValue valueWithCATransform3D:transform]];
}
animation.values = values;
return animation;
}
// All other animations use this basic animation
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:event];
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.removedOnCompletion = YES;
animation.fillMode = kCAFillModeForwards;
animation.duration = 0.25;
return animation;
}
My transform is a translation followed by a rotate, but I think a group animation with separate keyframe animations animating through a translation AND a rotate would result in crazy town. I've confirmed that size & transform are correct for all values of p that I pass though, and p strictly ranges from 0 to 1.
I've tried setting a non-default timing function, I've tried setting an array of timing functions, I've omitted the keyTImes, I've set a repeatCount of 0, removedOnCompletion=YES, and a fillMode of forwards and that had no effect. Am I not creating the transform keyframe animation correctly?
This technique worked back in iOS 3 but seemed to be broken in iOS 5.0.
5.1 'magically' fixed this, it seemed to be a bug in iOS 5.0. I'd file a radar, but it is now working in 5.1.
#Gsnyder: Some background: I am experimenting with Clear-like UI (for something completely unrelated to Clear) and came up with this: http://blog.massivehealth.com/post/18563684407/clear. That should explain the need for a rotate & translate.
I've since created a shutter transition that subdivides a view into N layers (instead of just 2) that looks like this when viewed from the side: /////.
My code is not animating the bounds, it is using the size at each step to determine the necessary transform.
#Paul.s: Implicit allows me to keep this abstraction within the layer class itself without polluting the view controller that owns it. The view controller should just be changing the bounds around and the layer should move appropriately. I'm not a fan of view controllers having dozens of custom animations when the views themselves can handle it.
I need to use a keyframe animation because the default animation between layer transforms / and _ animate through incorrect angles so the ///\ layers do not line up throughout the transform. The keyframe animations ensure the edges all line up correctly while they all animate.
I'm considering this closed, this seems to be a bug in iOS 5.0 and has since been fixed. Thanks everyone.
(void)animateViewWith3DCurrentView:(UIView *)currentView withPoing:(CGPoint)movePoint
{
//flip the view by 180 degrees in its place first.
currentView.layer.transform = CATransform3DRotate(currentView.layer.transform,myRotationAngle(180), 0, 1, 0);
//set the anchor point so that the view rotates on one of its sides.
currentView.layer.anchorPoint = CGPointMake(0.5, 0.5);
//Set up scaling
CABasicAnimation *resizeAnimation = [CABasicAnimation animationWithKeyPath:kResizeKey];
//we are going to fill the screen here. So 423,337
[resizeAnimation setToValue:[NSValue valueWithCGSize:CGSizeMake(423, 337)]];
resizeAnimation.fillMode = kCAFillModeForwards;
resizeAnimation.removedOnCompletion = NO;
// Set up path movement
UIBezierPath *movePath = [UIBezierPath bezierPath];
//the control point is now set to centre of the filled screen. Change this to make the path different.
// CGPoint ctlPoint = CGPointMake(0.0, 0.5);
CGPoint ctlPoint = CGPointMake(1024/2, 768/2);
//This is the starting point of the animation. This should ideally be a function of the frame of the view to be animated. Hardcoded here.
// Set here to get the accurate point..
[movePath moveToPoint:movePoint];
//The anchor point is going to end up here at the end of the animation.
[movePath addQuadCurveToPoint:CGPointMake(1024/2, 768/2) controlPoint:ctlPoint];
CAKeyframeAnimation *moveAnim = [CAKeyframeAnimation animationWithKeyPath:kPathMovement];
moveAnim.path = movePath.CGPath;
moveAnim.removedOnCompletion = YES;
// Setup rotation animation
CABasicAnimation* rotateAnimation = [CABasicAnimation animationWithKeyPath:kRotation];
//start from 180 degrees (done in 1st line)
CATransform3D fromTransform = CATransform3DMakeRotation(myRotationAngle(180), 0, 1, 0);
//come back to 0 degrees
CATransform3D toTransform = CATransform3DMakeRotation(myRotationAngle(0), 0, 1, 0);
//This is done to get some perspective.
CATransform3D persp1 = CATransform3DIdentity;
persp1.m34 = 1.0 / -3000;
fromTransform = CATransform3DConcat(fromTransform, persp1);
toTransform = CATransform3DConcat(toTransform,persp1);
rotateAnimation.toValue = [NSValue valueWithCATransform3D:toTransform];
rotateAnimation.fromValue = [NSValue valueWithCATransform3D:fromTransform];
//rotateAnimation.duration = 2;
rotateAnimation.fillMode = kCAFillModeForwards;
rotateAnimation.removedOnCompletion = NO;
// Setup and add all animations to the group
CAAnimationGroup *group = [CAAnimationGroup animation];
[group setAnimations:[NSArray arrayWithObjects:moveAnim,rotateAnimation, resizeAnimation, nil]];
group.fillMode = kCAFillModeForwards;
group.removedOnCompletion = NO;
group.duration = 0.7f;
group.delegate = self;
[group setValue:currentView forKey:kGroupAnimation];
[currentView.layer addAnimation:group forKey:kLayerAnimation];
}

Non-Smooth animation with Core Animation

This is a weird request, but I'm using Core Animation (CALayers), and I want my animation to be choppy and non-smooth. I want an image I set up to rotate like a second hand on a clock. Here's my code:
UIImage *arrowImage = [UIImage imageNamed:#"arrow.jpg"];
CALayer *arrow = [CALayer layer];
arrow.contents = (id)arrowImage.CGImage;
arrow.bounds = CGRectMake(0, 0, 169.25, 45.25);
arrow.position = CGPointMake(self.view.bounds.size.width / 2, arrowImage.size.height / 2);
arrow.anchorPoint = CGPointMake(0.0, 0.5);
[self.view.layer addSublayer:arrow];
CABasicAnimation *anim1 = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
anim1.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
anim1.fromValue = [NSNumber numberWithFloat:0];
anim1.toValue = [NSNumber numberWithFloat:((360*M_PI)/180)];
anim1.duration = 4.0;
[arrow addAnimation:anim1 forKey:#"transform"];
It produces a gliding motion, which I don't want. How do I get around this?
Any help is appreciated.
If you want it to be really choppy, don't use Core Animation at all. On the other hand, if you want something somewhere in between those two extremes, don't use linear media timing. Instead, you might want to try kCAMediaTimingFunctionEaseIn so that the animation accelerates slightly as the hand moves.
The simple way to do this would be to simply apply a transform to your view. The second hand would snap from one position to the next. Just change the rotation by 360/60 = 6 degrees for each second.
If you want the second-hand to do an animation for each tick, you could use a very fast UIView block-based animation. (say with a 1/15 second duration or so.)
Take a look at the UIView class methods who's names start with animateWithDuration.
Something like this:
- (void) moveSecondHand;
{
seconds++;
angle = M_PI*2*seconds/60 - M_PI/2;
CGAffineTransform transform = CGAffineTransformMakeRotation(angle);
[UIView animateWithDuration: 1.0/15
animations: *{
secondHand.transform = transform
}];
}
That's about all it would take. You're trigger that code with a timer once a second. By default animations use ease-in, ease-out timing, which models physical movement pretty well. Try different durations, but 1/15 is probably a good starting point (you want it fast, but not too fast to see.)
If you want a wobble to your animation you will need to get much fancier, and create an animation group that first moves it by the full amount, and then does a repeating animation that overshoots the stopping point by a small amount and then goes back.

iPhoneSDK Setting CALayer.transform does not work after animation is finished

I have a CALayer object. In viewDidLoad: I set a transform to it using layer.transform = CATransform3DRotate(CATransform3DIdentity, M_PI/8.0, 0, 0, 1); This works.
After loading, I apply a CAKeyframeAnimation to animate the rotation of that layer. It also works and animation finishes correctly. To prevent the layer from removing the animation after completion I use
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
All of this works fine. But a problem occurs when I try to change the transform again (without animation) layer.transform = CATransform3DRotate(CATransform3DIdentity, M_PI/8.0, 0, 0, 1);. Nothing happens. Layer doesn't change its transform to what I have specified. If I run the animation again, the animation will happen. But cannot change the transform property without animation.
Any help is appreciated..
First of all, why are you avoiding removing the animation upon completion? You don't need these two lines,
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
since you are actually setting the layer's transform property. Those two lines are what you would use if all you wanted was the final visual state after the animation while keeping the transform the same as the original (which frankly I've never had a use for).
The next thing you have to realize is that you are setting the transform to the same value twice. Setting the transform on a layer is not additive by default. What I mean is that while you are expecting it to take your first transform and concatenating the same transform again from the final transform state, it doesn't work that way with the code you're using. You won't see a change because you're setting the state to the state it already has.
I haven't tried it, but I think that you could probably take the first transform and concatenate the new one. Something like:
layer.transform = CATransform3DRotate(layer.transform, M_PI/8.0, 0, 0, 1);
Notice how I'm starting from the current transform on the layer (first parameter of CATransform3DRotate), instead of identity. Give that a try. If it doesn't work, try something like:
layer.transform = CATransform3DConcat(layer.transform,
CATransform3DMakeRotation(M_PI/8.0, 0, 0, 1));
OK, I managed to get this working.
What I did was setting the layer.tranform just after calling [layer addAnimation:]. In that way, after the animation is finished, the layer will stay in the desired position as it is. Here is my code:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
animation.repeatCount = 1;
animation.duration = 3.0;
animation.fromValue = [NSNumber numberWithDouble:currentRotationAngle];
animation.toValue = [NSNumber numberWithDouble:newRotationAngle];
[layer addAnimation:animation forKey:#"rotationAnimation"];
[layer setAffineTransform:CGAffineTransformMakeRotation(newRotationAngle)];
The problem I had was using layer.removedOnCompletion and layer.fillemode. I don't have to use them. In the new way the layer will display the layer using the layer.transform after animation is complete. CAAnimations only update the layer's presentationLayer (which maintains the layer's visual state) while animating. When the animation is finished, it will revert the layer's visual state to the one imposed by layer.transform.

How come my Core Animation transformation always returns to it's start-state?

I am trying to perform some kind of animation of a layer in my iPhone application. It does not matter what I do I always get the same results: after the animation is done it jerks back into it's original position. Even though I set removedOnCompletion to false there is no difference.
What am I missing here?
Thanks in advance!
EDIT: Really need help with this one guys. I am creating animations with CAKeyframeAnimation and CABasicAnimation objects, then adding them to a CAAnimationGroup which I in turn att to the layer. The animation works as predicted except that it always snaps back to it's original state. This is the case even though I set removedOnCompletion = NO; on all animation-objects and the animation group.
Some one please point me in the right direction! I you live in the Stockholm area I will buy you a coffe. =) New code posted below:
CABasicAnimation *leveloutLeafAnimation = [CABasicAnimation animationWithKeyPath:#"transform"];
leveloutLeafAnimation.removedOnCompletion = NO;
leveloutLeafAnimation.duration = 1.0;
leveloutLeafAnimation.repeatDuration = 20;
CATransform3D transformLeafToRotation = CATransform3DMakeRotation(0.0, 0.0, 0.0, 1);
CATransform3D transformLeafFromRotation = CATransform3DMakeRotation([self _degreesToRadians:270.0], 0.0, 0.0, 1);
leveloutLeafAnimation.fromValue = [NSValue valueWithCATransform3D:transformLeafFromRotation];
leveloutLeafAnimation.toValue = [NSValue valueWithCATransform3D:transformLeafToRotation];
//Create an animation group to combine the animations.
CAAnimationGroup *theAnimationGroup = [CAAnimationGroup animation];
//The animationgroup conf.
theAnimationGroup.delegate = self;
theAnimationGroup.duration = animationDuration;
theAnimationGroup.removedOnCompletion = NO;
theAnimationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
theAnimationGroup.animations = [NSArray arrayWithObjects:leveloutLeafAnimation, leafMoveAnimation, nil];
// Add the animation group to the leaf layer.
[leafViewLayer addAnimation:theAnimationGroup forKey:#"animatLeafFalling"];
Not sure if this is helpful, but this guy seems like he was having a similar problem.
You might have to set the transform property of the layer while the animation is running to transformLeafToRotation.
Good luck!