views:

119

answers:

3

I'm working with some Core Animation for the first time and in the process of implementing a playing card that I can flip around, I've decided to use a CALayer to display the contents (not sure how I'm going to get two sides, but that's another question) and I need to be able to flip it over, move it, etc...

I'm using CATransaction with some success - in the snippet below, the card moves from the bottom left to the top left and flips over. The problem is that I wan't it to flip the opposite way, but don't know how to tell it, "hey, you're going the wrong way!"


[CATransaction begin]; 
[CATransaction setValue:[NSNumber numberWithFloat:2.0f] forKey:kCATransactionAnimationDuration];
myCard.position = CGPointMake(CGRectGetMidX(self.bounds)/2,CGRectGetMidY(self.bounds)/2);
myCard.transform = CATransform3DMakeRotation(M_PI, 1, 0, 0);
[CATransaction commit]; 

A second question would be: how can I get it to do two transforms at once? I've tried nesting two CATransactions, but the second one just overrides the first. I also tried changing the vector for the rotation to be 2D, like saying to rotate around x and y axes, but that isn't equivalent to just flipping it by pi around two individual axes. Here's the nested code.


[CATransaction begin]; 
[CATransaction setValue:[NSNumber numberWithFloat:2.0f] forKey:kCATransactionAnimationDuration];
myCard.position = CGPointMake(CGRectGetMidX(self.bounds)/2,CGRectGetMidY(self.bounds)/2);
myCard.transform = CATransform3DMakeRotation(M_PI, 1, 0, 0); // rotate about x

  [CATransaction begin]; 
  [CATransaction setValue:[NSNumber numberWithFloat:1.0f] forKey:kCATransactionAnimationDuration];
  myCard.transform = CATransform3DMakeRotation(M_PI, 0, 1, 0); // rotate about y
  [CATransaction commit]; 

[CATransaction commit]; 

And here it is with UIView animation blocks inside... I've added sliders for the angle, x, y, z for the rotation vector, and t for time. The translation takes place over time = 2t and each of the rotations should take just t each.

[CATransaction begin]; 
[CATransaction setValue:[NSNumber numberWithFloat:t * 2] forKey:kCATransactionAnimationDuration];
myCard.position = CGPointMake(CGRectGetMidX(self.view.bounds)/2,CGRectGetMidY(self.view.bounds)/2);

  [UIView beginAnimations:nil context:nil];
  [UIView setAnimationDuration:t];
  myCard.transform = CATransform3DMakeRotation(angle, x, y, z);
  [UIView commitAnimations]; 

  [UIView beginAnimations:nil context:nil];
  [UIView setAnimationDuration:t];
  [UIView setAnimationDelay:t];
  myCard.transform = CATransform3DMakeRotation(angle * 2, x, y, z);
  [UIView commitAnimations];

[CATransaction commit]; 

And this is where I am now: It all works with one exception. The y rotation reverses (starts rotating in the opposite direction) when the card gets to pi/2 and 3*pi/2. It also flips about the x axis at these points. But x and z work great. So close!

CGMutablePathRef thePath = CGPathCreateMutable();
CGPathMoveToPoint(thePath,NULL,CGRectGetMidX(self.view.bounds)/2,CGRectGetMidY(self.view.bounds)/2*3);
CGPathAddCurveToPoint(thePath,NULL,
                      CGRectGetMidX(self.view.bounds)/2,CGRectGetMidY(self.view.bounds)/2*2,
                      CGRectGetMidX(self.view.bounds)/2,CGRectGetMidY(self.view.bounds)/2*1.5,
                      CGRectGetMidX(self.view.bounds)/2,CGRectGetMidY(self.view.bounds)/2);
CAKeyframeAnimation *moveAnimation=[CAKeyframeAnimation animationWithKeyPath:@"position"];
moveAnimation.path=thePath;
CFRelease(thePath);

CABasicAnimation *xRotation;
xRotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.x"];
xRotation.fromValue = [NSNumber numberWithFloat:0.0];
xRotation.toValue = [NSNumber numberWithFloat:x * angle * M_PI];

CABasicAnimation *yRotation;
yRotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.y"];
yRotation.fromValue = [NSNumber numberWithFloat:0.0];
yRotation.toValue = [NSNumber numberWithFloat:y * angle * M_PI];

CABasicAnimation *zRotation;
zRotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
zRotation.fromValue = [NSNumber numberWithFloat:0.0];
zRotation.toValue = [NSNumber numberWithFloat:z * angle * M_PI];

CAAnimationGroup *groupAnimation = [CAAnimationGroup animation];
groupAnimation.duration = t;
groupAnimation.removedOnCompletion = NO;
groupAnimation.fillMode = kCAFillModeForwards;
groupAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
groupAnimation.animations = [NSArray arrayWithObjects:moveAnimation, xRotation, yRotation, zRotation, nil];

[myCard addAnimation:groupAnimation forKey:@"animateCard"];
A: 

For your first question: I suspect you need to specify a negative angle for the rotation, i.e. -M_PI in your case.

martin clayton
It looks like if I go to almost `-M_PI,` it goes the opposite way as going to `+M_PI,` but if I go to `+M_PI` or `-M_PI` exactly, they apparently are the same to `Core Animation`, and they flip the same direction. I'm guessing that `CA` goes the quickest way there. I think I need to know if there's a way to explicitly tell it to take the long way, or to go a specific direction.
Steve
I just tried this - `[CATransaction begin];` `CATransform3D firstHalf = CATransform3DMakeRotation(-M_PI/2, 1, 0, 0);` `CATransform3D secondHalf = CATransform3DMakeRotation(-M_PI, 1, 0, 0);` `CATransform3DConcat(firstHalf, secondHalf);` `[CATransaction commit];` But it doesn't seem to do anything. Man these 3D things are hard.
Steve
A: 

The resulting transform is the same: (CGAffineTransform){-1,0,0,-1,0,0}. CoreAnimation picks the shortest rotation that will do the trick, and defaults to rotating clockwise (I think).

The easiest way to rotate the other way is to rotate by almost -M_PI (I'm not sure exactly how close you can get before it decides to rotate the "wrong" way).

The more complicated way is to break it up into two rotations: one from 0 to -M_PI/2 and one from -M_PI/2 to -M-PI. I think you can set an animation delay to make this happen...

tc.
I fiddled around with this for a while but haven't found a way to set a delay for a `CATransaction.` I tried nesting two `UIView` animation blocks inside the `CATransaction,` with the second one having a delay, but no luck so far. It might be because the thing that's actually getting drawn is a `CALayer` and not a `UIView`. Maybe? Added this version to the question.
Steve
+2  A: 

I believe that what you want in this case is to use a helper keypath like is described in this answer:

CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.x"];
rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI];
rotationAnimation.duration = duration;
[myCard.layer addAnimation:rotationAnimation forKey:@"rotationAnimation1"];

which should rotate about the X axis for the first stage of your animation. For the second, I believe if you use the following

CABasicAnimation *rotationAnimation2 = [CABasicAnimation animationWithKeyPath:@"transform.rotation.y"];
rotationAnimation2.toValue = [NSNumber numberWithFloat: M_PI];
rotationAnimation2.duration = duration2;
rotationAnimation2.cumulative = YES;
[myCard.layer addAnimation:rotationAnimation2 forKey:@"rotationAnimation2"];

it should make the animations cumulative and may produce the desired effect. I haven't tried this myself, so some tinkering may be required to get the exact result you need.

When you are working with a transform directly, Core Animation will interpolate the transform from the current value to the specified transform. It will find the shortest path to get to that transform, which will restrict the animation direction. If you try to animate the same transform property twice, the second value will simply override the first, not combine the two transforms together.

However, when using a helper keypath like this, Core Animation will interpolate between your starting angle and ending angle, so you can reverse direction by changing the sign of the ending angle. It optimizes the change in angle value, not the change in the underlying transform. You also should be able to combine animations on the keypath, because the transform is generated for you under the hood for each combination of keypath manipulations.

Brad Larson
Thanks, Brad. I found a similar answer (not sure which - I'm at work now...) that used `CAKeyframeAnimation` and had intermediate values for the rotation so that it could guarantee that the animation passed through all the key values. I made it work and can spin my card around all axes by setting start and ends like you'd think, with an intermediate stop half way. Actually it works for x and z perfectly, but with y, for example on the way to 2*M_PI, when it gets to M_PI, it flips and does another spin. I think that may be the same thing and I need more points.
Steve
Oh, and I can't get it to stay where it ends. I remember the `CA` guy from the WWDC video addressing that, but don't remember how to do it. On a a related note, can I do a `CATransaction` and set the transform for `myCard` back to the identity to remove any remaining transforms? (assuming I can get them to stick!)
Steve
EDIT: in my first comment, I was wrong (just played with the app on my phone). It flips when y rotations pass M_PI/2, M_PI and 3*M_PI/2 on the way to 2*M_PI.
Steve
@Steve - To get the animation to persist, set its `fillMode` to `kCAFillModeForwards` and `removedOnCompletion` to `NO`. I'm not sure what you're asking in regards to setting the transform back to the identity transform.
Brad Larson
@Brad - If I do a transform and leave my card upside down or something, then I want to easily bring it back to it's original orientation, can I remove all the transforms? When I was working with `CATransaction` directly, I could do something like `CATransform3DIdenty` (again, I can't remember what it was without being at my Mac). After applying transforms, I could bring it back to the starting point.
Steve
@Steve - If all you want to do is clear all animations and return to the original state, you could call `-removeAllAnimations` on the layer. If you had changed the transform by setting the property yourself, then you'd need to set the property to the identity transform. As an aside, you don't need to use CATransaction every time to make an animation run using Core Animation, only if you want to override timing, do grouping (instead of CAAnimationGroup), or disable animations completely.
Brad Larson
@Brad, you can see that things are working better now above. Maybe you can suggest why the y rotation is broken? Oh, and the `removeAllAnimations` worked just right. Thanks!
Steve
@Steve - I don't understand what you think is wrong here. Of course the card would be reversed when you flip between 90 and 270 degrees, because then the backside of the layer is exposed. You will be looking at the content you drew on the front now from the back, so it will be mirrored. You may need to use a second CALayer for the back side, setting `doubleSided` to NO for both layers so that only one faces the camera at a time.
Brad Larson
@Brad - Sorry, I haven't been clear. I've spent more time with this and can explain it better. When I try to rotate the `CALayer` 2*pi about the y axis, it will begin the rotation fine, but when it gets to pi/2, the rotation will reverse, and the contents of the layer will flip upside down (about the x axis). It will continue to animate in the opposite direction until it's done another pi radians, when it will reverse direction and flip about x again. In this case, the card ends up in the original orientation as expected, but the path it takes is wrong. Again, the x and z work perfectly.
Steve
@Brad - to add to the above comment, the x axis flipping occurs whenever the `CALayer` is perpendicular to the screen. Another example would be just doing a single pi rotation, it'll reverse direction half way and flip about the x axis and end up upside down and backwards. (Backwards is expected). PS - Thanks for taking the time to work through this with me!
Steve
@Brad - Here's my project in case you'd like to see it in action... https://files.me.com/gazelips/lx70gn
Steve