Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you translate and scale a view without the transforms from conflicting with each other?

I need to translate and scale a UIView from the center of the screen to the upper right corner.

 _____________________________
|                            |
|                            |
|         _________          |
|        |         |         |    START
|        |         |         |
|        |_________|         |
|                            |
|                            |
|____________________________|


 _____________________________
|                       __   |
|                      |__|  |
|                            |
|                            |    END
|                            |
|                            |
|                            |
|                            |
|____________________________|

I verified that my individual calculations for scale and translation were accurate. But once they are put together, they conflict with each other. The following code makes the view end up too close to the center.

CGFloat finalPadding = 10.0f;
CGFloat finalScale = 46.0f / 170.0f;
CGFloat finalX = self.view.frame.size.width 
    - self.platformProgressView.frame.size.width * finalScale 
    - finalPadding;
CGFloat finalY = finalPadding;
CGFloat deltaX = finalX - self.platformProgressView.frame.origin.x;
CGFloat deltaY = finalY - self.platformProgressView.frame.origin.y;

[UIView
    animateWithDuration:1.0
    delay:1.0
    options:UIViewAnimationOptionCurveEaseOut
    animations:^(void){
        self.platformProgressView.transform = CGAffineTransformConcat(
            CGAffineTransformMakeTranslation(deltaX, deltaY),
            CGAffineTransformMakeScale(finalScale, finalScale)
        );            
    }
    completion:^(BOOL finished) {
    }
];

Final effect:

 _____________________________
|                            |
|                            |
|                __          |
|               |__|         |
|                            |
|                            |
|                            |
|                            |
|____________________________|

Changing the order of the multiplication causes the view to veer off the right edge of the screen.

self.platformProgressView.transform = CGAffineTransformConcat(
     CGAffineTransformMakeScale(finalScale, finalScale),
    CGAffineTransformMakeTranslation(deltaX, deltaY)
);

Final effect (projected):

 _____________________________
|                            |
|                            |    __
|                            |   |__|
|                            |
|                            |
|                            |
|                            |
|                            |
|____________________________|

Applying them seperately causes an abrupt jump and the final position is even worse.

self.platformProgressView.transform = CGAffineTransformMakeTranslation(deltaX, deltaY);
self.platformProgressView.transform = CGAffineTransformMakeScale(finalScale, finalScale);

Final effect:

 _____________________________
|                            |
|                            | 
|                            |  
|                            |
|       __                   |
|      |__|                  |
|                            |
|                            |
|____________________________|
like image 494
Pwner Avatar asked Jun 18 '14 22:06

Pwner


1 Answers

Understanding transforms

The main thing to realize is that the origin for transforms is the center point of the view rectangle. This is best shown with an example.

First we translate the view. v1 is the view at it's original position, v2 is the view at its translated position. p is the padding that you desire (finalPadding in your code). c marks the center point of the view.

+--------------------------------+
|                        ^       |
|                        | p     |
|                        v       |
|              +- v2 --------+   |
|              |             |   |
|              |      c      |<->|
|              |             | p |
|              +-------------+   |
|                                |
|                                |
|    +- v1 --------+             |
|    |             |             |
|    |      c      |             |
|    |             |             |
|    +-------------+             |
|                                |
+--------------------------------+

Next we scale the view. v3 is the view at its scaled position. Note how the center point for v3 remains unchanged while the dimensions of the view around it shrink. Although the dimensions are correct, the positioning of the view and the resulting padding p' are wrong.

+--------------------------------+
|                       ^        |
|                       | p'     |
|                       |        |
|                       v        |
|                 +- v3 --+      |
|                 |   c   |<---->|
|                 +-------+  p'  |
|                                |
|                                |
|    +- v1 --------+             |
|    |             |             |
|    |      c      |             |
|    |             |             |
|    +-------------+             |
|                                |
+--------------------------------+

A fix for the delta calculations

Now that we know how scaling works, we need to fix the code where you calculate the translation deltas. Here is how to do it right:

CGRect windowFrame = self.window.frame;
CGRect viewFrame = self.platformProgressView.frame;
CGPoint finalCenter = CGPointZero;
finalCenter.x = (windowFrame.size.width
                 - (viewFrame.size.width * finalScale) / 2.0f
                 - finalPadding);
finalCenter.y = (finalPadding
                 + (viewFrame.size.height * finalScale) / 2.0f);
CGPoint viewCenter = self.platformProgressView.center;
CGFloat deltaX = finalCenter.x - viewCenter.x;
CGFloat deltaY = finalCenter.y - viewCenter.y;

Order of transforms

Finally, as you have noted yourself, the order in which transformations are concatenated with CGAffineTransformConcat matters. In your first attempt you have the sequence 1) transform + 2) scale. The result is that the scale transform - which comes later in the sequence - affects the deltas specified for the translate transform.

There are 2 solution here: Either your own second attempt, where you reverse the sequence so that it becomes 1) scale + 2) transform. Or you use the helper function which I suggested in the first revision of my answer. So this

self.platformProgressView.transform = CGAffineTransformConcat(
    CGAffineTransformMakeScale(finalScale, finalScale),
    CGAffineTransformMakeTranslation(deltaX, deltaY)
);

is equivalent to

CGAffineTransform CGAffineTransformMakeScaleTranslate(CGFloat sx, CGFloat sy, CGFloat dx, CGFloat dy)
{
  return CGAffineTransformMake(sx, 0.0f, 0.0f, sy, dx, dy);
}

self.platformProgressView.transform = CGAffineTransformMakeScaleTranslate(finalScale, finalScale, deltaX, deltaY);

A different solution: Setting the anchor point

If you're unhappy about the center point being the origin for the view's transforms you can change this by setting the anchorPoint property of the view's CALayer. The default anchor point is at 0.5/0.5, which represents the center of the view rectangle. Obviously, the anchor point is not a coordinate but a kind of multiplication factor.

Your original calculation for the translation deltas was correct if we assume that the anchor point is in the upper-left corner of the view. So if you do this

self.platformProgressView.layer.anchorPoint = CGPointMake(0.0f, 0.0f)

you can keep your original calculations.

Reference

There is probably a lot more of information material out there, but the WWDC 2011 video Understanding UIKit Rendering is something that I recently watched and that greatly helped me improve my understanding of the relationship between bounds, center, transform and frame.

Also, if you are going to change the CALayer anchorPoint property, you should probably read the property documentation in the CALayer class reference first.

like image 109
herzbube Avatar answered Nov 03 '22 00:11

herzbube