Search code examples
ipadiosuitoolbarcgaffinetransform

How to track successive CGAffineTransforms so as to arrive at a specific point on screen?


How do you execute multiple CGAffineTransform operations (in animation blocks) without keeping track of every operation executed?

The translation operation doesn't take x,y coordinates but instead values to shift by. So unless you know where you are currently translated to, say at "location 2," how do you know what values to shift by to get to "location 3?"

For example:

A UIView's frame is at (0, 0) - Position 1. I set the transform to translate to (768, 0) and rotate -90 degrees - Position 2. Some time passes and now I want to move to (768, 1024) and rotate another -90 degrees - Position 3.

How do I know what to translate by to move from Position 2 to Position 3?

In context, I'm trying to achieve an iPad view like the following:

  • a UIView that takes up the entire screen
  • a UIToolbar that takes up the top edge and is on top of the UIView
  • when the iPad rotates, the UIView stays with the device, but the toolbar will rotate so that it is always on the top edge of the screen.

I am using CGAffineTransform translate and rotate to move the toolbar. Works great, except when I rotate the iPad multiple times. The first translate/rotate will work perfect. The following transforms will be off because I don't know the correct values to shift by.

UPDATE:

It looks like if I take the current translation (tx, ty) values in the UIView.transform struct and use the difference between them and the new location, it works. However, if the view has been rotated, this does not work. The tx and ty values can be flipped because of the previous rotation. I'm not sure how to handle that.

UPDATE 2:

Doing some research, I've found that I can get the original, unrotated points from tx, ty by getting the abs value of the points and possibly swapping x and y if the UIView is perpendicular. Now I am stuck figuring out how to correctly apply the next set of transforms in the right order. It seems no matter how I concat them, the UIView ends up in the wrong place. It just seems like this is all too complicated and there must be an easier way.


Solution

  • The answer is, apparently, you don't track the transforms.

    So the way to rotate the toolbar around the screen is by not concatenating a rotate and translate transform. Instead, create a rotate transform and set the frame in the animation block. Further, based on the new UIInterfaceOrientation, set the degrees to rotate based on the compass values of 0, -90, -180, -270. Also, set the frame size base on the same locations.

    So:

    CGPoint portrait = CGPointMake(0, 0);
    CGPoint landscapeLeft = CGPointMake(768 - 44, 0);
    CGPoint landscapeRight = CGPointMake(0, 0);
    CGPoint upsideDown = CGPointMake(0, 1024 - 44);
    
    CGSize portraitSize = CGSizeMake(768, 44);
    CGSize landscapeLeftSize = CGSizeMake(44, 1024);
    CGSize landscapeRightSize = CGSizeMake(44, 1024);
    CGSize upsideDownSize = CGSizeMake(768, 44);
    
    CGFloat rotation;
    CGRect newLocation;
    switch(orientation) {
        case UIDeviceOrientationPortrait:
            NSLog(@"Changing to Portrait");
            newLocation.origin = portrait;
            newLocation.size = portraitSize;
            rotation = 0.0;
            break;
    
        case UIDeviceOrientationLandscapeRight:
            NSLog(@"Changing to Landscape Right");
            newLocation.origin = landscapeRight;
            newLocation.size = landscapeRightSize;
            rotation = -90.0;
            break;
    
        case UIDeviceOrientationLandscapeLeft:
            NSLog(@"Changing to Landscape Left");
            newLocation.origin = landscapeLeft;
            newLocation.size = landscapeLeftSize;
            rotation = -270.0;
            break;
    
        case UIDeviceOrientationPortraitUpsideDown:
            NSLog(@"Changing to Upside Down");
            newLocation.origin = upsideDown;
            newLocation.size = upsideDownSize;
            rotation = -180.0;
            break;
    
        default:
            NSLog(@"Unknown orientation: %d", orientation);
            newLocation.origin = portrait;
            newLocation.size = portraitSize;
            rotation = 0.0;
            break; 
    }
    
    CGRect frame = newLocation;
    CGAffineTransform transform = CGAffineTransformMakeRotation(DEGREES_TO_RADIANS(rotation));
    
    if(lastOrientation) {
        [UIView beginAnimations:nil context:NULL];
        [UIView setAnimationDuration:.3];
        [UIView setAnimationCurve:UIViewAnimationCurveEaseIn];
        [UIView setAnimationBeginsFromCurrentState:YES];
    }
    
    toolbar.transform = transform;
    toolbar.frame = frame;
    
    // Commit the changes
    if(lastOrientation) {
        [UIView commitAnimations];
    }
    
    lastOrientation = orientation;
    

    This works beautifully. However, an unexpected problem is that UI elements that iOS shows on your behalf are not oriented correctly. I.e., modal windows and popovers all keep the same orientation as the underlying UIView. That problem renders this whole thing moot.