To minimize and maximize a UIView I am using a UIPangestureRecognizer. The code is listed below:
-(void) pannningMyView:(UIPanGestureRecognizer*) panGesture{
CGPoint newPoint=[panGesture translationInView:self.view];
CGPoint oldPoint=self.myPanningView.frame.origin;
CGFloat dx=newPoint.x;
CGFloat dy=newPoint.y;
if(dx>0){
CGRect oldRect=self.myPanningView.frame;
oldRect.size.width+=dx;
oldRect.size.height+=dy;
self.myPanningView.frame=oldRect;
}
}
But, the transition is very fast, such that I move few pixels and it covers the entire screen. I am not able to figure out what correction is required to my code.
The problem is that your translations are cumulative because the translationInView is from the beginning of the continuous gesture, but you're you're adding the translation to the current frame, not the original frame. This is solved by checking the gesture state, and if you're at the start of the gesture then save the original frame, and then use that as the basis for future translations as the gesture proceeds.
-(void) panningMyView:(UIPanGestureRecognizer*) panGesture
{
static CGRect originalFrame; // or you could make this a non-static class ivar
if (panGesture.state == UIGestureRecognizerStateBegan)
{
originalFrame = self.myPanningView.frame;
}
else if (panGesture.state == UIGestureRecognizerStateChanged)
{
CGPoint translation = [panGesture translationInView:self.view];
if (translation.x > 0) {
CGRect newFrame = originalFrame;
newFrame.size.width += translation.x;
newFrame.size.height += translation.y;
self.myPanningView.frame = newFrame;
}
}
}
Note, I got rid of oldPoint because you didn't seem to be using it. I also renamed newPoint to translation because it's not a point on the screen but a measure of how much your finger has moved (or translated) on the screen. I also renamed oldRect to newFrame, because I think that more accurately captures what it is.
Essentially, I've tried to preserve the logic of your routine, but simply clarify your logic and variable names. I would have thought that you might want an additional else if cause, checking for ended or canceled gestures, using an animation to complete or reverse the gesture as appropriate, but I didn't tackle that as you didn't reference this in your original question.
Regardless, I hope you get the idea of what we're doing here. We're saving the original frame and applying the translation to that rather than applying it to the current frame.
Update:
In a follow up question, you asked how you might clarify the animation. You might do something like:
-(void) panningMyView:(UIPanGestureRecognizer*) panGesture
{
static CGRect originalFrame; // or you could make this a non-static class ivar
CGPoint translation = [panGesture translationInView:self.view];
if (panGesture.state == UIGestureRecognizerStateBegan)
{
originalFrame = self.myPanningView.frame;
}
else if (panGesture.state == UIGestureRecognizerStateChanged)
{
if (translation.x > 0) {
CGRect newFrame = originalFrame;
newFrame.size.width += translation.x;
newFrame.size.height += translation.y;
self.myPanningView.frame = newFrame;
}
}
else if (panGesture.state == UIGestureRecognizerStateEnded ||
panGesture.state == UIGestureRecognizerStateCancelled ||
panGesture.state == UIGestureRecognizerStateFailed)
{
CGRect finalFrame = originalFrame;
// if we've gone more than half way, move it all the way,
// otherwise return it to the original frame
if (translation.x > (self.view.frame.size.width / 2.0))
{
finalFrame.size.width += self.view.frame.size.width;
finalFrame.size.height += self.view.frame.size.height;
}
[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
self.myPanningView.frame = finalFrame;
}
completion:nil];
}
}
Related
I have a UILabel in a subview and I have a panGesture and pinchGesture on the UILabel. As of right now I can move the UILabel crossed all views. I want this UILabel do stay within the area of the subView. How would I accomplish this?
- (void)handlePanGesture:(UIPanGestureRecognizer *)panGesture {
CGPoint translation = [panGesture translationInView:panGesture.view.superview];
if (UIGestureRecognizerStateBegan == panGesture.state ||UIGestureRecognizerStateChanged == panGesture.state) {
panGesture.view.center = CGPointMake(panGesture.view.center.x + translation.x,
panGesture.view.center.y + translation.y);
[panGesture setTranslation:CGPointZero inView:self.view];
}
}
In this line,
CGPoint translation = [panGesture translationInView:panGesture.view.superview];
It is setting it to the superView and I am trying to set it to my subView but I can't seem to figure it out.
Here is my code to handle draggable button and restrict it to the main view boundary
I hope this code will help you
CGPoint translation = [recognizer translationInView:self.view];
CGRect recognizerFrame = recognizer.view.frame;
recognizerFrame.origin.x += translation.x;
recognizerFrame.origin.y += translation.y;
// Check if UIImageView is completely inside its superView
if (CGRectContainsRect(self.view.bounds, recognizerFrame)) {
recognizer.view.frame = recognizerFrame;
}
// Else check if UIImageView is vertically and/or horizontally outside of its
// superView. If yes, then set UImageView's frame accordingly.
// This is required so that when user pans rapidly then it provides smooth translation.
else {
// Check vertically
if (recognizerFrame.origin.y < self.view.bounds.origin.y) {
recognizerFrame.origin.y = 0;
}
else if (recognizerFrame.origin.y + recognizerFrame.size.height > self.view.bounds.size.height) {
recognizerFrame.origin.y = self.view.bounds.size.height - recognizerFrame.size.height;
}
// Check horizantally
if (recognizerFrame.origin.x < self.view.bounds.origin.x) {
recognizerFrame.origin.x = 0;
}
else if (recognizerFrame.origin.x + recognizerFrame.size.width > self.view.bounds.size.width) {
recognizerFrame.origin.x = self.view.bounds.size.width - recognizerFrame.size.width;
}
}
// Reset translation so that on next pan recognition
// we get correct translation value
[recognizer setTranslation:CGPointZero inView:self.view];
I've been working on this code for quite a while now but it just feels like one step forward and two steps back. I'm hoping someone can help me.
I'm working with Sprite Kit so I have a Scene file that manages the rendering, UI and touch controls. I have an SKNode thats functioning as the camera like so:
_world = [[SKNode alloc] init];
[_world setName:#"world"];
[self addChild:_world];
I am using UIGestureRecognizer, so I add the ones I need like so:
_panRecognizer = [[UIPanGestureRecognizer alloc]initWithTarget:self action:#selector(handlePanFrom:)];
[[self view] addGestureRecognizer:_panRecognizer];
_pinchRecognizer = [[UIPinchGestureRecognizer alloc]initWithTarget:self action:#selector(handlePinch:)];
[[self view] addGestureRecognizer:_pinchRecognizer];
The panning is working okay, but not great. The pinching is the real problem. The idea for the pinching is to grab a point at the center of the screen, convert that point to the world node, and then move to it while zooming in. Here is the method for pinching:
-(void) handlePinch:(UIPinchGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateBegan) {
_tempScale = [sender scale];
}
if (sender.state == UIGestureRecognizerStateChanged) {
if([sender scale] > _tempScale) {
if (_world.xScale < 6) {
//_world.xScale += 0.05;
//_world.yScale += 0.05;
//[_world setScale:[sender scale]];
[_world setScale:_world.xScale += 0.05];
CGPoint screenCenter = CGPointMake(_initialScreenSize.width/2, _initialScreenSize.height/2);
CGPoint newWorldPoint = [self convertTouchPointToWorld:screenCenter];
//crazy method why does this work
CGPoint alteredWorldCenter = CGPointMake(((newWorldPoint.x*_world.xScale)*-1), (newWorldPoint.y*_world.yScale)*-1);
//why does the duration have to be exactly 0.3 to work
SKAction *moveToCenter = [SKAction moveTo:alteredWorldCenter duration:0.3];
[_world runAction:moveToCenter];
}
} else if ([sender scale] < _tempScale) {
if (_world.xScale > 0.5 && _world.xScale > 0.3){
//_world.xScale -= 0.05;
//_world.yScale -= 0.05;
//[_world setScale:[sender scale]];
[_world setScale:_world.xScale -= 0.05];
CGPoint screenCenter = CGPointMake(_initialScreenSize.width/2, _initialScreenSize.height/2);
CGPoint newWorldPoint = [self convertTouchPointToWorld:screenCenter];
//crazy method why does this work
CGPoint alteredWorldCenter = CGPointMake(((newWorldPoint.x*_world.xScale - _initialScreenSize.width)*-1), (newWorldPoint.y*_world.yScale - _initialScreenSize.height)*-1);
SKAction *moveToCenter = [SKAction moveTo:alteredWorldCenter duration:0.3];
[_world runAction:moveToCenter];
}
}
}
if (sender.state == UIGestureRecognizerStateEnded) {
[_world removeAllActions];
}
}
I've tried many iterations of this, but this exact code is what is getting me the closest to pinching on a point in the world. There are some problems though. As you get further out from the center, it doesn't work as well, as it pretty much still tries to zoom in on the very center of the world. After converting the center point to the world node, I still need to manipulate it again to get it centered properly (the formula I describe as crazy). And it has to be different for zooming in and zooming out to work. The duration of the move action has to be set to 0.3 or it pretty much won't work at all. Higher or lower and it doesn't zoom in on the center point. If I try to increment the zoom by more than a small amount, it moves crazy fast. If I don't end the actions when the pinch ends, the screen jerks. I don't understand why this works at all (it smoothly zooms in to the center point before the delay ends and the screen jerks) and I'm not sure what I'm doing wrong. Any help is much appreciated!
Take a look at my answer to a very similar question.
https://stackoverflow.com/a/21947549/3148272
The code I posted "anchors" the zoom at the location of the pinch gesture instead of the center of the screen, but that is easy to change as I tried it both ways.
As requested in the comments below, I am also adding my panning code to this answer.
Panning Code...
// instance variables of MyScene.
SKNode *_mySkNode;
UIPanGestureRecognizer *_panGestureRecognizer;
- (void)didMoveToView:(SKView *)view
{
_panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePanFrom:)];
[[self view] addGestureRecognizer:_panGestureRecognizer];
}
- (void)handlePanFrom:(UIPanGestureRecognizer *)recognizer
{
if (recognizer.state == UIGestureRecognizerStateBegan) {
[recognizer setTranslation:CGPointZero inView:recognizer.view];
} else if (recognizer.state == UIGestureRecognizerStateChanged) {
CGPoint translation = [recognizer translationInView:recognizer.view];
translation = CGPointMake(-translation.x, translation.y);
_mySkNode.position = CGPointSubtract(_mySkNode.position, translation);
[recognizer setTranslation:CGPointZero inView:recognizer.view];
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
// No code needed for panning.
}
}
The following are the two helper functions that were used above. They are from the Ray Wenderlich book on Sprite Kit.
SKT_INLINE CGPoint CGPointAdd(CGPoint point1, CGPoint point2) {
return CGPointMake(point1.x + point2.x, point1.y + point2.y);
}
SKT_INLINE CGPoint CGPointSubtract(CGPoint point1, CGPoint point2) {
return CGPointMake(point1.x - point2.x, point1.y - point2.y);
}
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];
}
}
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
)
I have a UIPanGestureRecognizer that is attached to a UIView. What I am trying to do is essentially drag and drop into a bucket. If the user lets go of the UIView and it's not in the bucket, I want it to animate back to its original start location. My selector that gets called for my UIPanGestureRecognizer is:
- (void)handleDragDescriptionView:(UIPanGestureRecognizer *)gestureRecognizer {
UIView *panViewPiece = [gestureRecognizer view];
CGRect originalFrame = CGRectMake(946, 20, 58, 30);
CGPoint translation = [gestureRecognizer translationInView:panViewPiece];
if (gestureRecognizer.state == UIGestureRecognizerStateBegan || gestureRecognizer.state == UIGestureRecognizerStateChanged) {
panViewPiece.center = CGPointMake(panViewPiece.center.x + translation.x, panViewPiece.center.y + translation.y);
[gestureRecognizer setTranslation:CGPointZero inView:panViewPiece.superview];
}
else if (gestureRecognizer.state == UIGestureRecognizerStateCancelled) {
[UIView animateWithDuration:0.25 animations:^{
self.dragDescriptionView.frame = originalFrame;
}];
}
else {
[UIView animateWithDuration:0.25 animations:^{
self.dragDescriptionView.frame = originalFrame;
}];
}
}
I was wondering if I could do this without the originalFrame at the top of the method. I originally had
CGRect originalFrame = _dragDescriptionView.frame;
instead of the hardcoding, but it doesn't snap back. Probably because I'm updating that value as I am dragging. I don't particularly like hardcoding values, but I wasn't sure if there was a way around this. Thanks!
You really only need to track the view's original center. Make originalCenter an instance variable and only set it when the gesture begins:
#implementation MyViewController {
CGPoint _originalCenter;
}
- (void)handleDragDescriptionView:(UIPanGestureRecognizer *)gestureRecognizer {
UIView *panViewPiece = [gestureRecognizer view];
switch (gestureRecognizer.state) {
case UIGestureRecognizerStateBegan:
_originalCenter = panViewPiece.center;
break;
case UIGestureRecognizerStateChanged: {
CGPoint translation = [gestureRecognizer translationInView:panViewPiece.superview];
panViewPiece.center = CGPointMake(panViewPiece.center.x + translation.x, panViewPiece.center.y + translation.y);
[gestureRecognizer setTranslation:CGPointZero inView:panViewPiece.superview];
break;
}
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled: {
[UIView animateWithDuration:0.25 animations:^{
panViewPiece.center = _originalCenter;
}];
break;
}
default:
break;
}
}