I have a simple app which places an imageview on the screen, and allows the user to move it and scale it using the pinch and pan gesture recognizers. In my uipangesturerecognizer delegate method (scale), I'm trying to calculate the value of the transformed frame before I actually perform the transformation using CGAffineTransformScale. I'm not getting the correct value however, and am getting some weird result that isn't inline with what the transformed frame should be. Below is my method:
- (void)scale:(UIPinchGestureRecognizer *)sender
{
if([sender state] == UIGestureRecognizerStateBegan)
{
lastScale = [sender scale];
}
if ([sender state] == UIGestureRecognizerStateBegan ||
[sender state] == UIGestureRecognizerStateChanged)
{
CGFloat currentScale = [[[sender view].layer valueForKeyPath:#"transform.scale"] floatValue];
CGFloat newScale = 1 - (lastScale - [sender scale]) * (CropThePhotoViewControllerPinchSpeed);
newScale = MIN(newScale, CropThePhotoViewControllerMaxScale / currentScale);
newScale = MAX(newScale, CropThePhotoViewControllerMinScale / currentScale);
NSLog(#"currentBounds: %#", NSStringFromCGRect([[sender view] bounds]));
NSLog(#"currentFrame: %#", NSStringFromCGRect([[sender view] frame]));
CGAffineTransform transform = CGAffineTransformScale([[sender view] transform], newScale, newScale);
CGRect nextFrame = CGRectApplyAffineTransform([[sender view] frame], transform);
NSLog(#"nextFrame: %#", NSStringFromCGRect(nextFrame));
//NSLog(#"nextBounds: %#", NSStringFromCGRect(nextBounds));
[sender view].transform = transform;
lastScale = [sender scale];
}
}
Here is a printed result I get:
/* currentBounds: {{0, 0}, {316, 236.013}}
currentFrame: {{-115.226,-53.4392}, {543.452, 405.891}}
nextFrame: {{-202.566, -93.9454}, {955.382, 713.551}} */
With these results, the currentBounds coordinates is obviously 0,0 like it always is, and the size and width are that of the original image before it ever gets transformed. This value seems to stay the same no matter how many transformations I do.
currentFrame is the correct coordinates and the correct size and width based on the current state of the image in it's transformed state.
nextFrame is incorrect, it should match up with the currentFrame from the next set of values that gets printed but it doesn't.
So I have some questions:
1) Why is currentFrame displaying the correct value for the frame? I thought the frame was invalid after you perform transformations? This set of values was displayed after many enlargements and minimizations I was doing on the image with my fingers. It seems like the height and width of currentBounds is what I'd expect for the currentFrame.
2) Why is my next frame value being calculated incorrectly, and how do I accurately calculate the value of the transformed frame before I actually implement the transformation?
You need to apply your new transform to the view's untransformed frame. Try this:
CGAffineTransform transform = CGAffineTransformScale([[sender view] transform], newScale, newScale);
CGAffineTransform iTransform = CGAffineTransformInvert([[sender view] transform]);
CGRect rawFrame = CGRectApplyAffineTransform([[sender view] frame], iTransform);
CGRect nextFrame = CGRectApplyAffineTransform(rawFrame, transform);
Related
I have a UISlider for zooming the imageView(instead of UIPinchGesture) and i'm using UIRotationGesture,both of them works fine independently.Zooming without doing rotation gesture works fine! but Once i perform a rotation and them zoom in or out the imageview it behaves weird as it looses the rotation scale.(that's what i guess!)
How do i fix this?
Well im not good with this maths stuff struggling with this since few days and i have searched through the forums couldn't find the solution. Kindly help me:)
For Zooming:(here i'm not handling transform as im unsure of)
-(void)scale:(UISlider *)sender
{
float sliderValue = [sender value];
CGRect newFrame = placeRing.frame;
newFrame.size = CGSizeMake(sliderValue, sliderValue);
placeRing.frame = newFrame;
}
For rotation:
- (void)twoFingersRotate:(UIRotationGestureRecognizer *)recognizer
{
isRotated = TRUE;
if ([recognizer state] == UIGestureRecognizerStateBegan || [recognizer state] == UIGestureRecognizerStateChanged) {
rotation= recognizer.rotation;
rotatedTransform = CGAffineTransformRotate([placeRing transform], [recognizer rotation]);
placeRing.transform = rotatedTransform;
[recognizer setRotation:0];
}
}
The rotation applies transform which invalidates the frame property you are using to resize the view. Use bounds and center properties to zoom.
See the warning box in: http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UIView_Class/UIView/UIView.html#//apple_ref/doc/uid/TP40006816
-(void)scale:(UISlider *)sender
{
float sliderValue = [sender value];
placeRing.bounds = CGRectMake(0, 0, sliderValue, sliderValue);
}
I am creating a control for showing the timeline. Initially , it should show the year. When zooming in and the zoomScale reached a specific value, it will show the value of the individual months, and In the next zoom level, it should show the real values for the individual days.
I have creatd a scrollview and added the layers (for showing the month/year values) to this. After overriding the pinch guesture, I am able to do the zooming (Normal UIScrollview zooming). But I need to zoom it to a particular point. ie, Suppose i am zooming January, I need to keep it in the same position(for creating an effect like moving inside of January). Any suggestions to achieve this?
Is my way is currect? else please help me to start this.
Use This bellow method for your requirement,
- (void)zoomToRect:(CGRect)rect animated:(BOOL)animated
just call this above method with your selected frame or point , like if select January then just take the point ( position) of january button or view and just call above method with that frame..
for more information see this link UIScrollView_Class
i hope this helpful to you...
Here’s what it looks like:
#implementation UIScrollView (ZoomToPoint)
- (void)zoomToPoint:(CGPoint)zoomPoint withScale: (CGFloat)scale animated: (BOOL)animated
{
//Normalize current content size back to content scale of 1.0f
CGSize contentSize;
contentSize.width = (self.contentSize.width / self.zoomScale);
contentSize.height = (self.contentSize.height / self.zoomScale);
//translate the zoom point to relative to the content rect
zoomPoint.x = (zoomPoint.x / self.bounds.size.width) * contentSize.width;
zoomPoint.y = (zoomPoint.y / self.bounds.size.height) * contentSize.height;
//derive the size of the region to zoom to
CGSize zoomSize;
zoomSize.width = self.bounds.size.width / scale;
zoomSize.height = self.bounds.size.height / scale;
//offset the zoom rect so the actual zoom point is in the middle of the rectangle
CGRect zoomRect;
zoomRect.origin.x = zoomPoint.x - zoomSize.width / 2.0f;
zoomRect.origin.y = zoomPoint.y - zoomSize.height / 2.0f;
zoomRect.size.width = zoomSize.width;
zoomRect.size.height = zoomSize.height;
//apply the resize
[self zoomToRect: zoomRect animated: animated];
}
#end
Looking at it, it should be pretty straightforward to figure out how it works. If you see anything wrong with it, let me know in the comments.
Thanks :)
source: http://www.tim-oliver.com/2012/01/14/zooming-to-a-point-in-uiscrollview/
I had same problem,
I was having a world map image, & i wanted to zoom the map to certain location, India
This is how i fixed it.
I found out the position of India, in terms of CGRect(X,Y) [CGRect(500,300)]
& following code in DidLoad
[self.scrollViewForMap setZoomScale:2.0 animated:YES]; //for zooming
_scrollViewForMap.contentOffset = CGPointMake(500,300); //for location
This solved my problem. :)
- (void)pinchDetected:(UIPinchGestureRecognizer *)gestureRecognizer {
if([gestureRecognizer state] == UIGestureRecognizerStateBegan) {
// Reset the last scale, necessary if there are multiple objects with different scales
lastScale = [gestureRecognizer scale];
}
if ([gestureRecognizer state] == UIGestureRecognizerStateBegan ||
[gestureRecognizer state] == UIGestureRecognizerStateChanged) {
CGFloat currentScale = [[[gestureRecognizer view].layer valueForKeyPath:#"transform.scale"] floatValue];
// Constants to adjust the max/min values of zoom
const CGFloat kMaxScale = 2.2;
const CGFloat kMinScale = 0.64;
CGFloat newScale = 1 - (lastScale - [gestureRecognizer scale]);
newScale = MIN(newScale, kMaxScale / currentScale);
newScale = MAX(newScale, kMinScale / currentScale);
CGAffineTransform transform = CGAffineTransformScale([[gestureRecognizer view] transform], newScale, newScale);
[gestureRecognizer view].transform = transform;
[gestureRecognizer setScale:1.0];
lastScale = [gestureRecognizer scale]; // Store the previous scale factor for the next pinch gesture call
}
}
What worked for me (Swift 4):
func zoomIn(with gestureRecognizer: UIGestureRecognizer?) {
guard let location = gestureRecognizer?.location(in: gestureRecognizer?.view)
let rect = CGRect(x: location.x, y: location.y, width: 0, height: 0)
scrollView.zoom(to: rect, animated: true)
}
Or in other words creating a CGRect with zero width and height will cause the UIScrollView to zoom to the rects origin at maximumZoomScale
I'm fooling around with Apple's ScrollViewSuite, TapToZoom app, trying to place a view on the zoomed image. Here's what I want to do: on double tap, rotate the image by pi/2, zoom it by a factor of 8, then place a subview in the scrollView on top of the transformed image.
I know the view's desired frame in the original coordinate system. So I need to transform those coordinates to the new system after the zoom...
From the ScrollViewSuite...
- (void)handleDoubleTap:(UIGestureRecognizer *)gestureRecognizer {
// double tap zooms in
float newScale = [imageScrollView zoomScale] * 8; // my custom zoom
CGRect zoomRect = [self zoomRectForScale:newScale withCenter:[gestureRecognizer locationInView:gestureRecognizer.view]];
[imageScrollView zoomToRect:zoomRect animated:YES];
[self rotateView:imageScrollView by:M_PI_2]; // then the twist
}
My rotation code...
- (void)rotateView:(UIView *)anImageView by:(CGFloat)spin {
[UIView animateWithDuration:0.5 delay: 0.0
options: UIViewAnimationOptionAllowUserInteraction
animations:^{
anImageView.transform = CGAffineTransformMakeRotation(spin);
}
completion:^(BOOL finished){}];
}
Then, after the zoom is finished, place a view at a known location in the original coordinate system. This is where I think I need some help...
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)_scale {
CGFloat coX = scrollView.contentOffset.x;
CGFloat coY = scrollView.contentOffset.y;
// try to build the forward transformation that got us here, then invert it
CGAffineTransform rotate = CGAffineTransformMakeRotation(M_PI_2);
CGAffineTransform scale = CGAffineTransformMakeScale(_scale, _scale);
// should these be negative? some confusion here.
CGAffineTransform translate = CGAffineTransformMakeTranslation(coX, coY);
// what's the right order of concatenation on these? more confusion
CGAffineTransform aft0 = CGAffineTransformConcat(rotate, scale);
CGAffineTransform aft1 = CGAffineTransformConcat(aft0, translate);
// invert it
CGAffineTransform aft = CGAffineTransformInvert(aft1);
// this is the fixed rect in original coordinate system of the image
// the image is at 0,0 320, 300 in the view, so this will only work
// when the image is at the origin... need to fix that later,too.
CGRect rect = CGRectMake(248, 40, 90, 160);
CGRect xformRect = CGRectApplyAffineTransform(rect, aft);
// build a subview with this rect
UIView *newView = [scrollView viewWithTag:888];
if (!newView) {
newView = [[UIView alloc] initWithFrame:xformRect];
newView.backgroundColor = [UIColor yellowColor];
newView.tag = 888;
[scrollView addSubview:newView];
} else {
newView.frame = xformRect;
}
// see where it landed...
NSLog(#"%f,%f %f,%f", xformRect.origin.x, xformRect.origin.y, xformRect.size.width, xformRect.size.height);
}
I have a pinch gesture recognizer attached to an imageview from which I use pinches to enlarge and minimize the photo. Below is the code that I'm using in the delegate method:
- (void)scale:(UIPinchGestureRecognizer *)sender
{
if([sender state] == UIGestureRecognizerStateBegan)
{
lastScale = [sender scale];
}
if ([sender state] == UIGestureRecognizerStateBegan ||
[sender state] == UIGestureRecognizerStateChanged)
{
CGFloat currentScale = [[[sender view].layer valueForKeyPath:#"transform.scale"] floatValue];
CGFloat newScale = 1 - (lastScale - [sender scale]) * (UIComicImageViewPinchSpeed);
newScale = MIN(newScale, minScale / currentScale);
newScale = MAX(newScale, maxScale / currentScale);
CGAffineTransform transform = CGAffineTransformScale([[sender view] transform], newScale, newScale);
[sender view].transform = transform;
lastScale = [sender scale];
}
}
I need to determine where the new center of the imageview frame will be before I actually perform the transformation. Is there anyway to determine this? Basically, I'm trying to halt the scaling if it's about to move the image off the screen or close to it.
UPDATE
Thanks to Robin below for suggesting that method to figure out the transformed frame. The problem I'm running into now is that the frame becomes invalid after the transform is performed, and I need to keep track of the most recent frame in order to figure out the boundary of my image. Obviously, I can do this manually and store it in an instance variable, but wondering if there is a more "elegant" way to accomplish this?
Use CGRectApplyAffineTransform like this:
CGRect currentFrame = ....;
CGRect newFrame = CGRectApplyAffineTransform(currentFrame, transform);
// Then test if newFrame is within the limits you want
I'm working on an iPad app that will use UIGestureRecognizers to allow the user to pan, scale and rotate objects (subclass of UIView) on the screen.
I understand that the [UIView frame] property isn't valid after a transform is done, so I'm trying to take the values of my UIGestureRecognizers and keep the "frame" myself.
Here's the code I'm using to attempt this (you may recognize a lot of code from Apple's sample project, SimpleGestureRecognizers):
// Shape.h (partial)
#interface Shape : UIView <UIGestureRecognizerDelegate> {
CGFloat centerX;
CGFloat centerY;
CGFloat rotatation;
CGFloat xScale;
CGFloat yScale;
}
// Shape.m (partial)
- (void)panPiece:(UIPanGestureRecognizer *)gestureRecognizer
{
UIView *piece = [gestureRecognizer view];
[self adjustAnchorPointForGestureRecognizer:gestureRecognizer];
if ([gestureRecognizer state] == UIGestureRecognizerStateBegan || [gestureRecognizer state] == UIGestureRecognizerStateChanged) {
CGPoint translation = [gestureRecognizer translationInView:[piece superview]];
self.centerX += translation.x;
self.centerY += translation.y;
[piece setCenter:CGPointMake([piece center].x + translation.x, [piece center].y + translation.y)];
for ( HandleView *h in [self handles] ) {
[h setCenter:CGPointMake([h center].x + translation.x, [h center].y + translation.y)];
[h setNeedsDisplay];
}
[gestureRecognizer setTranslation:CGPointZero inView:[piece superview]];
NSLog(#"(%.0f, %.0f, %.0f, %.0f) %.2f˚, (%.2fx, %.2fx)", [self frame].origin.x, [self frame].origin.y, [self frame].size.width, [self frame].size.height, [self rotation], [self xScale], [self yScale]);
}
}
// rotate the piece by the current rotation
// reset the gesture recognizer's rotation to 0 after applying so the next callback is a delta from the current rotation
- (void)rotatePiece:(UIRotationGestureRecognizer *)gestureRecognizer
{
[self adjustAnchorPointForGestureRecognizer:gestureRecognizer];
if ([gestureRecognizer state] == UIGestureRecognizerStateBegan || [gestureRecognizer state] == UIGestureRecognizerStateChanged) {
CGFloat rot = [self normalizeRotation:[gestureRecognizer rotation]];
self.rotation += rot * 180.0 / M_PI;
NSLog(#"Rotation: %.12f", [gestureRecognizer rotation]);
[gestureRecognizer view].transform = CGAffineTransformRotate([[gestureRecognizer view] transform], [gestureRecognizer rotation]);
[gestureRecognizer setRotation:0];
NSLog(#"(%.0f, %.0f, %.0f, %.0f) %.2f˚, (%.2fx, %.2fx)", [self frame].origin.x, [self frame].origin.y, [self frame].size.width, [self frame].size.height, [self rotation], [self xScale], [self yScale]);
}
}
// scale the piece by the current scale
// reset the gesture recognizer's rotation to 0 after applying so the next callback is a delta from the current scale
- (void)scalePiece:(UIPinchGestureRecognizer *)gestureRecognizer
{
[self adjustAnchorPointForGestureRecognizer:gestureRecognizer];
if ([gestureRecognizer state] == UIGestureRecognizerStateBegan || [gestureRecognizer state] == UIGestureRecognizerStateChanged) {
self.xScale *= [gestureRecognizer scale];
self.yScale *= [gestureRecognizer scale];
[gestureRecognizer view].transform = CGAffineTransformScale([[gestureRecognizer view] transform], [gestureRecognizer scale], [gestureRecognizer scale]);
[gestureRecognizer setScale:1];
NSLog(#"(%.0f, %.0f, %.0f, %.0f) %.2f˚, (%.2fx, %.2fx)", [self frame].origin.x, [self frame].origin.y, [self frame].size.width, [self frame].size.height, [self rotation], [self xScale], [self yScale]);
}
}
Because of some weirdness I noticed with the rotations, I implemented the following method to help keep an accurate rotation value:
- (CGFloat) normalizeRotation:(CGFloat)rot
{
if (abs(rot) > 0.05) {
if (rot > 0) {
rot -= M_PI;
} else {
rot += M_PI;
}
return [self normalizeRotation:rot];
} else {
return rot;
}
}
Anyway, the shape on-screen pans, scales and rotates fine. All is as you would expect and the performance is good.
The problem is that, after a user moves, resizes and rotates a UIView, I want to let them tap it and give them "handles" that allow for resizing other than the "square" resizing that pinching gives (i.e., when you use the pinch gesture, you upsize or downsize in the same ratio for both x and y). Now, even with the code above, the values that are stored aren't ever quite accurate.
The "handles" I'm using are simply 10x10 dots that are supposed to go at each corner and halfway down each "side" of the UIView's frame/rectangle. When I first place a square and tap it to get the handles before doing anything else, the handles appear in the appropriate place. When I move, resize and rotate an object, then tap it, the handles are all shifted off of the shape some amount. It generally seems to be about 20 pixels.
Are the values in the UIGestureRecognizer objects just not accurate enough? That doesn't seem to be the case, because those values are used to change the object on-screen, and that is accurate.
I'm pretty sure there's no way for me to get a real representation of the UIView's frame after messing with it so much, but where's the flaw in my custom code that's giving me bad values? Am I losing precision when converting between degrees and radians?
On a related note: Apple obviously has internal code that keeps track of how to draw a view that's been translated/transformed. The box of solid color that I'm currently using is moved, zoomed and rotated correctly. So, is there any way to access the values that they use for displayed a translated/transformed UIView?
Thanks!