How can I move and rotate a view using CGAffineTransform? - uiview

I want to move a view using a pan gesture recognizer.
UIPanGestureRecognizer *gesture;
CGPoint touch = [gesture locationInView:view.superview];
view.frame = CGRectMake(touch.x, touch.y, view.frame.size.width, view.frame.size.height);
Also, I would like to rotate the view as it moves.
view.transform = CGAffineTransformMakeRotation(multiplier * M_2_PI);
I have two basic problems:
The movement didn't start from the point the user touched the view.
When I try to both move and rotate the view it stretches beyond logic.
Can someone give me a very basic code sample on how to fix those issues using CGAffineTransform rather than go read this and that?

You can find code example here https://github.com/K-Be/ViewMovingTest
Main idea is
Save starting point of center of the view.
Find translation and apply it for center of the view.
If you want to change frame, you should set identity transform before and restore transform after applying, because frame is a function of bounds, center, transform (https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIView_Class/UIView/UIView.html#//apple_ref/occ/instp/UIView/frame)
This is some code:
if (_panRecognizer.state == UIGestureRecognizerStateBegan)
{
_startCenter = _frameView.center;
}
else if (_panRecognizer.state == UIGestureRecognizerStateChanged)
{
CGPoint transition = [_panRecognizer translationInView:self.view];
CGPoint newCenter = CGPointMake(_startCenter.x + transition.x, _startCenter.y + transition.y);
self.frameView.center = newCenter;
}
else
{
}
and
CGAffineTransform transform = self.frameView.transform;
self.frameView.transform = CGAffineTransformIdentity;
self.frameView.frame = CGRectInset(self.view.bounds, CGRectGetWidth(self.view.bounds) / 3.0, CGRectGetHeight(self.view.bounds) / 3.0);
self.frameView.transform = transform;

Related

How to Move UIView using touchesMoved having affineTransform

I have UIView which is transform vertically by using
currentView.layer.affineTransform = CATransform3DGetAffineTransform(CATransform3DConcat(currentView.layer.transform,CATransform3DMakeRotation(M_PI, 1.0, 0.0, 0.0f)));
Now I need to move this UIView from one location to other by using touches, for this I have used
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
}
-(void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
}
But I am not able to maintain the transform, How we can achive this?
Basically I have added some UI elements on UIView using following call
-(void)AddItemOnView:(UIView*)aView
Angle:(CGFloat)aDegree
XOrigin:(CGFloat)aXOrigin
YOrigin:(CGFloat)aYOrigin
Width:(CGFloat)aWidth
Height:(CGFloat)aHeight
FlipX:(CGFloat)aFlipHorrizontal
FlipY:(CGFloat)aFlipVerticle
{
UIView* currentView = aView;
if(currentView)
{
CGFloat angle = aDegree;
CGFloat flipHorrizontal = aFlipHorrizontal;
CGFloat flipVerticle = aFlipVerticle;
CGFloat xOrigin = aXOrigin;
CGFloat yOrigin = aYOrigin;
CGFloat width = aWidth;
CGFloat height = aHeight;
currentView.layer.anchorPoint = CGPointMake(0.0f, 0.0f);
currentView.frame = CGRectIntegral(CGRectMake(0, 0, width, height));
/* Flip The View Horrizontly*/
if(flipHorrizontal < 0)
{
/* Concat With previous Layer Operation */
currentView.layer.affineTransform = CATransform3DGetAffineTransform(CATransform3DConcat(currentView.layer.transform,CATransform3DMakeRotation(M_PI, 0.0, 1.0, 0.0f)));
/* Need to set anchor point ==> Top Right Corner */
currentView.layer.anchorPoint = CGPointMake(1.0f, 0.0f);
}
/* Flip The View Verticaly*/
if(flipVerticle < 0)
{
/* Concat With previous Layer Operation */
currentView.layer.affineTransform = CATransform3DGetAffineTransform(CATransform3DConcat(currentView.layer.transform,CATransform3DMakeRotation(M_PI, 1.0, 0.0, 0.0f)));
if(flipHorrizontal < 0)
{
/* This needs to set as we have already done flip X */
/* Need to set anchor point ==> Bottom Right Corner */
currentView.layer.anchorPoint = CGPointMake(1.0f, 1.0f);
}
else
{
/* Need to set anchor point ==> Bottom Left Corner */
currentView.layer.anchorPoint = CGPointMake(0.0f, 1.0f);
}
}
/* Perform Rotation */
if(angle != 0)
{
/* Concat With previous Layer Operation */
currentView.layer.affineTransform = CATransform3DGetAffineTransform(CATransform3DConcat(currentView.layer.transform,CATransform3DMakeRotation(DegreesToRadians(angle), 0, 0, 1.0)));
if(flipHorrizontal < 0 || flipVerticle < 0)
{
/* Countinue with previous anchor point */
}
else
{
/* Need to set anchor point ==> Top Left Corner */
currentView.layer.anchorPoint = CGPointMake(0.0f, 0.0f);
}
}
/* Set Origins of View */
currentView.layer.position = CGPointMake(xOrigin, yOrigin);
[self addSubview:currentView];
}
}
Now I am looking to Move these added UIViews having transform.
I'm not sure what you mean by "I am not able to maintain the transform" Anyway, here is a slightly different approach that I think may help you.
For starters, when you apply to a view a transform other than the 'Identity' transform the frame property becomes meaningless. This means you cannot use its origin member to change the view's position. You have to use the view's center property instead.
Also, for dragging, I strongly recommend you use a UIPanGestureRecognizer instead of the touches... methods. This is because the gesture recognizer maintains state for you and it's super easy to drag things around.
Here is some example code:
// Create your view and apply all the transforms you want
// --code here--
// Create and assign the UIPanGestureRecognizer
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(panDetected:)];
[self.currentView addGestureRecognizer:panGesture];
// Here is where the dragging happens
-(void)panDetected:(UIPanGestureRecognizer*)panGesture {
// Get the view that detected the gesture
UIView *view = panGesture.view;
// If dragging started or changed...
if (panGesture.state == UIGestureRecognizerStateBegan || panGesture.state == UIGestureRecognizerStateChanged) {
// Get the translation in superview coordinates
CGPoint translation = [panGesture translationInView:view.superview];
// Get your view's center
CGPoint viewCenter = view.center;
// Add the delta
viewCenter.x += translation.x;
viewCenter.y += translation.y;
view.center = viewCenter;
// Reset delta from the gesture recognizer
[panGesture setTranslation:CGPointZero inView:view.superview];
}
}
I tested this code in a project of mine with a view that has a rotation transform and it works perfectly.
Hope this helps!

Issue scaling layer at the center of a pinch gesture

I am currently having a map(tilemap) within a layer that i would like to pan/zoom using the following code:
- (void) pinchGestureUpdated: (UIPinchGestureRecognizer *) recognizer {
if([recognizer state] == UIGestureRecognizerStateBegan) {
_lastScale = [recognizer scale];
CGPoint touchLocation1 = [recognizer locationOfTouch:0 inView:recognizer.view];
CGPoint touchLocation2 = [recognizer locationOfTouch:1 inView:recognizer.view];
CGPoint centerGL = [[CCDirector sharedDirector] convertToGL: ccpMidpoint(touchLocation1, touchLocation2)];
_pinchCenter = [self convertToNodeSpace:centerGL];
}
else if ([recognizer state] == UIGestureRecognizerStateChanged) {
// NSLog(#"%d", recognizer.scale);
CGFloat newDeltaScale = 1 - (_lastScale - [recognizer scale]);
newDeltaScale = MIN(newDeltaScale, kMaxScale / self.scale);
newDeltaScale = MAX(newDeltaScale, kMinScale / self.scale);
CGFloat newScale = self.scale * newDeltaScale;
//self.scale = newScale;
[self scale: newScale atCenter:_pinchCenter];
_lastScale = [recognizer scale];
}
}
- (void) scale: (CGFloat) newScale atCenter: (CGPoint) center {
CGPoint oldCenterPoint = ccp(center.x * self.scale, center.y * self.scale);
// Set the scale.
self.scale = newScale;
// Get the new center point.
CGPoint newCenterPoint = ccp(center.x * self.scale, center.y * self.scale);
// Then calculate the delta.
CGPoint centerPointDelta = ccpSub(oldCenterPoint, newCenterPoint);
// Now adjust your layer by the delta.
self.position = ccpAdd(self.position, centerPointDelta);
}
my issue is that the zoom is not taking in effect at the center of the pinch even though i am trying to change it at the same time i am zooming in through this method: (void) scale: (CGFloat) newScale atCenter: (CGPoint) center. Is there any reason this might not happening properly? Also how do i convert to the center location of the pinch into the coordinates system for my scene/layer?
Everything was actually fine in my approach. The problem i was having though is that the layer anchor point was different from the map one i defined, which was introducing an offset during scaling. I had to set both anchor to ccp(0,0).
The concersion from the screen coordinates of the pinch gesture's center to the layer is correct and can be achived by the following instructions when using UIKIt gesture recognizers:
CGPoint centerGL = [[CCDirector sharedDirector] convertToGL: ccpMidpoint(touchLocation1, touchLocation2)];
_pinchCenter = [self convertToNodeSpace:centerGL];
First of all, you cannot do ([recognizer state] == UIGestureRecognizerStateBegan) because state is a bitfield! So you have to do:
([recognizer state] & UIGestureRecognizerStateBegan)
The center location of your pinch is going to be in coordinates on the screen basically. As far as how you convert that into your own coordinate system, you need to figure out what the bounds on the device's screen are of the part of your scene/layer that is shown at the time the gesture starts. That's going to be coordinates like 10,10 x 200,200 or something, representing the pixel grid of the screen. Then you will have to figure out, in the coordinate system of your own app's scene/layer, what 10,10 maps to, and what 200,200 maps to. From there, you can derive a factor to apply to the screen coordinates of the pinch gesture's center, that would translate the pinch gesture's screen coordinates into the scene/layer coordinates.
What you're trying to do is tricky because as you scale the scene/layer, your centering that scaling around a point that's not in the center of the view. I'm sure if you look through some of Apple's sample code in one of the map-related apps, you can probably find some examples of methods that have this kind of pinch zooming.
I hope this helps.

How to restrict a moveable view by Pan gesture

I have a UIImageView which is moveable via a pan gesture.
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
[self.photoMask addGestureRecognizer:pan];
I would like to restrict the area this can be moved on screen. Rather than the user be able to drag the view right to the side of the screen, I want to restrict it by a margin of some sort. How can I do this?
Also, how is this then handled when rotated?
EDIT ---
#pragma mark - Gesture Recognizer
-(void)handlePan:(UIPanGestureRecognizer *)gesture {
NSLog(#"Pan Gesture");
gesture.view.center = [gesture locationInView:self.view];
}
This is my current method to handle the pan. What I need to do is continue to move the imageview by the center point and also restrict its movement when close to the edge of the screen by 50 for example.
One possible solution to this is in your handlePan method, check the location of the point on the screen, and only commit the change if it is within the bounds you wish to restrict it to.
For ex.
-(void) handlePan:(UIGestureRecognizer*)panGes{
CGPoint point = [panGes locationInView:self.view];
//Only allow movement up to within 100 pixels of the right bound of the screen
if (point.x < [UIScreen mainScreen].bounds.size.width - 100) {
CGRect newframe = CGRectMake(point.x, point.y, theImageView.frame.size.width, theImageView.frame.size.height);
theImageView.frame = newframe;
}
}
I believe this would also correctly handle any screen rotation
EDIT
To move your image view by the center of its frame, the handlePan method could look something like this.
-(void)handlePan:(UIPanGestureRecognizer *)gesture {
CGPoint point = [gesture locationInView:self.view];
//Only allow movement up to within 50 pixels of the bounds of the screen
//Ex. (IPhone 5)
CGRect boundsRect = CGRectMake(50, 50, 220, 448);
if (CGRectContainsPoint(boundsRect, point)) {
imgView.center = point;
}
}
Check whether the point is within your desired bounds, and if so, set the center of your image view frame to that point.
I'm not sure if I'm being over-simplistic here but I think you can accomplish this by using an if clause.
-(void)handlePan:(UIPanGestureRecognizer*)gesture {
UIImageView *viewToDrag = gesture.view; // this is the view you want to move
CGPoint translation = [gesture translationInView:viewToDrag.superview]; // get the movement delta
CGRect movedFrame = CGRectOffset(viewToDrag.frame, translation.x, translation.y); // this is the new (moved) frame
// Now this is the critical part because I don't know if your "margin"
// is a CGRect or maybe some int values, the important thing here is
// to compare if the "movedFrame" values are in the allowed movement area
// Assuming that your margin is a CGRect you could do the following:
if (CGRectContainsRect(yourPermissibleMargin, movedFrame)) {
CGPoint newCenter = CGPointMake(CGRectGetMidX(movedFrame), CGRectGetMidY(movedFrame));
viewToDrag.center = newCenter; // Move your view
}
// -OR-
// If you have your margins as int values you could do the following:
if ( (movedFrame.origin.x + movedFrame.size.width) < 50) {
CGPoint newCenter = CGPointMake(CGRectGetMidX(movedFrame), CGRectGetMidY(movedFrame));
viewToDrag.center = newCenter; // Move your view
}
}
You'll probably have to adapt this to meet your specific needs.
Hope this helps!
Here is the answer in Swift 4 -
Restrict the view's movement to superview
#objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer)
{
// Allows smooth movement of stickers.
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed
{
let point = gestureRecognizer.location(in: self.superview)
if let superview = self.superview
{
let restrictByPoint : CGFloat = 30.0
let superBounds = CGRect(x: superview.bounds.origin.x + restrictByPoint, y: superview.bounds.origin.y + restrictByPoint, width: superview.bounds.size.width - 2*restrictByPoint, height: superview.bounds.size.height - 2*restrictByPoint)
if (superBounds.contains(point))
{
let translation = gestureRecognizer.translation(in: self.superview)
gestureRecognizer.view!.center = CGPoint(x: gestureRecognizer.view!.center.x + translation.x, y: gestureRecognizer.view!.center.y + translation.y)
gestureRecognizer.setTranslation(CGPoint.zero, in: self.superview)
}
}
}
}
If you want more control over it, match restrictByPoint value to your movable view's frame.
- (void)dragAction:(UIPanGestureRecognizer *)gesture{
UILabel *label = (UILabel *)gesture.view;
CGPoint translation = [gesture translationInView:label];
if (CGRectContainsPoint(label.frame, [gesture locationInView:label] )) {
label.center = CGPointMake(label.center.x,
label.center.y);
[gesture setTranslation:CGPointZero inView:label];
}
else{
label.center = CGPointMake(label.center.x,
label.center.y + translation.y);
[gesture setTranslation:CGPointZero inView:label];
}
}

iOS - UIPinchGestureRecognizer & UIPanGestureRecognizer - Reset

In my application for zooming and panning, i'm using above said gesture recognizers. This is working fine.
I want to a button which will bring back the image to initial state. That means show the actual image or reset to initial state. Can some one tell me how to achieve this?
The code is as below:
-(void)handlePanGesture:(UIPanGestureRecognizer*)recognizer
{
CGPoint translation = [(UIPanGestureRecognizer*)recognizer translationInView:[self superview]];
recognizer.view.center = CGPointMake(recognizer.view.center.x + translation.x, recognizer.view.center.y + translation.y);
[(UIPanGestureRecognizer*)recognizer setTranslation:CGPointMake(0, 0) inView:[self superview]];
}
-(void)handlePinchGesture:(UIPinchGestureRecognizer*)recognizer
{
static CGRect initialBounds;
if (recognizer.state == UIGestureRecognizerStateBegan)
{
initialBounds = self.bounds;
}
CGFloat factor = [(UIPinchGestureRecognizer *)recognizer scale];
CGAffineTransform zt = CGAffineTransformScale(CGAffineTransformIdentity, factor, factor);
self.bounds = CGRectApplyAffineTransform(initialBounds, zt);
}
Based on #borrden's comment.
Check if the current center of the ImageView and original center are same. If not reset the center of the ImageView. You can add a UIView.animation.. to make it look good.
Resize the imageView to original size by setting it to CGAffineTransformIdentity. This can also be added to the UIView.animation.. in the above.
Code. Make changes as per your need.
UIView.animateWithDuration(0.2, delay: 0.0, options: .CurveEaseIn, animations: {
//Move image back to center
self.mainImageView.center = self.originalCenter!
self.layoutIfNeeded()
//Resize image to original
self.mainImageView.transform = CGAffineTransformIdentity
}, completion: nil
)

UIGestureRecognizer to Drag UIView

I'm trying to build a GestureRecognizer to drag a UIView from right to left. If the user drag the View to the half of the screen, the View will be automatically dragged into the left corner to discard it, but if the user do not drag up to half the screen it will return to the corner right of the screen.
Little lost here, any tutorials or something?
Thanks
EDIT : Heres some code, its a UIImageView, inside a UIScrollView, i need to drag the entire scroll or just the image inside it :
_myScroll = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, self.window.bounds.size.width, self.window.bounds.size.height)]; [_myScroll setContentSize:CGSizeMake(self.window.bounds.size.width , 1300)];
_tutorial = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, self.window.bounds.size.width, 1300)]; [_tutorial setImage:[UIImage imageNamed:#"Default"]];
_tutorial.contentMode = UIViewContentModeScaleAspectFit; [_myScroll addSubview:_tutorial];
_tutorial.userInteractionEnabled = YES;
UIPanGestureRecognizer * recognizer = [[UIPanGestureRecognizer alloc]initWithTarget:_tutorial action:#selector(handlePan:)];
[_tutorial addGestureRecognizer:recognizer];
[self.window addSubview:_myScroll];
And the method i try to drag the UIImageView
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:_myScroll];
recognizer.view.center = CGPointMake(recognizer.view.center.x + translation.x,
recognizer.view.center.y + translation.y);
[recognizer setTranslation:CGPointMake(0, 0) inView:_myScroll];
}
Have a look over here. This is a UIGestureRecognizer tutorial which will give you the basics to handle pinch and pan gestures. Once you've learnt the basics, it's really easy to accomplish what you want. All you need is to check the position of the view once the pan is completed to send it to the correct position. Have a look at this other tutorial on UIScrollViews, it may help you find your way through the second part.
Both tutorials come from raywenderlich.com, probably the most useful iOS tutorials site I know.
Here is some code on your handlePan: method you can work on :
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:_myScroll];
recognizer.view.center = CGPointMake(recognizer.view.center.x + translation.x,
recognizer.view.center.y + translation.y);
if (recognizer.state == UIGestureRecognizerStateEnded) {
// Check here for the position of the view when the user stops touching the screen
// Set "CGFloat finalX" and "CGFloat finalY", depending on the last position of the touch
// Use this to animate the position of your view to where you want
[UIView animateWithDuration: aDuration
delay: 0
options: UIViewAnimationOptionCurveEaseOut
animations:^{
CGPoint finalPoint = CGPointMake(finalX, finalY);
recognizer.view.center = finalPoint; }
completion:nil];
}
[recognizer setTranslation:CGPointMake(0, 0) inView:_myScroll];
}
I won't give you the perfect answer here, you'll have to work on the code to find a way to make it work as you want it to.
Here is one last tip. You can do some kind of "smooth" animation by using a slideFactor. It will help your animation being more "realistic". Work on this code, that you add right at the beginning of the "if":
CGPoint velocity = [recognizer velocityInView:self.view];
CGFloat magnitude = sqrtf((velocity.x * velocity.x) + (velocity.y * velocity.y));
CGFloat slideMult = magnitude / 200;
float slideFactor = 0.1 * slideMult; // Increase for more slide
Then in UIView animateWithDuration:, use slideFactor as the duration.

Resources