How to make a CATransform3dMakeRotation rotate the

2019-04-12 12:25发布

问题:

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"];

回答1:

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.



回答2:

I'm guessing the flipping on an axis is caused by gimbal lock happening inside Apple's library. The easy fix that works 99.999% of the time is to just not use exact 0, 90, 180, 270, and 360 degree rotations. I've seen stuff like this happen in my code, but I just use something like (M_PI*0.99) instead of M_PI. This is always a problem if you are using the video card's matrix multipliers with euler angles.

To apply multiple transforms at the same time, you can nest them. This works exactly like multiplying matrices in OpenGL.

// translate and scale at the same time
float scale = 1.6;
CATransform3D scaleMatrix = CATransform3DMakeScale(scale, scale, 1);
CATransform3D finalMatrix = CATransform3DTranslate(scaleMatrix, frame.size.width/scale, frame.size.height/scale, 0);
CABasicAnimation* animBottomTrans = [CABasicAnimation animationWithKeyPath:@"transform"];
[animBottomTrans setFromValue:[NSValue valueWithCATransform3D:finalMatrix]];
[animBottomTrans setToValue:[NSValue valueWithCATransform3D:CATransform3DIdentity]];
[animBottomTrans setDuration:duration];
[myLayer addAnimation:animBottomTrans forKey:@"transform"];

Remember that since these are nested you have to deal with the previous transform in any transforms that come after it. In this code I have to change the amount I translated because I have already scaled. If you rotate 90degrees on the z axis and then rotate 90 degrees on the y axis, you have effectively just rotated on the z and x axises (the z rotation swaps your x and y axises).



回答3:

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...