Rotate using a transform, then change frame origin, and view expands? - iphone

This is quite the iPhone quandry. I am working on a library, but have narrowed down my problem to very simple code. What this code does is create a 50x50 view, applies a rotation transform of a few degrees, then shifts the frame down a few times. The result is the 50x50 view is now much larger looking.
Here's the code:
// a simple 50x50 view
UIView *redThing = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 50, 50)];
redThing.backgroundColor = [UIColor redColor];
[self.view addSubview:redThing];
// rotate a small amount (as long as it's not 90 or 180, etc.)
redThing.transform = CGAffineTransformRotate(redThing.transform, 0.1234);
// move the view down 2 pixels
CGRect newFrame = CGRectMake(redThing.frame.origin.x, redThing.frame.origin.y + 2, redThing.frame.size.width, redThing.frame.size.height);
redThing.frame = newFrame;
// move the view down another 2 pixels
newFrame = CGRectMake(redThing.frame.origin.x, redThing.frame.origin.y + 2, redThing.frame.size.width, redThing.frame.size.height);
redThing.frame = newFrame;
// move the view down another 2 pixels
newFrame = CGRectMake(redThing.frame.origin.x, redThing.frame.origin.y + 2, redThing.frame.size.width, redThing.frame.size.height);
redThing.frame = newFrame;
// move the view down another 2 pixels
newFrame = CGRectMake(redThing.frame.origin.x, redThing.frame.origin.y + 2, redThing.frame.size.width, redThing.frame.size.height);
redThing.frame = newFrame;
So, what the heck is going on? Now, if I move the view by applying a translation transform, it works just fine. But that's not what I want to do and this should work anyway.
Any ideas?

From the UIView documentation:
If the transform property is also set, use the bounds and center properties instead; otherwise, animating changes to the frame property does not correctly reflect the actual location of the view.
Warning: If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.
In other words, I would be wary of the frame property when a transform is set.

Related

Adding sublayer to view.layer changes the frame position?

I'm adding a CALayer as a sublayer to a UIView's layer property as follows:
_graphicLayer = [[GraphicLayer alloc] init];
self.bounds = _graphicLayer.bounds;
_graphicLayer.position = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
[self.layer addSublayer:_graphicLayer];
As you can see, I change the position of _graphicLayer to account for the centered anchorPoint. I'm noticing after I add this sublayer, that it's changing the views frame to (-self.bounds.width / 2, -self.bounds.height / 2). Why is this happening? If I change the position of _graphicLayer, I thought that was only relative to its parent view. Why would it affect the views frame property? (and I don't want to have to adjust the anchorPoint of either the views layer property or _graphicLayer).
You're currently doing
self.bounds = _graphicLayer.bounds;
which would change the view's (self's) frame as it would change the width and height. Maybe you meant to do :
_graphicLayer.bounds = self.bounds;
See here about how bounds affect the frame:Link

Setting a view's bounds changes the coordinates of the frame, WHY?

Why setting the bounds property of a UIView messes up it's frame's coordinates?
For example:
self.view.frame = CGRectMake(10, 10, 200, 200);
CGRect b = CGRectMake(0, 0, 399, 323);
self.view.bounds = b;
I would expect the view's frame to be (10, 10, 399, 323) but instead the coordinates get some weird values like (-89.5 -51.5; 399 323).
Thanks!
From the UIView class reference:
Changing the bounds size grows or shrinks the view relative to its center point.
So it is keeping the center point in the same place, which means the origin of the frame has to adjust.
If you want to resize the view but keep the origin in the same place, set the frame instead of the bounds.

Antialiasing edges of UIView after transformation using CALayer's transform

I have a UIView object that rotates using CALayer's transform:
// Create uiview object.
UIImageView *block = [[UIImageView alloc] initWithFrame....]
// Apply rotation.
CATransform3D basicTrans = CATransform3DIdentity;
basicTrans.m34 = 1.0/-distance;
blockImage.layer.transform = CATransform3DRotate(basicTrans, rangle, 1.0f, 0.0f, 0.0f);
After rotating the edges of the object are not antialiasing. I need to antialias them.
Help me, please. How can it be done?
One way to do this is by placing the image inside another view that's 5 pixels bigger. The bigger view should have a transparent rasterized border that will smooth the edges of the UIImageView:
view.layer.borderWidth = 3;
view.layer.borderColor = [UIColor clearColor].CGColor;
view.layer.shouldRasterize = YES;
view.layer.rasterizationScale = [[UIScreen mainScreen] scale];
Then, place your UIImageView inside this parent view and center it (With 2.5 pixels around each edge).
Finally, rotate the parent view instead of the image view.
It works very well - you can also encapsulate the whole thing in class that creates the hierarchy.
Simply add this key-value pair to your Info.plist: UIViewEdgeAntialiasing set to YES.
check allowsEdgeAntialiasing property of CALayer.
block.layer.allowsEdgeAntialiasing = YES; // iOS7 and above.
I had a similar issue when rotating around the z-axis. Setting shouldRasterize = YES prevented the jagged edges however it came at a performance cost. In my case I was re-using the views (and its layers) and keeping the shouldRasterize = YES was slowing things down.
The solution was, to turn off rasterization right after I didn't need it anymore. However since animation runs on another thread, there was no way of knowing when the animation was complete...until I found out about an extremely useful CATransaction method. This is an actual code that I used and it should illustrate its use:
// Create a key frame animation
CAKeyframeAnimation *wiggle = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
NSInteger frequency = 5; // Higher value for faster vibration
NSInteger amplitude = 25; // Higher value for lower amplitude
// Create the values it will pass through
NSMutableArray *valuesArray = [[NSMutableArray alloc] init];
NSInteger direction = 1;
[valuesArray addObject:#0.0];
for (NSInteger i = frequency; i > 0; i--, direction *= -1) {
[valuesArray addObject:#((direction * M_PI_4 * (CGFloat)i / (CGFloat)amplitude))];
}
[valuesArray addObject:#0.0];
[wiggle setValues:valuesArray];
// Set the duration
[wiggle setAdditive:YES];
[wiggle setValueFunction:[CAValueFunction functionWithName:kCAValueFunctionRotateZ]];
[wiggle setDuration:0.6];
// Turn on rasterization to prevent jagged edges (anti-aliasing issues)
viewToRotate.layer.shouldRasterize = YES;
// ************ Important step **************
// Very usefull method. Block returns after ALL animations have completed.
[CATransaction setCompletionBlock:^{
viewToRotate.layer.shouldRasterize = NO;
}];
// Animate the layer
[viewToRotate.layer addAnimation:wiggle forKey:#"wiggleAnimation"];
worked like a charm for me.
I have not tried using this with implicit animations (i.e. animations that happen due to value change in animatable property for a non-view associated layer), however I would expect it to work as long as the CATransaction method is called before the property change, just as a guarantee the block is given to CATransaction before an animation starts.

bounds and frames: how do I display part of an UIImage

My goal is simple; I want to create a program that displays an UIImage, and when swiped from bottom to top, displays another UIImage. The images here could be a happy face/sad face. The sad face should be the starting point, the happy face the end point. When swiping your finger the part below the finger should be showing the happy face.
So far I tried solving this with the frame and bounds properties of the UIImageview I used for the happy face image.
What this piece of code does is wrong, because the transition starts in the center of the screen and not the bottom. Notice that the origin of both frame and bounds are at 0,0...
I have read numerous pages about frames and bounds, but I don't get it. Any help is appreciated!
The loadimages is called only once.
- (void)loadImages {
sadface = [UIImage imageNamed:#"face-sad.jpg"];
happyface = [UIImage imageNamed:#"face-happy.jpg"];
UIImageView *face1view = [[UIImageView alloc]init];
face1view.image = sadface;
[self.view addSubview:face1view];
CGRect frame;
CGRect contentRect = self.view.frame;
frame = CGRectMake(0, 0, contentRect.size.width, contentRect.size.height);
face1view.frame = frame;
face2view = [[UIImageView alloc]init];
face2view.layer.masksToBounds = YES;
face2view.contentMode = UIViewContentModeScaleAspectFill;
face2view.image = happyface;
[self.view addSubview:face2view];
frame = CGRectMake(startpoint.x, 0, contentRect.size.width, contentRect.size.height);
face2view.frame = frame;
face2view.clipsToBounds = YES;
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
CGPoint movepoint = [[touches anyObject] locationInView: self.view];
NSLog(#"movepoint: %f %f", movepoint.x, movepoint.y);
face2view.bounds = CGRectMake(0, 0, 320, 480 - movepoint.y);
}
The UIImages and UIImageViews are properly disposed of in the dealloc function.
Indeed, you seem to be confused about frames and bounds. In fact, they are easy. Always remember that any view has its own coordinate system. The frame, center and transform properties are expressed in superview's coordinates, while the bounds is expressed in the view's own coordinate system. If a view doesn't have a superview (not installed into a view hierarchy yet), it still has a frame. In iOS the frame property is calculated from the view's bounds, center and transform. You may ask what the hell frame and center mean when there's no superview. They are used when you add the view to another view, allowing to position the view before it's actually visible.
The most common example when a view's bounds differ from its frame is when it is not in the upper left corner of its superview: its bounds.origin may be CGPointZero, while its frame.origin is not. Another classic example is UIScrollView, which frequently modifies its bounds.origin to make subviews scroll (in fact, modifying the origin of the coordinate system automatically moves every subview without affecting their frames), while its own frame is constant.
Back to your code. First of all, when you already have images to display in image views, it makes sense to init the views with their images:
UIImageView *face1view = [[UIImageView alloc] initWithImage: sadface];
That helps the image view to immediately size itself properly. It is not recommended to init views with -init because that might skip some important code in their designated initializer, -initWithFrame:.
Since you add face1view to self.view, you should really use its bounds rather than its frame:
face1view.frame = self.view.bounds;
Same goes for the happier face. Then in -touchesMoved:… you should either change face2view's frame to move it inside self.view or (if self.view does not contain any other subviews besides faces) modify self.view's bounds to move both faces inside it together. Instead, you do something weird like vertically stretching the happy face inside face2view. If you want the happy face to slide from the bottom of self.view, you should initially set its frame like this (not visible initially):
face2view.frame = CGRectOffset(face2view.frame, 0, CGRectGetHeight(self.view.bounds));
If you choose to swap faces by changing image views' frames (contrasted with changing self.view's bounds), I guess you might want to change both the views' frame origins, so that the sad face slides up out and the happy face slides up in. Alternatively, if you want the happy face to cover the sad one:
face2view.frame = face1view.frame;
Your problem seems to have something to do with the face2view.bounds in touchesMoved.
You are setting the bounds of this view to the rect, x:0, y:0, width:320, height:480 - y
x = 0 == left on the x axis
y = 0 == top on the y axis
So you are putting this image frame at the upper left corner, and making it fill the whole view. That's not what you want. The image simply becomes centered in this imageView.

My UIViewController is "lying" about its center property

I'm asking my ViewController for it's view.center property and drawing a new UIView that centers itself around this "center"... I am getting (160, 250) as a response. but when the new UIView draws it's below center... So I'm wondering who's giving me this info and what it relates to? This is clearly where the center of the view is in relation to the WINDOW if you take into account the 20px height of the status bar. which would push the center of the view down 10px. but when drawing myView it appears to draw in relation to the ViewController.view and not the Window so it appears 20px below center...
I would expect the ViewController to give me its center (160, 230) so I could draw in its center...do I need to manually account for the status bar and subtract 20 from height every time? or is there some view space translation i'm overlooking? From ViewController.m:
- (void)setUpMyView {
// Create my view
MyView *aView = [[MyView alloc] init];
self.myView = aView;
[aView release];
myView.center = self.view.center;
NSLog(#"CenterX: %f, Y: %f", self.view.center.x, self.view.center.y);
CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_2);
myView.transform = transform;
[self.view addSubview:myView];
}
console: CenterX: 160.000000, Y: 250.000000
It's not "lying" actually, it's giving you an answer in a different coordinate system.
When you get or set a view's center, it is calculated relative to its parent's coordinate system. self.view's parent is the application's window, hence its center is (160, 250) as you surmised. But myView's parent is self.view, which has its own local coordinate system. In this case, that coordinate system is 20 pixels lower than that of the window.
What you want is to find the center of self.myView in its own coordinate system. There are two ways you could do it.
1) You could calculate it based on the bounds property, which is a CGRect that specifies the boundaries of a view in its own coordinate system:
myView.center = CGPointMake(self.view.bounds.size.width / 2,
self.view.bounds.size.height / 2);
2) Or you could use UIView's convertPoint:fromView: method to convert the coordinate from the window's coordinate system into that of self.view:
// Pass nil as the source view to convert from the window's coordinate system
myView.center = [self.view convertPoint:self.view.center fromView:nil];
Maybe add it to the Subview before you ask its center and size.
MyView *aView = [[MyView alloc] init];
self.myView = aView;
[aView release];
[self.view addSubview:myView]; // add it to the subview first before asking center.
myView.center = self.view.center;
NSLog(#"CenterX: %f, Y: %f", self.view.center.x, self.view.center.y);
CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_2);
myView.transform = transform;
I think I know the answer. You View center is correct!
You forgot to add the iPhone status bar is 20px down from the top of the screen and hence included in your view center. Bingo.