Tuesday, October 28, 2008

Demystifying CGAffineTransform

One of the most common questions I get about iPhone programming is about how to rotate or scale or make various other changes to a view. I'm not talking about hardcore Core Animation kind of stuff, just simple, "How do I place a label at 45°" kind of questions. The expectation is that there should be a property called "angle" or "scale" that can be set, or that you should be able to hold down the option key in Interface Builder to rotate it there.

With Leopard, Apple gave us Core Animation,and it is very cool, but in order to achieve all that coolness, they had to add some concepts that make it not always intuitive. (BTW: If you do want to know more about Core Animation, I've been reading Bill Dudney's Core Animation for Mac OS X and the iPhone and it's very good).

Any change to a view's position, scale, or rotation can now be stored in a property of the view called transform. It's a CGAffineTransform struct, and it's a bit cryptic if you've never worked with transformation matrices before.

A transformation matrix nothing more than a two-dimensional array of numbers. Okay, perhaps I shouldn't say "just", as transformation matrices are quite powerful. They can store complex changes to the position and shape of an object, including rotating, scaling, moving, and skewing (or shearing) the view as it's drawn. This adds a little bit of programmer complexity if you want to do anything more than the absolute basics, but it opens up a world of possibilities.

The CGAffineTransform Data Structure


So, what does this data structure look like? This:


struct CGAffineTransform {
CGFloat a;
CGFloat b;
CGFloat c;
CGFloat d;
CGFloat tx;
CGFloat ty;
};

yeah, it doesn't look like a matrix, does it? It is; it'sit is a 3x3 matrix, it's just that certain values in the matrix are constant, they can't change, so they're not represented by a variable. Here's what the matrix would look like drawn out in human-friendly form:

| a b 0 |
| c d 0 |
| tx ty 1 |


These 9 numbers are used to store the rotation, scale, and position of an object using something called Matrix Multiplication.
Note: By the way, this exact same process is used to transform shapes in three-dimensional graphics. The matrices used for 3D are 4X4 instead of 3x3 to capture the additional z dimension.


So, if a view has a transform, how does Core Graphics figure out how and where to draw it?

Basically, it figures out where it would draw each point (x,y) without the transformation, and then does the following math to figure out the new, transformed point:
new x position = old x position * a + old y position * c + tx
new y position = old x position*b + old y position * d + ty

And that works? Yep, it does, amazingly enough. If you're interested in the math behind it all, there are many good sources. Just google "Matrix Transformation" to find some. If you're going to be doing complex transformations, it's a good idea to have a basic understanding of the underlying maths, but for basic usage, you can get away without it.

The Basic Transformations


The reason you can get away without understanding the intricacies of the math is because Apple has provided us with a number of functions to retrieve standard matrices and to standard transformations. In order to use any of these transformations, you will need to include the CoreGraphics framework in your project, and include the CoreGraphics header file:
#import <CoreGraphics/CoreGraphics.h>


The Identity Transformation


All views (and layers, but we're not talking about layers today) start out with their transform property set to the Identity Matrix. This matrix represents the object without any changes. It hasn't been rotated, scaled, sheared, or translated (moved). For a view that has the identity matrix for the transform property will be drawn based solely on the size and origin in the bounds property.

Note: Views have both a frame (coordinates in superview's coordinate system) and bounds (coordinates in own coordinate system) property, but if you transform a view, you should not use or rely on the frame property anymore. If you are using transformations, work with the bounds property only, not the frame property, as transformations are applied to the bounds, but aren't necessarily reflected accurately in frame


Any time you want to reset a view or layer to its original, untransformed state, you simply set its transform to the Identity Transformation using the constant value CGAffineTransformIdentity like so:
theView.transform = CGAffineTransformIdentity;


The Translate Transformation


Translation is just a fancy way of saying "moving". You can, of course, accomplish a move by changing the origin value of the view's frame property (which moves the view in relation to its superview), but since we can't use the frame property along with other transformations, the translate transformation is an important one. If you want to translate a view, you use a Core Graphics function called CGAffineTransformTranslate(). This method takes three paramters. The first is an existing CGAffineTransform value that the translation will be applied to. To translate a view from its current position, you would pass the view's transform property here. To translate the view from its original position, you would pass in CGAffineTransformIdentity. Here is an example that would move the view five points to the right and ten points down.
theView.transform = CGAffineTransformTranslate(theView.transform, 5.0, 10.0);

Note: "points" or "units" generally mean pixels, but as we're moving to resolution independence and starting to support the third dimension, it's no longer correct to say it's always and forever true that one point is one pixel. But, it usually is


Matrix multiplication is cumulative, so if you translate by five pixels, then translate again by five pixels in the same direction, you get a translation of ten pixels, assuming that there were no other transformations in between the two calls.

The Rotation Transformation


The next most common translation, and the first one we're discussing that can't be done without transformations, is rotation, which is handled by the function called CGAffineTransormRotate(). This function takes two parameters, the first being the existing transformation matrix, and the second being the angle of rotation expressed in radians.

Radians? Who the heck thinks in radians? There's a convenience conversion macro in Core Graphics for Mac OS X called degreesToRadian(), but right now, it's inexplicably absent from Core Graphics for iPhone. It's an easy enough conversion, though - just add this line of code to your header file:
#define degreesToRadians(x) (M_PI * x / 180.0)

then you can rotate a view like this:
theView.transform = CGAffineTransformRotate(theView.transform, degreesToRadians(45));


The Scale Transformation


The final of the basic transformation is the scale transformation, which allows you to resize your view without touching its bounds property. To scale a view to double its original size, we use CGAffineTransformScale(), like so:
theView.transform = CGAffineTransformScale(theView.transform, 2.0, 2.0);


Note: If you are transforming the Identity Matrix, you can use the "Make" version of these functions which do not take a CGAffineTransform as a parameter, and just assume the Identity Matrix. So, for example, calling:
theView.transform = CGAffineTransformTranslate(CGAffineTransformIdentity, 2.0, 2.0);

You could call
theView.transform = CGAffineTransformMakeTranslation(2.0, 2.0);

Those two lines of code are functionally identical.


Some Things to Be Aware Of


Here are a few things that you should make note of as you start working with transformations.

The Center of the World


When you are scaling or rotating, the object will get scaled or rotated from the center of the object. In Core Animation, you can set an Anchor Point that will change that behavior, but if you are not using Core Animation and don't want to deal with layers, then in order to scale or rotate from, for example, you would have to also have to manually do translation transformations to reposition the view after the scale or rotation. Which brings another thing to mind:

Order Matters


When you apply success transformations, the order matter. Rotating and then translating will give you a different result then translating and then rotating. This can bite you if you're not careful.

Stepping Back


As I said earlier, you can always get back to the starting point by setting the view's transform to the Identity Matrix. But, what if you just want to back out one transformation. Say, you rotated, then scaled, then translated, and you just want to "untranslate" it. This can be useful if, for example, you want to do complex animations, such as having a view move one way, and then return to its original position. There is another transformation to look at called the Inverse transformation. The inverse of a transformation is the translation that will negate that translation when applied after it. You can get the inverse transformation for any CGAffineTransform by using the function CGAffineTransformInvert(), like so:
CGAffineTransform inverse = CGAffineTransformInvert(CGAffineTransformMakeTranslation(5.0, 5.0));

The value of inverse in the code above is the same as creating a translation of (-5.0, -5.0), but you don't have to know what the transformation is to get the inverse using this method.

Conclusion


Okay, we've only scratched the surface of what is a very complex part of the iPhone, but I hope this helps make the concept of "transforms" and "transformations" more approachable and will help some people make more sense of the documentation. As always, if you have questions, feel free to ping me via e-mail or IM, or send a tweet my way. I use the same username every where, which is jeff underscore lamarche, except for gmail, where I'm jeff dot lamarche.

Here's a sample Xcode project that shows how to use some transformations on views created in Interface Builder.

No comments:

Post a Comment