Flipping with proper perspective distortion in Core Animation
An ordinary flip transition in UIKit (for example, one you bring up with UIViewAnimationOptionTransitionFlipFromLeft or UIViewAnimationOptionTransitionFlipFromRight) goes like this:
The “front” view rotates 180° clockwise or counterclockwise
The “back” view simultaneously rotates 180° clockwise or counterclockwise from behind
There is visible perspective distortion (me gusta)
In the middle of the transition, the camera dollies back a bit, so the corners of the animated layers are fully visible, instead of being clipped
Stock stuff is never enough. Let’s replicate this so it can be hacked. Actually, replication is fairly easy:
Creating temporary view hierarchy
First, a few ideas with customized UIKit / Core Animation transitions: a. Everything on the screen can be ephemeral. b. Everything can be a image view holding a rendered proxy image.
Let’s get to work. Assuming there’s a containerView, which contains a frontView and a backView, both at the same size of the container view:
containerView +- frontView +- backView
As long as the frontView and the backView are simultaneously animated, the flip animation is done.
We’ll first make sure the layers are no longer double-sided. This prevents headaches with overlapping stuff, and some annoying artifacts when one attempts to fade, or further animate, the container view, without having to hack zPosition:
frontView.layer.doubleSided = NO; backView.layer.doubleSided = NO;
Deal with the back view first by assigning a rotation transform:
backView.layer.transform = CATransform3DMakeRotation(M_PI, 0, 1, 0);
Now the view hierarchy is fully built and ready for animating / transforming.
Implementing the flip animation
If you simply slap a CABasicAnimation onto frontView.layer and backView.layer, you’ll find that the layers will often not rotate in the intended direction. Use CAKeyframeAnimation objects instead. For example, you’ll want this animation for the front view:
CAKeyframeAnimation * flipToBackAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform"]; flipToBackAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; flipToBackAnimation.removedOnCompletion = YES; flipToBackAnimation.duration = animationDuration; /* Usually 0.5 */ flipToBackAnimation.values = [NSArray arrayWithObjects: [NSValue valueWithCATransform3D:CATransform3DMakeRotation(0, 0, 1, 0)], [NSValue valueWithCATransform3D:CATransform3DMakeRotation(0.5 * M_PI, 0, 1, 0)], [NSValue valueWithCATransform3D:CATransform3DMakeRotation(M_PI, 0, 1, 0)], nil])); /* flipToFrontAnimation is omitted */
Make another animation running from M_PI to 2 * M_PI for the back view.
Now, set the transforms on the layers to the proper values before animating:
frontView.layer.transform = [[flipToBackAnimation.values lastObject] CATransform3DValue]; backView.layer.transform = [[flipToFrontAnimation.values lastObject] CATransform3DValue];
Finally slap them on the layer when the time has came:
[frontView.layer addAnimation:flipToBackAnimation forKey:kCATransition]; [backView.layer addAnimation:flipToFrontAnimation forKey:kCATransition];
Adding perspective distortion
At this point your layers should be flipping as intended, but there is no perspective distortion like in the UIKit version. Actually, this is fairly easy. First create a CATransform3D:
CATransform3D perspectiveTransform = CATransform3DIdentity; perspectiveTransform.m34 = -1.0f / 850.0f;
The idea is to modify value of m34 in the transformation matrix, so the distance from the layers to the projection plane is changed. Apple simply states that (where the zDistance is 850.0f in our case)
The value of zDistance affects the sharpness of the transform. (Core Animation Programming Guide)
Empirically a value of 2000 also works nicely. At this point, slap it onto the containerView already:
self.containerView.layer.sublayerTransform = perspectiveTransform;
As an aside, this is what Apple says in the header for sublayerTransform:
A transform applied to each member of the `sublayers' array while rendering its contents into the receiver's output. Typically used as the projection matrix to add perspective and other viewing effects into the model. Defaults to identity. Animatable.
Implementing camera dollying
Camera dollying is seen in Apple’s flip animation implementation preventing the rotated layers from get clipped by screen bounds. It can be implemented either as a Z offset translation animation, or a scale animation. Empirically, a scale animation works a bit better:
CAKeyframeAnimation * scaleAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform"]; scaleAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; scaleAnimation.removedOnCompletion = YES; scaleAnimation.duration = animationDuration; /* Usually 0.5 */ scaleAnimation.values = [NSArray arrayWithObjects: [NSValue valueWithCATransform3D:CATransform3DIdentity], [NSValue valueWithCATransform3D:CATransform3DMakeScale(.95, .95, .95)], [NSValue valueWithCATransform3D:CATransform3DIdentity], nil]));
Put this on the container view’s layer. At this point, what you’ve got should be pretty close to Apple’s implementation.
Ideally, the view being hidden should fade out at the middle of the rotation, while the view being shown should fade in after the transition has gone 50%. This can be (effortlessly) done with a keyframe animation on opacity, wrapped together with the scale animation on the container view, in a CAAnimationGroup.






