Anatomy of a folding animation

In this post I want to dive into the minutiae of how a good-looking folding animation can be achieved, and also how I was doing it wrong the first time I took a stab at creating one.  Earlier this year I started looking into fold animations as an example of advanced use of matrix transforms to animate views for my Enter The Matrix presentation for CocoaConf Chicago.

(For a similar in-depth look at page-flip animations see Anatomy of a page-flip animation.)

Initial Efforts


It seemed relatively straight-forward.  Given 4 “panels” (A,B,C,D if you will), fold B & C inward until they disappear while sliding A & D together until they meet.  So the first step was rotating B around the x-axis away from the user along its top edge, and the same for C except along its bottom edge.

Of course that leaves a gap between B & C, so you need to move B down and C up by just the right amount and also move A & D to keep pace with them.  I quickly discovered that the height of panels B & C during rotation is a cosine function of the angle of rotation and not linear.  However, due to perspective issues B & C don’t disappear (are completely on edge) at 90° the way you would expect.  Because you are simultaneously translating and rotating the panels, at 90° the panels are actually slightly over rotated relative to the viewer.  To get them to just disappear, you need to rotate them to arctan((z × 2) / y) where z is the height of the viewer (see perspective below) and y is the height of the panel (or roughly 83.9° in my case).  So you have to rotate the panels from 0 to 83.9° while animating the vertical offset of panels A-D from 0 to cos(±90°) × height.  This roughly works, but if you examine the animation closely, you either see a small gap open between 2 of the 4 panels or else an overlap of a few pixels between 2 of the panels.

Enter CATransformLayer

It turns out, I was thinking about this all wrong.  What I needed to use in order to keep all 4 of my panels together was CATransformLayers.  I was pointed in the right direction by this StackOverflow answer.  As Apple’s documentation says, “CATransformLayers are used to create true 3D layer hierarchies”.  Instead of treating all 4 panels as separate UIViews that need to be transformed independently, we will create the panels as 4 sublayers that are organized together along with CATransformLayers into the proper sublayer hierarchy within a single UIView.  The transform layers keep each panel layer properly adjoined to its neighbors.  To follow along with code, see my MPFoldTransition project (which was the culmination of this dive into folding animations), but in this post here I’ll be talking more about the big picture rather than code details.

I ended up building a layer hierarchy with all 4 panels contained within a single view, and panels B & C (the 2 folding panels) contained within the bounds of a single parent layer (the purpose of which will become clear in a bit).  The difference is that now all these layers are connected together relative to one another in a true 3D hierarchy, so when I rotate panel B away from the user, all 4 panels rotate like so:


In order to compensate for this rotation and to keep panel A flat, I need to rotate panel A in the opposite direction along its bottom edge by the same amount.


Then panel C needs to be rotated towards the user by double the angle of rotation of panel B.


And finally in order to remain flat, panel D needs to be rotated away from the user by the angle of rotation of panel B.  Notice how panels A & D need to be rotated just so that they appear to remain flat throughout the entire animation.  This is of course because each panel is being rotated relative to its parent layer and not relative to the plane of the entire view.

Perspective

This gives us all 4 panels well-connected with no gaps or overlaps, but we’re still not finished.  If you look at the last 2 pictures above you may notice a perspective issue; i.e. that panels B & C are not of equal height.  It’s more noticeable the closer you get to 90°.


This is a perspective issue similar to the one I faced in my original design.  To fix it, at the same time we animate the rotation of these 4 panels, we need to also animate the height of the 2 folding panels B & C to 0.  This keeps the 2 panels of equal height throughout the animation.  If you wish for the border between B & C to remain in a fixed position vertically (as I did), then you actually need to animate the height using a cosine curve rather than a simple linear one (see the code for the CAKeyframeAnimation implementation).

Shadows


Now that perspective is tackled, the next issue is one of improving the 3D illusion by dimming the folding panels as they collapse.  This is easily achieved by adding a sublayer to each folding panel layer and animating its opacity.


This helps, but when the 2 folding panels have the same background color (as they usually do), then they seem to be a single panel (morphing into an hourglass shape instead of folding away from us), which detracts from the 3D illusion.  To solve this, I adjusted the maximum opacity of the 2 shadow layers to be slightly different (set one  to be 90% of the other).  This provides a nice contrast between the 2 folding panels as seen here:

The proper (amount of) perspective

The perspective value that you set as the m34 component of your CATransform3D struct is = -1/z where z is the height of the viewer in points from the view’s surface.  Changing this value dramatically affects the illusion of perspective.  If 0 (z approaching infinity), everything is flat.

No perspective: m34 = 0

If too small (viewer still too far away) then the perspective is too subtle.

Too little perspective: m34 = -1/1800 or -1/(12 × height)

If too large (viewer too close) then the perspective is exaggerated and distorted

Too much perspective: m34 = -1/150 or -1/(1 × height)

I prefer to adjust z relative to the dimension of the objects I am animating. In this case I’ve been happy with 4.666667 × height. I think any value in the range from 4 to 5 yields attractive results.

Other Minutiae

Of course being the OCD-type, I just had to antialiase the edges of the folding panels so that they’re not jagged, even though my default animation duration is only 300 ms.


A different cool effect (which I termed “cubic” for wont of a better description) can be achieved if you rotate panels A & D at a fixed 90° angle relative to their neighbors instead of animating them to remain flat.  The effect is akin to rotating a block- one side appears while another disappears.


If the folding panels (B & C) are created by splitting a single view in 2, and that view has an odd height (in pixels, not points) then once again you see flickering of gaps between panels during the animation. To solve this I make both panels of integer pixel height. e.g. when splitting a view 99 pixels tall, I’ll make the panels 50 and 49 pixels tall instead of 49.5. (Note that on a Retina display 49.5 points is just fine because that’s really 99 pixels.)

Update: Gradient Shadows

I recently switched to using CAGradientLayers for the shadows.  I think these look much better than the solid color shadows, and they also create the impression of a crease between the two folding panels (because the light end of one gradient will be juxtaposed with the dark end of the other).

However, if the bottom sleeve (D) has the same background color as the lower folding panel (C), you end up with no crease if the gradient shadow blends all the way to clear.

Fixing this is simple – just set the end color of the lower gradient to around [[UIColor blackColor] colorWithAlphaComponent:0.25] instead of clear.  This will create enough color difference to suggest a crease while not darkening the lower panel overly much.

This isn’t necessary for the cubic effect, because then panels A and D will have their own shadow gradients.

To achieve the proper effect, I animate the opacity of the shadows on panels B & C along a cosine path so that their intensity is (inversely) proportional to the height of the panel.  For the cubic effect, the shadows on panels A & D are similarly animated along a sine path (again so as to be inversely proportional to panel height).  Using a simple linear animation would lead to a discrepancy between the strength of the shadows and the positions of the panels.

Resources

  • MPFoldTransition project with demo app and code you are free to use to easily add these same animations to your own iOS apps.
  • Enter The Matrix project with lots of matrix transformation examples, including both flip and fold animations (the project has now been updated to incorporate the techniques described above).
  • Edge Antialiasing post with a discussion of what it is, why you’d want to do it, and how to do it efficiently.  Also with its own sample project.
  • If you’re going to be on the East coast in June, come see me speak about these types of things at CocoaConf DC (and of course learn from a great lineup of even better speakers).

MPFoldTransition – add fold transitions to your app


I’ve put a project I call MPFoldTransition up on GitHub. It provides a class you can use to add a fold transition to your application in just a single line of code (in most cases).
What is a fold transition? It’s an animation such as shown above (or as popularized by the Clear todo list app) where an object folds in upon itself until it disappears and the content surrounding it moves in to fill the gap. Except in this case, I’m doing it for an entire UIView or UIViewController and not just a single UITableViewCell. (For an in-depth analysis of what goes into making a folding animation, read my post here.)

Update: The GitHub project now includes flip transitions as well!

Update 2: The folding effect has been improved with gradient shadows.

Update 3: For a touch gesture-enabled container controller with page-flipping (not just a transition), see MPFlipViewController.

Features

There are 3 style bits that can be combined to create 8 different animations.

Direction


Controls whether the current view collapses inward (Fold) or if the next view expands outward (Unfold).

Mode


Determines whether the next view slides in flat (Normal) or rotates in at right angles to the collapsing halves of the previous view (Cubic).

Orientation


Sets whether the fold is vertical or horizontal.

Present a modal view controller

There are extension methods to UIViewController to present or dismiss modal view controllers using fold transitions:

- (void)presentViewController:(UIViewController *)viewControllerToPresent
                    foldStyle:(MPFoldStyle)style 
                   completion:(void (^)(BOOL finished))completion;

- (void)dismissViewControllerWithFoldStyle:(MPFoldStyle)style 
                                completion:(void (^)(BOOL finished))completion;

From your UIViewController subclass you would call this to present your modal view controller:

[self presentViewController:modalViewController
                  foldStyle:MPFoldStyleDefault 
                 completion:nil];

And then call this to dismiss it:

[self dismissViewControllerWithFoldStyle:MPFoldStyleUnfold 
                              completion:nil];

Tip: dismiss your modal controller using a style with the opposite fold bit (Fold or Unfold), so that you get the reverse animation.

Push a view controller onto a navigation stack

There are extension methods to UINavigationController to push or pop a view controller using fold transitions:

- (void)pushViewController:(UIViewController *)viewController
                 foldStyle:(MPFoldStyle)style;

- (UIViewController *)popViewControllerWithFoldStyle:
    (MPFoldStyle)style;

From your UIViewController subclass you would call this to push a new view controller onto the stack:

[self.navigationController pushViewController:detailViewController
                                    foldStyle:MPFoldStyleDefault];

And then call this to pop it back off:

[self.navigationController popViewControllerWithFoldStyle:MPFoldStyleUnfold];

Tip: pop your view controller using a style with the opposite fold bit (Fold or Unfold) from the style used to push it onto the stack, so that you get the reverse animation.

Transition between any 2 views or controllers

MPFoldTransition has class methods for generic view and view controller transitions:

+ (void)transitionFromViewController:(UIViewController *)fromController
                    toViewController:(UIViewController *)toController
                            duration:(NSTimeInterval)duration
                               style:(MPFoldStyle)style
                          completion:(void (^)(BOOL finished))completion;

+ (void)transitionFromView:(UIView *)fromView
                   toView:(UIView *)toView
                 duration:(NSTimeInterval)duration
                    style:(MPFoldStyle)style
         transitionAction:(MPTransitionAction)action
               completion:(void (^)(BOOL finished))completion;

If you really need to get under the hood (e.g. to adjust the timing curve), you can initialize your own instance of MPFoldTransition, set the properties as desired, and then call the perform method to execute the transition.

Storyboard support

You can even incorporate modal or navigation stack fold segues without writing a single line of code! Simply use 1 of the 3 custom UIStoryboardSegue subclasses that are included. These cover modal presentation and push/pop to a navigation stack. Just create a segue between 2 controllers in your storyboard, select Custom as the segue type, then enter MPFoldModalSegue, MPFoldNavPushSegue, or MPFoldNavPopSegue as the Segue Class.

Demo project

The GitHub project includes a sample project that demonstrates the use of all the different API’s as well as all of the transition styles.

iOS version

iOS 5-only for now because I wrote it with ARC and included storyboard support.