Scale UIView with the top center as the anchor point? - objective-c

I'm scaling a UIView with CGAffineTransformMakeScale but I want to keep it anchored to it's current top center point as it's scaled.
I've looked into setting view.layer.anchorPoint but I believe I need to use this in conjunction with setting view.layer.position to account for the change in the anchor point.
If anyone could point me in the right direction I'd be very grateful!

Take a look at my answer here to understand why your view is moving when you change the anchor point: https://stackoverflow.com/a/12208587/77567
So, start with your view's transform set to identity. When the view is untransformed, find the center of its top edge:
CGRect frame = view.frame;
CGPoint topCenter = CGPointMake(CGRectGetMidX(frame), CGRectGetMinY(frame));
Then the anchor point to the middle of the top edge:
view.layer.anchorPoint = CGPointMake(0.5, 0);
Now the layer's position (or the view's center) is the position of the center of the layer's top edge. If you stop here, the layer will move so that the center of its top edge is where its true center was before changing the anchor point. So change the layer's position to the center of its top edge from a moment ago:
view.layer.position = topCenter;
Now the layer stays in the same place on screen, but its anchor point is the center of its top edge, so scales and rotations will leave that point fixed.

By using an extension
Scaling an UIView with a specific anchor point while avoiding misplacement:
extension UIView {
func applyTransform(withScale scale: CGFloat, anchorPoint: CGPoint) {
layer.anchorPoint = anchorPoint
let scale = scale != 0 ? scale : CGFloat.leastNonzeroMagnitude
let xPadding = 1/scale * (anchorPoint.x - 0.5)*bounds.width
let yPadding = 1/scale * (anchorPoint.y - 0.5)*bounds.height
transform = CGAffineTransform(scaleX: scale, y: scale).translatedBy(x: xPadding, y: yPadding)
}
}
So to shrink a view to half of its size with its top center as anchor point:
view.applyTransform(withScale: 0.5, anchorPoint: CGPoint(x: 0.5, y: 0))

It's an old question, but I faced the same problem and took me a lot of time to get to the desired behavior, expanding a little bit the answer from #Rob Mayoff (I found the solution thanks to his explanation).
I had to show a floating-view scaling from a point touched by the user. Depending on screen coords, It could scale down, right, left, up or from the center.
The first thing to do is to set the center of our floating view in the touch point (It has to be the center, 'cause as Rob explanation):
-(void)onTouchInView:(CGPoint)theTouchPosition
{
self.floatingView.center = theTouchPosition;
....
}
The next issue is to setup the anchor point of the layer, depending on what we want to do (I measured the point relative to the screen frame, and determined where to scale it):
switch (desiredScallingDirection) {
case kScaleFromCenter:
{
//SCALE FROM THE CENTER, SO
//SET ANCHOR POINT IN THE VIEW'S CENTER
self.floatingView.layer.anchorPoint=CGPointMake(.5, .5);
}
break;
case kScaleFromLeft:
//SCALE FROM THE LEFT, SO
//SET ANCHOR POINT IN THE VIEW'S LEFT-CENTER
self.floatingView.layer.anchorPoint=CGPointMake(0, .5);
break;
case kScaleFromBottom:
//SCALE FROM BOTTOM, SO
//SET ANCHOR POINT IN THE VIEW'S CENTER-BOTTOM
self.floatingView.layer.anchorPoint=CGPointMake(.5, 1);
break;
case kScaleFromRight:
//SCALE FROM THE RIGHT, SO
//SET ANCHOR POINT IN THE VIEW'S RIGHT-CENTER
self.floatingView.layer.anchorPoint=CGPointMake(1, .5);
break;
case kScallingFromTop:
//SCALE FROM TOP, SO
//SET ANCHOR POINT IN THE VIEW'S CENTER-TOP
self.floatingView.layer.anchorPoint=CGPointMake(.5, 0);
break;
default:
break;
}
Now, all it's easy. Depending on the effect you want to give to the animation (The idea was a bouncing scaling from 0 to 100%):
//SETUP INITIAL SCALING TO (ALMOST) 0%
CGAffineTransform t=CGAffineTransformIdentity;
t=CGAffineTransformScale(t, .001, .001);
self.floatingView.transform=t;
[UIView animateWithDuration:.2
delay:0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
//BOUNCE A LITLE UP!
CGAffineTransform t = CGAffineTransformMakeScale(1.1,1.1);
self.floatingView.transform=t;
} completion:^(BOOL finished) {
[UIView animateWithDuration:.1
animations:^{
//JUST A LITTLE DOWN
CGAffineTransform t = CGAffineTransformMakeScale(.9,.9);
self.floatingView.transform = t;
} completion:^(BOOL finished) {
[UIView animateWithDuration:.05
animations:^{
//AND ALL SET
self.floatingView.transform = CGAffineTransformIdentity;
}
completion:^(BOOL finished) {
//ALL SET
}];
}];
}];
That's it. As you can see, the main thing is to setup correctly the view's position and anchor point. Then, it's all piece of cake!
Regards, and let the code be with you!

Related

Rotation around different anchorPoint and position makes image jump to new position first

I'm doing a rotation animation on a view and want it to rotate around the view's center X and bottom Y. I change the anchorPoint and position of the layer and run the animation. Here's my code:
- (void)viewDidLoad {
[super viewDidLoad];
_imageView = [UIImageView newAutoLayoutView];
_imageView.image = [PCImage imageNamed:#"Umbrella"];
[self.view addSubview:_imageView];
[_imageView autoAlignAxisToSuperviewAxis:ALAxisVertical];
[_imageView autoPinEdgeToSuperviewEdge:ALEdgeBottom];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
CGPoint newPosition = CGPointMake(CGRectGetMidX(_imageView.frame), CGRectGetMaxY(_imageView.frame));
NSLog(#"frame %#, new position %#", NSStringFromCGRect(_imageView.frame), NSStringFromCGPoint(newPosition));
_imageView.layer.anchorPoint = CGPointMake(.5, 1.0);
_imageView.layer.position = newPosition;
[UIView animateKeyframesWithDuration:2.0 delay:2.0 options:UIViewKeyframeAnimationOptionCalculationModeLinear | UIViewKeyframeAnimationOptionAutoreverse | UIViewKeyframeAnimationOptionRepeat animations:^{
[UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:.1 animations:^{
_imageView.transform = CGAffineTransformMakeRotation(M_PI / 64);
}];
} completion:nil];
}
edit
The rotation is working, but the view 'jumps' upward to a new position first, where the view's bottom is now where the view's center Y was when first laid out. I thought changing the anchorPoint and updating the position would prevent the jumping. The view is pinned to the superview's bottom edge, and center X to the superview's center X in autolayout, if that might matter. Any ideas?
edit2
I've read other good posts on this like the following but I must be missing something..
Scale UIView with the top center as the anchor point?
I ended up changing the view to not use autolayout after reading this post:
Adjust anchor point of CALayer when autolayout is used
Looks like transforms and autolayout aren't designed to work well together.
_imageView = [UIImageView new];
_imageView.image = [PCImage imageNamed:#"Umbrella"];
_imageView.frame = CGRectMake(0, kScreenHeight - _imageView.image.size.height, _imageView.image.size.width, _imageView.image.size.height);
[self.view addSubview:_imageView];
At some point hoping to experiment with other ideas in that post.

ConvertRect accounting for UIScrollView zoom and contentOffset

I've been trying to get the converted CGRect of a UIView within a UIScrollView. It works fine if I'm not zoomed, but once I zoom, the new CGRect shifts. Here is the code that's gotten me close:
CGFloat zoomScale = (scrollView.zoomScale);
CGRect newRect = [self.view convertRect:widgetView.frame fromView:scrollView];
CGPoint newPoint = [self.view convertPoint:widgetView.center fromView:scrollView];
// Increase the size of the CGRect by multiplying by the zoomScale
CGSize newSize = CGSizeMake(newRect.size.width * zoomScale, newRect.size.height * zoomScale);
// Subtract the offset of the UIScrollView for proper positioning
CGPoint newCenter = CGPointMake(newPoint.x - scrollView.contentOffset.x, newPoint.y - scrollView.contentOffset.y);
// Create rect with the proper width/height (x and y set by center)
newRect = CGRectMake(0, 0, newSize.width, newSize.height);
[self.view addSubview:widgetView];
widgetView.frame = newRect;
widgetView.center = newCenter;
I'm fairly certain that my issue lies in the zoomScale - I should probably be modifying the x and y coordinates based on the zoomScale value. Everything I have tried thus far has been unsuccessful, though.
I received the following answer from user Brian2012 on the iOS dev forums:
What I did:
Created a UIScrollView that covers the view controller's main view.
Put a desktop view (a standard UIView) in the scroll view. The origin of the desktop is at 0,0 and the size is bigger than the scroll view so I could scroll around without having to zoom first.
Put some widget views (UIImageView) into the desktop view at various locations.
Set the contentSize of the scroll view to the size of the desktop view.
Implemented viewForZoomingInScrollView to return the desktop view as the view to scroll.
Put NSLogs in scrollViewDidZoom to print out the frame of the desktop view and one of the widget views.
What I found out:
The widget frame never changes from the initial value that I set. So for example, if a widget started at position 108, 108 with a size of 64x64, then the frame is always reported as 108,108,64,64 regardless of zooming or scrolling.
The desktop frame origin never changes. I put the origin of the desktop at 0,0 in the scroll view, and the origin is always reported as 0,0 regardless of zooming or scrolling.
The only thing that changes is the desktop view's frame size, and the size is just the original size multiplied by the scroll view's zoomScale.
Conclusion:
To figure out the location of a widget relative to the coordinate system of the view controller's main view, you need to do the math yourself. The convertRect method doesn't do anything useful in this case. Here's some code to try
- (CGRect)computePositionForWidget:(UIView *)widgetView fromView:(UIScrollView *)scrollView
{
CGRect frame;
float scale;
scale = scrollView.zoomScale;
// compute the widget size based on the zoom scale
frame.size.width = widgetView.frame.size.width * scale;
frame.size.height = widgetView.frame.size.height * scale;
// compute the widget position based on the zoom scale and contentOffset
frame.origin.x = widgetView.frame.origin.x * scale - scrollView.contentOffset.x + scrollView.frame.origin.x;
frame.origin.y = widgetView.frame.origin.y * scale - scrollView.contentOffset.y + scrollView.frame.origin.y;
// return the widget coordinates in the coordinate system of the view that contains the scroll view
return( frame );
}
I have also similar kind of issue, i fixed it by a different trick. i catch the zoom scale in a temporary variable, and set scrollView's zoom scale to minimum(1.0) and then calculate my frame using convertRect() and set the original zoom scale again
CGFloat actualZoomScal = self.baseVideoView.zoomScale;
CGPoint actualOffset = self.baseVideoView.contentOffset;
self.baseVideoView.zoomScale = 1.0;
CGRect iFrame = [[RGLayout layout] rectToPixels:[self recordingIndicatorFrame]];
self.recordIndicator = [[RiscoRecordingIndicatorView alloc] initWithFrame:[self convertRect:iFrame fromView:self.previewView]];
[self addSubview:self.recordIndicator] ;
[self bringSubviewToFront:self.recordIndicator] ;
self.baseVideoView.zoomScale = actualZoomScal;
self.baseVideoView.contentOffset = actualOffset;

How to rotate an object around a arbitrary point?

I want to rotate an UILabel around an arbitrary point in a circular manner, not a straight line. This is my code.The final point is perfect but it goes through a straight line between the initial and the end points.
- (void)rotateText:(UILabel *)label duration:(NSTimeInterval)duration degrees:(CGFloat)degrees {
/* Setup the animation */
[UILabel beginAnimations:nil context:NULL];
[UILabel setAnimationDuration:duration];
CGPoint rotationPoint = CGPointMake(160, 236);
CGPoint transportPoint = CGPointMake(rotationPoint.x - label.center.x, rotationPoint.y - label.center.y);
CGAffineTransform t1 = CGAffineTransformTranslate(label.transform, transportPoint.x, -transportPoint.y);
CGAffineTransform t2 = CGAffineTransformRotate(label.transform,DEGREES_TO_RADIANS(degrees));
CGAffineTransform t3 = CGAffineTransformTranslate(label.transform, -transportPoint.x, +transportPoint.y);
CGAffineTransform t4 = CGAffineTransformConcat(CGAffineTransformConcat(t1, t2), t3);
label.transform = t4;
/* Commit the changes */
[UILabel commitAnimations];
}
You should set your own anchorPoint
Its very much overkill to use a keyframe animation for what really is a change of the anchor point.
The anchor point is the point where all transforms are applied from, the default anchor point is the center. By moving the anchor point to (0,0) you can instead make the layer rotate from the bottom most corner. By setting the anchor point to something where x or y is outside the range 0.0 - 1.0 you can have the layer rotate around a point that lies outside of its bounds.
Please read the section about Layer Geometry and Transforms in the Core Animation Programming Guide for more information. It goes through this in detail with images to help you understand.
EDIT: One thing to remember
The frame of your layer (which is also the frame of your view) is calculated using the position, bounds and anchor point. Changing the anchorPoint will change where your view appears on screen. You can counter this by re-setting the frame after changing the anchor point (this will set the position for you). Otherwise you can set the position to the point you are rotating to yourself. The documentation (linked to above) also mentions this.
Applied to you code
The point you called "transportPoint" should be updated to calculate the difference between the rotation point and the lower left corner of the label divided by the width and height.
// Pseudocode for the correct anchor point
transportPoint = ( (rotationX - labelMinX)/labelWidth,
(rotationX - labelMinY)/labelHeight )
I also made the rotation point an argument to your method. The full updated code is below:
#define DEGREES_TO_RADIANS(angle) (angle/180.0*M_PI)
- (void)rotateText:(UILabel *)label
aroundPoint:(CGPoint)rotationPoint
duration:(NSTimeInterval)duration
degrees:(CGFloat)degrees {
/* Setup the animation */
[UILabel beginAnimations:nil context:NULL];
[UILabel setAnimationDuration:duration];
// The anchor point is expressed in the unit coordinate
// system ((0,0) to (1,1)) of the label. Therefore the
// x and y difference must be divided by the width and
// height of the label (divide x difference by width and
// y difference by height).
CGPoint transportPoint = CGPointMake((rotationPoint.x - CGRectGetMinX(label.frame))/CGRectGetWidth(label.bounds),
(rotationPoint.y - CGRectGetMinY(label.frame))/CGRectGetHeight(label.bounds));
[label.layer setAnchorPoint:transportPoint];
[label.layer setPosition:rotationPoint]; // change the position here to keep the frame
[label.layer setTransform:CATransform3DMakeRotation(DEGREES_TO_RADIANS(degrees), 0, 0, 1)];
/* Commit the changes */
[UILabel commitAnimations];
}
I decided to post my solution as an answer. It works fine accept it doesn't have the old solutions's curve animations (UIViewAnimationCurveEaseOut), but I can sort that out.
#define DEGREES_TO_RADIANS(angle) (angle / 180.0 * M_PI)
- (void)rotateText:(UILabel *)label duration:(NSTimeInterval)duration degrees:(CGFloat)degrees {
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddArc(path,nil, 160, 236, 100, DEGREES_TO_RADIANS(0), DEGREES_TO_RADIANS(degrees), YES);
CAKeyframeAnimation *theAnimation;
// animation object for the key path
theAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
theAnimation.path=path;
CGPathRelease(path);
// set the animation properties
theAnimation.duration=duration;
theAnimation.removedOnCompletion = NO;
theAnimation.autoreverses = NO;
theAnimation.rotationMode = kCAAnimationRotateAutoReverse;
theAnimation.fillMode = kCAFillModeForwards;
[label.layer addAnimation:theAnimation forKey:#"position"];
}
CAKeyframeAnimation is the right tool for this job. Most UIKit animations are between start and end points. The middle points are not considered. CAKeyframeAnimation allows you to define those middle points to provide a non-linear animation. You will have to provide the appropriate bezier path for your animation. You should look at this example and the one's provided in the Apple documentation to see how it works.
translate, rotate around center, translate back.

Get size of UIView after applying CGAffineTransform

I was surprised not to find an answer to this question, maybe is something very simple I somehow overlook :
How to get the real size of an UIView after I apply a CGAffineTransform to it?
eg.
my UIView has size 300 x 200, I apply a scaling transform let's say factor 2 both horizontal and vertical, so the UIView now takes 600 x 400 on the screen, but it's bounds and it's layer's bounds are still returning a size of 300 x 200 ... where do I find the real size of the UIView ?
ps. forgot to mention I want to also rotate the uiview. If I apply only scaling CGSizeApplyAffineTransform works great, but when there's also rotation, then it does not work properly.
Edit: drawnonward pointed me in the right direction, I just refined a bit the code to compile and here it is :
UIView* view = (your view being transformed);
CGAffineTransform trans = (view.transform or create a new transformation);
CGRect rect = [view bounds];
CGMutablePathRef path = CGPathCreateMutable();
rect.origin = CGPointZero;
CGPathAddRect(path , &trans , rect);
rect = CGPathGetBoundingBox( path );
CGPathRelease( path );
Now rect.size contains the dimensions of the view with the transformation applied
Thanks again to drawnonward
I use this in Objective C:
CGRect transformedBounds = CGRectApplyAffineTransform(view.bounds, view.transform);
or in Swift 4:
let transformedBounds = view.bounds.applying(view.transform)
[myView frame] returns the frame of the view as seen by the parent, for layout and relative sizes. [myView bounds] returns the bounds of the view as seen by itself, for drawing. If you have transforms applied to multiple views, you can use convertRect: to or from a view.
Edit:
Maybe something like this.
CGRect rect = [view bounds];
CGPathRef path = CGPathCreateMutable();
rect.origin = CGPointZero;
CGPathAddRect( rect , [view transform] );
rect = CGPathGetBoundingBox( path );
CGPathRelease( path );
The use [view center] to find the position in the superview.
Old question, but bumped into here, after searching a solution and tons of attempts. It was simple;
view.layer.frame has all transformations applied and you'll get the size from view.layer.frame.size easily.
-- below here is not an answer to this question - -
And for my problem, I was trying to calculate new center value after changing layer.anchorPoint of my rotated view, so it doesn't move. And finally did it like this;
CGPoint topLeft = [self.superview convertPoint:CGPointMake(0, 0) fromView:self];
self.layer.anchorPoint = CGPointMake(0, 0);
self.center = topLeft;
for reverse
CGPoint center = [self.superview convertPoint:CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2) fromView:self];
self.layer.anchorPoint = CGPointMake(.5, .5);
self.center = center;
finally.
Use CGSizeApplyAffineTransform(size, transform) and it will return a transformed size. There are similar CGPoint and CGRect functions as well.
Simpler: A view with (bounds) size s to which transform tr is applied has resulting size:
CGSizeMake(s.width*hypotf(tr.a, tr.b), s.height*hypotf(tr.c, tr.d))
However, if view's superview or any ancestor view has any non-unit transform applied, this size makes little sense in absolute terms.
If you want the absolute size of a view in window coordinates after any arbitrary transform has been applied to that view or its superviews, you should first compute the absolute transform matrix by composing all the view transform up to the root window, and then apply the above formula to the result.
But you apply a rotating transform, it don't get right size by CGPathGetBoundingBox.
If you applied the CGAffineTransform the view's .layer then the adjusted CGRect region after scale and/or translation transforms is simply view.layer.frame

NSAffineTransforms not being used?

I have a subclass of NSView, and in that I'm drawing an NSImage. I'm unsing NSAffineTransforms to rotate, translate and scale the image.
Most of it works fine. However, sometimes, the transforms just don't seem to get activated.
For example, when I resize the window, the rotate transform doesn't happen.
When I zoom in on the image, it puts the lower left of the image in the correct place, but doesn't zoom it, but it does zoom the part of the image that would be to the right of the original sized image. If I rotate this, it zooms correctly, but translates wrong. (The transation may be a calculation error on my part)
Here is the code of my drawRect: (sorry for the long code chunk)
- (void)drawRect:(NSRect)rect
{
// Drawing code here.
double rotateDeg = -90* rotation;
NSAffineTransform *afTrans = [[NSAffineTransform alloc] init];
NSGraphicsContext *context = [NSGraphicsContext currentContext];
NSSize sz;
NSRect windowFrame = [[self window] frame];
float deltaX, deltaY;
NSSize superSize = [[self superview] frame].size;
float height, width, sHeight, sWidth;
NSRect imageRect;
if(image)
{
sz = [ image size];
imageRect.size = sz;
imageRect.origin = NSZeroPoint;
imageRect.size.width *= zoom;
imageRect.size.height *= zoom;
height = sz.height * zoom ;
width = sz.width *zoom ;
sHeight = superSize.height;
sWidth = superSize.width;
}
I need to grab the sizes of everything early so that I can use them later when I rotate. I am not sure that I need to protect any of that, but I'm paranoid from years of C...
[context saveGraphicsState];
// rotate
[afTrans rotateByDegrees:rotateDeg];
// translate to account for window size;
deltaX = 0;
deltaY = 0;
// translate to account for rotation
// in 1 and 3, X and Y are reversed because the entire FRAME
// (inculding axes) is rotated!
switch (rotation)
{
case 0:
// NSLog(#"No rotation ");
break;
case 1:
deltaY -= (sHeight - height);
deltaX -= sHeight ;
break;
case 2:
deltaX -= width;
deltaY -= ( 2*sHeight - height);
// it's rotating around the lower left of the FRAME, so,
// we need to move it up two frame hights, and then down
// the hieght of the image
break;
case 3:
deltaX += (sHeight - width);
deltaY -= sHeight;
break;
}
Since I'm rotating around the lower left corner, and I want the image to be locked to the upper left corner, I need to move the image around. When I rotate once, the image is in the +- quadrant, so I need to shift it up one view-height, and to the left a view-height minus an image height. etc.
[afTrans translateXBy:deltaX yBy:deltaY];
// for putting image in upper left
// zoom
[afTrans scaleBy: zoom];
printMatrix([afTrans transformStruct]);
NSLog(#"zoom %f", zoom);
[afTrans concat];
if(image)
{
NSRect drawingRect = imageRect;
NSRect frame = imageRect;
frame.size.height = MAX(superSize.height, imageRect.size.height) ;
[self setFrame:frame];
deltaY = superSize.height - imageRect.size.height;
drawingRect.origin.y += deltaY;
This makes the frame the correct size so that the image is in the upper left of the frame.
If the image is bigger than the window, I want the frame to be big enough so scroll bars appear. If it isn't I want the frame to be big enough that it reaches the top of the window.
[image drawInRect:drawingRect
fromRect:imageRect
operation:NSCompositeSourceOver
fraction:1];
if((rotation %2) )
{
float tmp;
tmp = drawingRect.size.width;
drawingRect.size.width = drawingRect.size.height;
drawingRect.size.height = tmp;
}
This code may be entirely historical, now that I look at it... the idea was to swap height andwidth if I rotated 90 or 270 degs.
}
else
NSLog(#"no image");
[afTrans release];
[context restoreGraphicsState];
}
Why do you use the superview's size? That's something you should almost never need to worry about. You should make the view work on its own without dependencies on being embedded in any specific view.
Scaling the size of imageRect is probably not the right way to go. Generally when calling -drawImage you want the source rect to be the bounds of the image, and scale the destination rect to zoom it.
The problems you're reporting kind of sound like you're not redrawing the entire view after changing the transformation. Are you calling -setNeedsDisplay: YES?
How is this view embedded in the window? Is it inside an NSScrollView? Have you made sure the scroll view resizes along with the window?