Transforms

When I discussed how to resize UIPickerViews last year, I touched upon the fact that a UIView's transform property is rather unusual; according to the documentation:

The origin of the transform is the value of the center property, or the layer’s anchorPoint property if it was changed. (Use the layer property to get the underlying Core Animation layer object.)

This turns out to be something of a hassle when trying to reason about transformations. Fortunately, a little math can sort things out.

Math

It’s probably worth pointing out that the affine transformations used in computer graphics aren’t arbitrary collections of numbers; they are matrices which are applied to vectors which represent points in a coordinate system. The results of these computations represent points in a new coordinate system. For instance, points specified in the coordinate system of a UIView are transformed into the coordinate system of its superview, and its superview’s superview, and so on, ultimately being brought into the coordinate system of the physical screen.

The Mac universe seems to consistently use row vectors, so, given the definition of a CGAffineTransform, a point is transformed like this:

                         [a  b  0]
[x' y' 1]  = [x  y  1] * [c  d  0]
                         [tx ty 1]

that is:

x' = x*a + y*c + tx
y' = x*b + y*d + ty

The Problem

Unfortunately, if you just grab the transform of a UIView and invoke CGPointApplyAffineTransform() to transform a point, you’re very likely to get the wrong answer. More precisely, if you were to execute:

CGPoint np0 = [aView.superview convertPoint:op fromView:aView];

and:

 CGPoint  np1 = CGPointApplyAffineTransform(op, aView.transform);

for an arbitrary point op, you would likely get two different results for np0 and np1.

This happens because the transform property is a big fat lie. More specifically, that business about the “origin of the transform [being] the value of the center property” is nonsense; the origin of any transform is the origin of the initial coordinate system. What’s really happening is that prefix and suffix transforms are being concatenated with the matrix stored in the transform property before it is applied to any points. If you want to reason closely about the transforms being done by the system, you have to take these “hidden” transforms into account.

Wrappers

When converting points from a UIView's coordinate system to that of its parent, the system effectively moves the point at the center of the bounds rectangle to the origin of the initial coordinate system, then applies the UIView's transform, then moves the point at the origin of the transformed coordinate system to the UIView's center property. (Remember that center is a point in the superview’s coordinate space.)

In code, it looks something like this:

CGAffineTransform MakeEffectiveTransform(UIView* subView)
{
	CGRect b = subView.bounds;
	CGAffineTransform t0 = CGAffineTransformMakeTranslation(-b.origin.x-b.size.width/2, -b.origin.y-b.size.height/2);
	CGAffineTransform t1 = CGAffineTransformMakeTranslation(subView.center.x, subView.center.y);
	return CGAffineTransformConcat(t0, CGAffineTransformConcat(subView.transform, t1));
}

CGPoint ConvertPointToSuperview(CGPoint p, UIView* subview)
{
	return CGPointApplyAffineTransform(p, MakeEffectiveTransform(subview));
}

Lying

This analysis lets us derive a simple function that will convert “proper” transformations (i.e. matrices that give us the right answers when multiplied with row vectors) into matrices suitable for assignment to transform properties. We just need to apply the inverses of the prefix and suffix transforms, s.t. the prefix and suffix will be canceled out when applied. Something like this would work:

CGAffineTransform MakeAssignableTransform(CGAffineTransform t, UIView* subView)
{
	CGRect b = subView.bounds;
	CGAffineTransform t0 = CGAffineTransformMakeTranslation(b.origin.x+b.size.width/2, b.origin.y+b.size.height/2);
	CGAffineTransform t1 = CGAffineTransformMakeTranslation(-subView.center.x, -subView.center.y);
	return CGAffineTransformConcat(t0, CGAffineTransformConcat(t, t1));
}

Why Should You Care?

The truth is, most of this stuff will make absolutely no difference to you most of the time. But, one day, you may find yourself trying to debug some bizarre animation problem, and digging into the underlying transforms on that day, it will be helpful to bear in mind that you can’t just run points through them and expect to have the results make any sense.

I’m still trying to decide whether I think this was a clever move on AAPL’s (NeXT’s?) part. I guess you need something like this if you want to have a distinct center property … I don’t much like it, though.

Share and Enjoy:
  • Twitter
  • Facebook
  • Digg
  • Reddit
  • HackerNews
  • del.icio.us
  • Google Bookmarks
  • Slashdot
This entry was posted in iPhone. Bookmark the permalink.

Comments are closed.