New center point for transformed UIView with new frame - ios

I been working on an iOS app which should display indoor blueprints. You should be able to switch between floors and each floor image is controlled by gesture recognisers to handle pan, rotate and scale.
I have been using this example for the gesture recognisers: https://github.com/GreenvilleCocoa/UIGestures/blob/master/UIGestures/RPSimultaneousViewController.m
So now to the problem. Whenever the user switch floor I want to keep the transformation of the image as well as the corresponding center lat/lng. However, the new image can have another rotation offset and aspect ratio.
I have been able to update the new frame of the image with the new size and update the transform with the new rotation offset and verified it. It is when I try to calculate the new center point I can not get it to work. The following code is how I currently do it and it works as long as the view is not rotated:
-(void)changeFromFloor:(int)oldFloorNr toFloor:(int)newFloorNr
{
CGPoint centerPoint = CGPointMake(self.frame.size.width/2, self.frame.size.height/2);
// This is the old non transformed center point.
CGPoint oldCenterOnImage = [self.layer convertPoint:centerPoint toLayer:self.mapOverlayView.layer]; // Actual non transformed point
// This point is verified to be the corresponding non transformed center point
CGPoint newCenterOnImage = [self calculateNewCenterFor:oldCenterOnImage fromFloor:oldFloorNr toFloor:newFloorNr];
// Change image, sets a new image and change the fram of mapOverlayView
[self changeImageFromFloor:oldFloorNr toFloor:newFloorNr]
// Adjust transformed rotation on map if new map have different rotation
[self adjustRotationFromFloorNr:oldFloorNr toFloorNr:newFloorNr];
CGPoint centerOfMapOverlay = CGPointMake((self.mapOverlayView.frame.size.width / 2), (self.mapOverlayView.frame.size.height / 2));
CGPoint newCenterOnImageTransformed = CGPointApplyAffineTransform(newCenterOnImage, self.mapOverlayView.transform);
CGFloat newCenterX = centerPoint.x + centerOfMapOverlay.x - newCenterOnImageTransformed.x;
CGFloat newCenterY = centerPoint.y + centerOfMapOverlay.y - newCenterOnImageTransformed.y;
// This only works without any rotation
self.mapOverlayView.center = CGPointMake(newCenterX, newCenterY);
}
Any idea where I go wrong? I have been working with this problem some days now and I can not seem to figure it out.
Please let me know if I need to add something or if something is unclear.
Thanks!
Code added after help was given:
CGPoint centerOfMapOverlay = CGPointMake(
(self.mapOverlayView.bounds.size.width / 2,
(self.mapOverlayView.bounds.size.height / 2)
);
centerOfMapOverlay = CGPointApplyAffineTransform(
centerOfMapOverlay,
self.mapOverlayView.transform
);

If you change the transform on it's view then the frame property becomes undefined. You should instead use the center property to change the view's position and bounds.size to change the view's size.

Related

Rotating a view using another view's center as the anchor point

I have two image views. The first is the blueish arrow, and the second is the white circle, with a black dot drawn to represent the center of the circle.
I'm trying to rotate the arrow so it's anchor point is the black dot in the picture like this
Right now I'm setting the anchor point of the arrow's layer to a point calculated like this
CGFloat y = _userImageViewContainer.center.y - CGRectGetMinY(_directionArrowView.frame);
CGFloat x = _userImageViewContainer.center.x - CGRectGetMinX(_directionArrowView.frame);
CGFloat yOff = y / CGRectGetHeight(_directionArrowView.frame);
CGFloat xOff = x / CGRectGetWidth(_directionArrowView.frame);
_directionArrowView.center = _userImageViewContainer.center;
CGPoint anchor = CGPointMake(xOff, yOff);
NSLog(#"anchor: %#", NSStringFromCGPoint(anchor));
_directionArrowView.layer.anchorPoint = anchor;
Since the anchor point is set as a percentage of the view, i.e. the coords for the center are (.5, .5), I'm calculating the percentage of the height in arrow's frame where the black dot falls. But my math, even after working out by hand, keeps resulting in .5, which isn't right because it's further than half way down when the arrow is in the original position (vertical, with the point up).
I'm rotating based on the user's compass heading
CLHeading *heading = [notif object];
// update direction of arrow
CGFloat degrees = [self p_calculateAngleBetween:[PULAccount currentUser].location.coordinate
and:_user.location.coordinate];
_directionArrowView.transform = CGAffineTransformMakeRotation((degrees - heading.trueHeading) * M_PI / 180);
The rotation is correct, it's just the anchor point that's not working right. Any ideas of how to accomplish this?
I've always found the anchor point stuff flaky, especially with rotation. I'd try something like this.
CGPoint convertedCenter = [_directionArrowView convertPoint:_userImageViewContainer.center fromView:_userImageViewContainer ];
CGSize offset = CGSizeMake(_directionArrowView.center.x - convertedCenter.x, _directionArrowView.center.y - convertedCenter.y);
// I may have that backwards, try the one below if it offsets the rotation in the wrong direction..
// CGSize offset = CGSizeMake(convertedCenter.x -_directionArrowView.center.x , convertedCenter.y - _directionArrowView.center.y);
CGFloat rotation = 0; //get your angle (radians)
CGAffineTransform tr = CGAffineTransformMakeTranslation(-offset.width, -offset.height);
tr = CGAffineTransformConcat(tr, CGAffineTransformMakeRotation(rotation) );
tr = CGAffineTransformConcat(tr, CGAffineTransformMakeTranslation(offset.width, offset.height) );
[_directionArrowView setTransform:tr];
NB. the transform property on UIView is animatable, so you could put that last line there in an animation block if desired..
Maybe better use much easier solution - make arrow image size bigger, and square. So the black point will be in center of image.
Please compare attached images and you understand what I'm talking about
New image with black dot in center
Old image with shifted dot
Now you can easy use standard anchor point (0.5, 0.5) to rotate edited image

Moving a Rotated UIButton

I have a problem. I'm working on making a game. As part of my game I need images to be rotated and then moved in the direction of the rotated angle inside a game loop (using an NSTimer). In essence I'm trying to create the effect of launching a projectile. The code works fine when moving in perpendicular directions such as 0, 90, 180, 270, and 360 degrees, but any other angle and the image starts to glitch out. The object on the screen maintains its correct bounds and contents, but the actual displayed image disappears. Does anybody know what the problem is or someway I could get around it? If needed, I can make and post a video of my problem so you can see what I'm talking about.
Here is a sample of the code I'm using. The "background" variable is just a UIImageView:
angle = 60;
background.transform = CGAffineTransformRotate(object.transform, angle*M_PI/180); //converts degrees to radians and rotates the image
background.frame = CGRectMake( background.frame.origin.x + cos(angle*m_PI/180)*32; background.frame.origin.y -sin(angle*M_PI/180)*32, background.frame.size.width, background.frame.size.height); //moves the image in the direction of the angle
For starters, there is a semicolon after the x origin in your CGRect instead of a comma. Was that just a typo?
The UIView documentation for frame states:
Warning: If the transform property is not the identity transform, the
value of this property is undefined and therefore should be ignored.
Changes to this property can be animated. However, if the transform
property contains a non-identity transform, the value of the frame
property is undefined and should not be modified. In that case, you
can reposition the view using the center property and adjust the size
using the bounds property instead.
So there you have it, you should not be trying to change the frame when setting a custom transform. You are only trying to adjust the position of the view anyway so just modify your code to adjust center instead of the origin coordinates.
To change the size, you can use the bounds.
CGRect bounds = myView.bounds;
bounds.size.width = whatever;
bounds.size.height = whatever;
myView.bounds = bounds;

Why am I unable to detect when my UIView I push off screen using UIKit Dynamics is no longer visible?

I'm using UIKit Dynamics to push a UIView off screen, similar to how Tweetbot performs it in their image overlay.
I use a UIPanGestureRecognizer, and when they end the gesture, if they exceed the velocity threshold it goes offscreen.
[self.animator removeBehavior:self.panAttachmentBehavior];
CGPoint velocity = [panGestureRecognizer velocityInView:self.view];
if (fabs(velocity.y) > 100) {
self.pushBehavior = [[UIPushBehavior alloc] initWithItems:#[self.scrollView] mode:UIPushBehaviorModeInstantaneous];
[self.pushBehavior setTargetOffsetFromCenter:centerOffset forItem:self.scrollView];
self.pushBehavior.active = YES;
self.pushBehavior.action = ^{
CGPoint lowestPoint = CGPointMake(CGRectGetMinX(self.imageView.bounds), CGRectGetMaxY(self.imageView.bounds));
CGPoint convertedPoint = [self.imageView convertPoint:lowestPoint toView:self.view];
if (!CGRectIntersectsRect(self.view.bounds, self.imageView.frame)) {
NSLog(#"outside");
}
};
CGFloat area = CGRectGetWidth(self.scrollView.bounds) * CGRectGetHeight(self.scrollView.bounds);
CGFloat UIKitNewtonScaling = 5000000.0;
CGFloat scaling = area / UIKitNewtonScaling;
CGVector pushDirection = CGVectorMake(velocity.x * scaling, velocity.y * scaling);
self.pushBehavior.pushDirection = pushDirection;
[self.animator addBehavior:self.pushBehavior];
}
I'm having an immense amount of trouble detecting when my view actually completely disappears from the screen.
My view is setup rather simply. It's a UIScrollView with a UIImageView within it. Both are just within a UIViewController. I move the UIScrollView with the pan gesture, but want to detect when the image view is off screen.
In the action block I can monitor the view as it moves, and I've tried two methods:
1. Each time the action block is called, find the lowest point in y for the image view. Convert that to the view controller's reference point, and I was just trying to see when the y value of the converted point was less than 0 (negative) for when I "threw" the view upward. (This means the lowest point in the view has crossed into negative y values for the view controller's reference point, which is above the visible area of the view controller.)
This worked okay, except the x value I gave to lowestPoint really messes everything up. If I choose the minimum X, that is the furthest to the left, it will only tell me when the bottom left corner of the UIView has gone off screen. Often times as the view can be rotating depending on where the user pushes from, the bottom right may go off screen after the left, making it detect it too early. If I choose the middle X, it will only tell me when the middle bottom has gone off, etc. I can't seem to figure out how to tell it "just get me the absolute lowest y value.
2. I tried CGRectIntersectsRect as shown in the code above, and it never says it's outside, even seconds after it went shooting outside of any visible area.
What am I doing wrong? How should I be detecting it no longer being visible?
If you take a look on UIDynamicItem protocol properties, you can see they are center, bounds and transform. So UIDynamicAnimator actually modifies only these three properties. I'm not really sure what happens with the frame during the Dynamics animations, but from my experience I can tell it's value inside the action block is not always reliable. Maybe it's because the frame is actually being calculated by CALayer based on center, transform and bounds, as described in this excellent blog post.
But you for sure can make use of center and bounds in the action block. The following code worked for me in a case similar to yours:
CGPoint parentCenter = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds));
self.pushBehavior.action = ^{
CGFloat dx = self.imageView.center.x - parentCenter.x;
CGFloat dy = self.imageView.center.y - parentCenter.y;
CGFloat distance = sqrtf(dx * dx + dy * dy);
if(distance > MIN(parentCenter.y + CGRectGetHeight(self.imageView.bounds), parentCenter.x + CGRectGetWidth(self.imageView.bounds))) {
NSLog(#"Off screen!");
}
};

How to Rotate CAShapeLayer containing UIBezierPath? [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Rotate CGPath without changing its position
I searched and tested a variety of code for a couple of hours and I can't get this to work.
I am adding an arbitrary UIBezierPath at a random location to a CAShapeLayer which gets added to a view. I need to rotate the path so that I can handle device rotations. I can rotate the layer instead of the path. I just need the result to be rotated.
I already have methods to handle transforming the bezier path by scaling and translation. It works great, but now I need to simply rotate 90 degrees left or right.
Any recommendations on how to do this?
Basic code:
UIBezierPath *path = <create arbitrary path>
CAShapeLayer *layer = [CAShapeLayer layer];
[self addPathToLayer:layer
fromPath:path];
// I could get the center of the box but where is the box center for the view it is in?
// CGRect box = CGPathGetPathBoundingBox(path.CGPath);
// layer.anchorPoint = ? How to find the center of the box for the anchor point?
// Rotating here appears to rotate around 0,0 of the view
layer.transform = CATransform3DMakeRotation(DegreesToRadians(-90), 0.0, 0.0, 1.0);
I see the following post:
BezierPath Rotation in a UIView
I suppose I could rotate as-is and then translate the path back into place. I just need to figure out what the translation values would be.
I should also state that what I am seeing after I try to rotate is that the image moves off-screen somewhere. I tried rotating 25 degrees to see movement and it pivots around the view's origin of 0,0 so that if I rotate 90 degrees the image is off-screen. I am running these test WITHOUT rotating the device - just to see how rotation works.
UPDATE #1 - 12/4/2012: For some bizarre reason if I set the position to a value I found empirically it moves the rotated bezier path into the correct position after rotation:
layer.position = CGPointMake(280, 60);
This values are a guess from starting/stopping the app and making adjustments. I have no idea why I need to adjust the position on rotation. The anchor point should be in the center of the layer. However, I did find that both the frame and position of a CAShapeLayer are all ZERO even though the path is set, and also the fact that the path is in the correct position within the view. The 280, 60 position shifts the path into what would be the center of the path bounding box when a rotation of +90 is made. If I change the rotation value I need to adjust the position. I should not have to do this manually adjustment.
I think a last resort is to somehow convert the bezier path to an image and then add it. I found that if I set the layer content to an image, then rotate, it rotates about its center point with no positional adjustment needed. Not so with setting the path.
UPDATE #2 12/4/2012 - I tried setting the frame and with fiddling I get it to center as follows:
CGRect box = CGPathGetPathBoundingBox(path.CGPath);
CGRect rect = CGRectMake(0, 0, box.origin.x + (3.5 * box.size.width), box.origin.y + (3.5 * box.size.height));
layer.frame = rect;
layer.transform = CATransform3DMakeRotation(DegreesToRadians(90), 0.0, 0.0, 1.0);
Why multiply by 3.5? I have no clue. I found that adding the box origin with about 3.5 times the size of the box shifts the rotated CAShapeLayer path to about where it should be.
There must be a better way to do this. This is a better solution than my previous post since the frame size does not depend on the rotation angle. I just don't know why the frame needs to be set to the value I am setting it to. I THOUGHT it should be
CGRectMake(0, 0, box.origin.x + (box.size.width / 2), box.origin.y + (box.size.height / 2));
However, it shifts the image to the left too much.
Another clue I found is that if I set the frame of [self view].frame (the frame of the entire parent view, which is the screen of the iPhone), then rotate, the rotation point is the center of the screen, an the path/image orbits around this center point. This is why I tried shifting the frame to what the center of the path should be so that it orbits around the box center.
UPDATE #3 12/4/2012 - I tried to render the layer as an image. However, it appears that just setting the path of a layer does not make it an "image" in the layer since it is empty
CGRect box = CGPathGetPathBoundingBox(path.CGPath);
layer.frame = box;
UIImage *image = [ImageHelper imageFromLayer:layer]; // ImageHelper library I created
CAShapeLayer *newLayer = [CAShapeLayer layer];
newLayer.frame = CGRectMake(box.origin.x, box.origin.y, image.size.width, image.size.height);
newLayer.contents = (id) image.CGImage;
It appears that rotating the layer with its path set is no different than simply rotating the bezier path itself. I will go back to rotating the bezier path and see if I can fiddle with the position elements or something. There's got to be a solution to this.
Goal: Rotate a UIBezierPath around its center point within the view it was originally created in.
UPDATE #4 12/4/2012 - I ran a series of tests measuring the values needed for translation in order to place a UIBezierPath in its previous center location.
CGAffineTransform rotate = CGAffineTransformMakeRotation(DegreesToRadians(-15));
[path applyTransform:rotate];
// CGAffineTransform translate = CGAffineTransformMakeTranslation(-110, 70); // -45
CGAffineTransform translate = CGAffineTransformMakeTranslation(-52, -58); // -15
[path applyTransform:translate];
However, the ratios of x/y translations do not correspond so I cannot extrapolate what translation is required based on the angle. It appears that 'CGAffineTransformMakeRotation' uses some arbitrary anchor put to make the rotation, which at the moment appears to be maybe (viewWidth / 2, 0). I am making this much harder than it needs to be. There's something I am missing to make a simple rotation so that the center point is maintained. I just need to "spin" the path 90 degrees left or right.
UPDATE #5 12/4/2012 - After running additional tests it appears that the anchor point for rotating a UIBezierPath is the origin from where all of the points were drawn. In this case the origin is 0,0 and all of the points are relative to that point. Therefore, it a rotation is applied, the rotation is occurring around the origin, and is why the path shifts up-right on -90 and up-left on 90. I need to somehow set the anchor point for the rotation to the center so it "spins" around the center, rather than the original origin point. 12 hours spent on this one issue.
After some detailed analysis and graphing the bounding box on paper I found my assertion that the origin of 0,0 is correct.
A solution to this problem is to translate the path (the underlying matrix) to the origin, with the center of the bounding box at origin, rotate, then translate the path back to its original location.
Here's how to rotate a UIBezierPath 90 degrees:
CGAffineTransform translate = CGAffineTransformMakeTranslation(-1 * (box.origin.x + (box.size.width / 2)), -1 * (box.origin.y + (box.size.height / 2)));
[path applyTransform:translate];
CGAffineTransform rotate = CGAffineTransformMakeRotation(DegreesToRadians(90));
[path applyTransform:rotate];
translate = CGAffineTransformMakeTranslation((box.origin.x + (box.size.width / 2)), (box.origin.y + (box.size.height / 2)));
[path applyTransform:translate];
Plug in -90 degrees to rotate in the other direction.
This formula can be used when rotating the device from portrait to landscape and vice/versa.
I still don't think this is the ideal solution but the result is what I need for now.
If anyone has a better solution for this please post.
UPDATE 12/7/2012 - I found what I think is the best solution, and very simple as I though it would be. Rather than using rotate, translate, and scale methods on the bezier path, I instead extract the array of points as CGPoint objects, and scale/translate them as needed based on the view size as well as the orientation. I then create a new bezier path and set the layer to this path.
The result is perfect scaling, translation, rotation.

rotate UIImageView around an arbitrary point

I have a UIImageView that I rotate around its center:
imageHorizon.layer.anchorPoint = CGPointMake(0.5, 0.5);
imageHorizon.transform = CGAffineTransformRotate(imageHorizon.transform, angleToRotate*(CGFloat)(M_PI/180));
Sometimes I also move this image to the left or right and then rotate again. I would like to keep the rotation center all the time on the same point (which is actually the center of the super view). How can I do that ?
cheers,
self.imgView.layer.anchorPoint = CGPointMake(0.0,1.0);
self.imgView.layer.position = CGPointMake(100,200.0);
CGAffineTransform cgaRotateHr = CGAffineTransformMakeRotation(-(3.141/4));
[self.imgView setTransform:cgaRotateHr];
This is an older question, but the other solutions did not work well for me, so I came up with another solution:
Rotating an image is essentially just a normal rotation with a translation applied, ensuring that the point you want to rotate around is still in the same spot after the rotation. To do this, calculate the position's CGPoint in your image before the rotation, get the position after the rotation, and apply the difference as a translation on the image, "snapping" it into the right position. Here is the code that I've been using:
Keep in mind that the translation should be applied via CGAffineTransform, not moving the .center, because the translation will need to be relative to the rotation, and CGAffineTransformTranslate() takes care of that.
// Note: self is the superview of _imageView
// Get the rotation point
CGPoint rotationPointInSelf = self.center; // or whatever point you want to rotate around
CGPoint rotationPointInImage = [_imageView convertPoint:rotationPointInSelf fromView:self];
// Rotate the image
_imageView.transform = CGAffineTransformRotate(_imageView.transform, angle);
// Get the new location of the rotation point
CGPoint newRotationPointInImage = [_imageView convertPoint:rotationPointInSelf fromView:self];
// Calculate the difference between the point's old position and its new one
CGPoint translation = CGPointMake(rotationPointInImage.x - newRotationPointInImage.x, rotationPointInImage.y - newRotationPointInImage.y);
// Move the image so the point is back in it's old location
_imageView.transform = CGAffineTransformTranslate(_imageView.transform, -translation.x, -translation.y);
You can make the image a subview of another view and then rotate the superview to get that effect. Another approach is to set the anchorPoint property as described in the docs.
I'm using this code to rotate around the point (0,0).
Maybe it help you figure out how to active what you want.
float width = self.view.frame.size.width;
float height = self.view.frame.size.height;
CGRect frame_smallView = CGRectMake(-width, -height, width, height);
UIView *smallView = [[UIView alloc] initWithFrame:frame_smallView];
smallView.backgroundColor = darkGrayColor;
// Select x and y between 0.0-1.0.
// The default is (0.5f,0.5f) that is the center of the layer
// (1.0f,1.0f) is the right bottom corner
smallView.layer.anchorPoint = CGPointMake(1.0f, 1.0f);
// Rotate around this point
smallView.layer.position = CGPointMake(0, 0);
[self.view insertSubview:smallView belowSubview:self.navBar];
[UIView animateWithDuration:1
animations:^{
smallView.transform = CGAffineTransformMakeRotation(M_PI);
}
completion:^(BOOL finished){
[self.navigationController popViewControllerAnimated:NO];
}];

Resources