if (gesture.state == UIGestureRecognizerStateBegan) {
_initial = [gesture locationInView:self.view];
}else if (gesture.state == UIGestureRecognizerStateChanged){
CGPoint p = [gesture locationInView:self.view];
double dy = p.y - _initial.y;
if (dy > 0) {
NSLog(#"Finger moved to the up");
}
else {
NSLog(#"Finger moved to the down");
}
}
This is my method to detect UILongPressGestureRecognizer direction, but I want
detect direction not compare initial point but compare stateChanged point.
It's hard to describe, like : form 0 to 7 is up, but 7 to -5 is down and then -5 to -2 is up.
You should use translationInView.
CGPoint translation = [gesture translationInView:view.superview];
if (translation.y>0) {
// moving up
} else {
// moving down
}
A friend of mine give me some advice and it works well:
CGPoint point = [gestureRecognizer locationInView:self.view];
if(gestureRecognizer.state == UIGestureRecognizerStateBegan){
_lastY = point.y;
}else if(gestureRecognizer.state == UIGestureRecognizerStateChanged) {
double y = point.y - _lastY;
if (y < 0.f) {
NSLog(#"****up*****");
}else{
NSLog(#"****down*****");
}
_lastY = point.y;
}
I know it's not a good one but it works, any idea?
Swift 3
Here's a complete function to determine left or right movement of UILongPressGestureRecognizer:
#objc func swipePress (_ sender: UILongPressGestureRecognizer) {
let location = sender.location(in: self.view)
print ("Location : \(location.x)")
if sender.state == .ended {
print("swipe ended")
buttonCreated = false
animateRemoveArrows()
if gestureDirection == "right" {
print("going forward")
if myWebView.canGoForward {
myWebView.goForward() //go forward
} else {
}
} else if gestureDirection == "left" {
print("going backward")
if myWebView.canGoBack {
myWebView.goBack() //go back
} else {
}
} else {
}
} else if sender.state == .began {
gestureDirection = "" // reset direction
print("swipe started")
firstPoisitionX = location.x
firstPoisitionY = location.y
print ("Last x: \(firstPoisitionX)")
} else if sender.state == .changed {
if location.y > (firstPoisitionY + 100) {
print("going up or down")
} else if location.y < (firstPoisitionY - 100) {
print("going up or down")
} else if location.x > (firstPoisitionX + 50) {
print("going right")
if buttonCreated == false { // only create one button
showArrow(direction: "right")
print(rightArrow.center)
gestureDirection = "right"
buttonCreated = true
}
} else if location.x < (firstPoisitionX - 50) {
print("going left")
if buttonCreated == false { // only create one button
showArrow(direction: "left")
print(leftArrow.center)
gestureDirection = "left"
buttonCreated = true
}
}
}
}
Related
I'm doing a page view controller with a photo album, and since I haven't been able to find a way to pan the image zoomed without changing the picture, I want the image to return to its original size when the user stops pinching.
This is my code:
#objc func pinch(sender:UIPinchGestureRecognizer) {
if sender.state == .began || sender.state == .changed {
let currentScale = self.contentImageView.frame.size.width / self.contentImageView.bounds.size.width
let newScale = currentScale*sender.scale
let transform = CGAffineTransform(scaleX: newScale, y: newScale)
self.contentImageView.transform = transform
sender.scale = 1
if sender.state == .ended {
contentImageView.transform = CGAffineTransform.identity
}
}
}
The zooming part works fine, but when I release the pinch it doesn't do anything, any suggestions would be very appreciated.
From your code, you never get reset:
if sender.state == .ended {
self.contentImageView.transform = CGAffineTransform.identity
}
Because you try to validate it inside of your first if. Try to change it like this:
if sender.state == .began || sender.state == .changed {
...
} else if sender.state == .ended {
self.contentImageView.transform = CGAffineTransform.identity
}
func handlePanGesture (panGesture: UIPanGestureRecognizer) {
let translation = panGesture.translation(in: panGesture.view?.superview)
if panGesture.state == .began || panGesture.state == .changed {
panGesture.view?.center = CGPoint(x: (panGesture.view?.center.x)! + translation.x, y: (panGesture.view?.center.y)! + translation.y)
let distance: Double = sqrt(pow((Double(panGesture.view!.center.x) - Double(poin1.x)),2) + pow((Double(panGesture.view!.center.y) - Double(poin1.y)),2))
if distance <= 50{
panGesture.view!.center.x = CGFloat(poin1.x)
panGesture.view!.center.y = CGFloat(poin1.y)
}
panGesture.setTranslation(CGPoint.zero, in: self.view)
}
}
I want custom func handlePanGesture using many points to move in the gesture.
I'm trying to lock my image I'm dragging (objectDragging) to the x or y center, depending which way I'm moving or influencing it to move. If I'm influencing it to the right, I want it to lock on its initial Y position, and only allow it to go right (or back left, on a straight line). If I'm influencing it upward, lock its x position and allow it to move straight up or down on the exact line. I'm using a switch for my movement handling, with lots of code in the .end case, so I didn't want to include a lot of code. Just hoping for an idea — wasn't able to find a lot.
switch(recognizer.state) {
objectDragging = (recognizer.view?.tag)!
case .began:
initPosx = Int(recognizer.view!.center.x)
initPosy = Int(recognizer.view!.center.y)
case .changed:
...
After the suggestion of using velocity from Paulw11 I've had success limiting the direction of the image I'm moving by deciding which way we are initially moving the image (in .began) and then locking the x or y (in .changed). Incase anyone comes across this with a similar issue, heres the gist of my solution that is working well for my application:
switch(recognizer.state) {
case .began:
let velocity = recognizer.velocity(in: self.view)
if (velocity.x > velocity.y) && (velocity.x > 0) {
print("moving right")
myBlocks[objectDragging].center.y = CGFloat(initPosy)
movingRight = true
}
else if ((abs(velocity.x)) > velocity.y) && (velocity.x < 0) {
print("moving left")
myBlocks[objectDragging].center.y = CGFloat(initPosy)
movingLeft = true
}
else if (velocity.y > velocity.x) && (velocity.y > 0) {
print("moving down")
myBlocks[objectDragging].center.x = CGFloat(initPosx)
movingDown = true
}
else if ((abs(velocity.y)) > velocity.x) && (velocity.y < 0){
print("moving up")
myBlocks[objectDragging].center.x = CGFloat(initPosx)
print(abs(velocity.y))
movingUp = true
}
case .changed:
if (movingRight == true) {
myBlocks[objectDragging].center.y = CGFloat(initPosy)
}
else if (movingLeft == true) {
myBlocks[objectDragging].center.y = CGFloat(initPosy)
}
else if (movingDown == true) {
myBlocks[objectDragging].center.x = CGFloat(initPosx)
}
else if (movingUp == true) {
myBlocks[objectDragging].center.x = CGFloat(initPosx)
}
case .ended:
movingUp = false
movingLeft = false
movingRight = false
movingDown = false
I'm building a slide out menu using a tutorial from Ray Wenderlich and can't figure out how to disable left to right panning but keep right to left? I remove addLeftPanelViewController() but it still shows the pan, just not working correctly. I don't want anything to move, or unhide on the left to right.
Thanks for any help.
override func viewDidLoad() {
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: "handlePanGesture:")
centerNavigationController.view.addGestureRecognizer(panGestureRecognizer)
}
let gestureIsDraggingFromLeftToRight = (recognizer.velocityInView(view).x > 0)
switch(recognizer.state) {
case .Began:
if (currentState == .BothCollapsed) {
if (gestureIsDraggingFromLeftToRight) {
addLeftPanelViewController()
} else {
addRightPanelViewController()
}
showShadowForCenterViewController(true)
}
case .Changed:
recognizer.view!.center.x = recognizer.view!.center.x + recognizer.translationInView(view).x
recognizer.setTranslation(CGPointZero, inView: view)
case .Ended:
if (leftViewController != nil) {
// animate the side panel open or closed based on whether the view has moved more or less than halfway
let hasMovedGreaterThanHalfway = recognizer.view!.center.x > view.bounds.size.width
animateLeftPanel(shouldExpand: hasMovedGreaterThanHalfway)
} else if (rightViewController != nil) {
let hasMovedGreaterThanHalfway = recognizer.view!.center.x < 0
animateRightPanel(shouldExpand: hasMovedGreaterThanHalfway)
}
default:
break
}
try this
add Expending on the SlideOutState enum
enum SlideOutState {
case BothCollapsed
case SettingPanelExpanded
case Expanding
}
then
switch(recognizer.state) {
case .Began:
if (currentState == .BothCollapsed) {
if (gestureIsDraggingFromLeftToRight) {
addSettingPanelViewController()
showShadowForCenterViewController(true)
currentState = .Expanding
}
}
if (currentState == .SettingPanelExpanded) {
if (gestureIsDraggingFromLeftToRight==false) {
currentState = .Expanding
}
}
case .Changed:
if (currentState == .Expanding){
println(recognizer.velocityInView(view).x)
recognizer.view!.center.x = recognizer.view!.center.x + recognizer.translationInView(view).x
recognizer.setTranslation(CGPointZero, inView: view)
}
case .Ended:
if (settingViewController != nil) {
// animate the side panel open or closed based on whether the view has moved more or less than halfway
let hasMovedGreaterThanHalfway = recognizer.view!.center.x > view.bounds.size.width
animateSettingPanel(shouldExpand: hasMovedGreaterThanHalfway)
}
default:
break
}
Wrap your switch statement in an if condition:
if currentState == .LeftPanelExpanded || gestureIsDraggingFromLeftToRight {
//your existing switch statement
}
I am trying to move a sub view across the screen which works, but i also want to add inertia or momentum to the object.
My UIPanGestureRecognizer code that i already have is below.
Thanks in advance.
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
[self addGestureRecognizer:panGesture];
(void)handlePan:(UIPanGestureRecognizer *)recognizer
{
CGPoint translation = [recognizer translationInView:self.superview];
recognizer.view.center = CGPointMake(recognizer.view.center.x + translation.x,
recognizer.view.center.y + translation.y);
[recognizer setTranslation:CGPointMake(0, 0) inView:self.superview];
if (recognizer.state == UIGestureRecognizerStateEnded) {
[self.delegate card:self.tag movedTo:self.frame.origin];
}
}
Again thanks.
Have a look at RotationWheelAndDecelerationBehaviour. there is an example for how to do the deceleration for both linear panning and rotational movement. Trick is to see what is the velocity when user ends the touch and continue in that direction with a small deceleration.
Well, I'm not a pro but, checking multiple answers, I managed to make my own code with which I am happy.
Please tell me how to improve it and if there are any bad practices I used.
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
CGPoint translatedPoint = [recognizer translationInView:self.postViewContainer];
CGPoint velocity = [recognizer velocityInView:recognizer.view];
float bottomMargin = self.view.frame.size.height - containerViewHeight;
float topMargin = self.view.frame.size.height - scrollViewHeight;
if ([recognizer state] == UIGestureRecognizerStateChanged) {
newYOrigin = self.postViewContainer.frame.origin.y + translatedPoint.y;
if (newYOrigin <= bottomMargin && newYOrigin >= topMargin) {
self.postViewContainer.center = CGPointMake(self.postViewContainer.center.x, self.postViewContainer.center.y + translatedPoint.y);
}
[recognizer setTranslation:CGPointMake(0, 0) inView:self.postViewContainer];
}
if ([recognizer state] == UIGestureRecognizerStateEnded) {
__block float newYAnimatedOrigin = self.postViewContainer.frame.origin.y + (velocity.y / 2.5);
if (newYAnimatedOrigin <= bottomMargin && newYAnimatedOrigin >= topMargin) {
[UIView animateWithDuration:1.2 delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^ {
self.postViewContainer.center = CGPointMake(self.postViewContainer.center.x, self.postViewContainer.center.y + (velocity.y / 2.5));
}
completion:^(BOOL finished) {
[self.postViewContainer setFrame:CGRectMake(0, newYAnimatedOrigin, self.view.frame.size.width, self.view.frame.size.height - newYAnimatedOrigin)];
}
];
}
else {
[UIView animateWithDuration:0.6 delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^ {
if (newYAnimatedOrigin > bottomMargin) {
self.postViewContainer.center = CGPointMake(self.postViewContainer.center.x, bottomMargin + self.postViewContainer.frame.size.height / 2);
}
if (newYAnimatedOrigin < topMargin) {
self.postViewContainer.center = CGPointMake(self.postViewContainer.center.x, topMargin + self.postViewContainer.frame.size.height / 2);
}
}
completion:^(BOOL finished) {
if (newYAnimatedOrigin > bottomMargin)
[self.postViewContainer setFrame:CGRectMake(0, bottomMargin, self.view.frame.size.width, scrollViewHeight)];
if (newYAnimatedOrigin < topMargin)
[self.postViewContainer setFrame:CGRectMake(0, topMargin, self.view.frame.size.width, scrollViewHeight)];
}
];
}
}
}
I have used two different animation, one is the default inertia one and the other if for when the user flings the containerView with high velocity.
It works well under iOS 7.
I took the inspiration from the accepted answer's implementation. Here is a Swift 5.1 version.
Logic:
You need to calculate the angle changes with the velocity at which the pan gesture ended and keep rotating the wheel in an endless timer until the velocity wears down because of deceleration rate.
Keep decreasing the current velocity in every iteration of the timer
with some factor (say, 0.9).
Keep a lower limit on the velocity to
invalidate the timer and complete the deceleration process.
Main function used to calculate deceleration:
// deceleration behaviour constants (change these for different deceleration rates)
private let timerDuration = 0.025
private let decelerationSmoothness = 0.9
private let velocityToAngleConversion = 0.0025
private func animateWithInertia(velocity: Double) {
_ = Timer.scheduledTimer(withTimeInterval: self.timerDuration, repeats: true) { [weak self] timer in
guard let this = self else {
return
}
let concernedVelocity = this.currentVelocity == 0.0 ? velocity : this.currentVelocity
let newVelocity = concernedVelocity * this.decelerationSmoothness
this.currentVelocity = newVelocity
var angleTraversed = newVelocity * this.velocityToAngleConversion * this.maximumRotationAngleInCircle
if !this.isClockwiseRotation {
angleTraversed *= -1
}
// exit condition
if newVelocity < 0.1 {
timer.invalidate()
this.currentVelocity = 0.0
} else {
this.traverseAngularDistance(angle: angleTraversed)
}
}
}
Full working code with helper functions, extensions and usage of aforementioned function in the handlePanGesture function.
// deceleration behaviour constants (change these for different deceleration rates)
private let timerDuration = 0.025
private let decelerationSmoothness = 0.9
private let velocityToAngleConversion = 0.0025
private let maximumRotationAngleInCircle = 360.0
private var currentRotationDegrees: Double = 0.0 {
didSet {
if self.currentRotationDegrees > self.maximumRotationAngleInCircle {
self.currentRotationDegrees = 0
}
if self.currentRotationDegrees < -self.maximumRotationAngleInCircle {
self.currentRotationDegrees = 0
}
}
}
private var previousLocation = CGPoint.zero
private var currentLocation = CGPoint.zero
private var velocity: Double {
let xFactor = self.currentLocation.x - self.previousLocation.x
let yFactor = self.currentLocation.y - self.previousLocation.y
return Double(sqrt((xFactor * xFactor) + (yFactor * yFactor)))
}
private var currentVelocity = 0.0
private var isClockwiseRotation = false
#objc private func handlePanGesture(panGesture: UIPanGestureRecognizer) {
let location = panGesture.location(in: self)
if let rotation = panGesture.rotation {
self.isClockwiseRotation = rotation > 0
let angle = Double(rotation).degrees
self.currentRotationDegrees += angle
self.rotate(angle: angle)
}
switch panGesture.state {
case .began, .changed:
self.previousLocation = location
case .ended:
self.currentLocation = location
self.animateWithInertia(velocity: self.velocity)
default:
print("Fatal State")
}
}
private func animateWithInertia(velocity: Double) {
_ = Timer.scheduledTimer(withTimeInterval: self.timerDuration, repeats: true) { [weak self] timer in
guard let this = self else {
return
}
let concernedVelocity = this.currentVelocity == 0.0 ? velocity : this.currentVelocity
let newVelocity = concernedVelocity * this.decelerationSmoothness
this.currentVelocity = newVelocity
var angleTraversed = newVelocity * this.velocityToAngleConversion * this.maximumRotationAngleInCircle
if !this.isClockwiseRotation {
angleTraversed *= -1
}
if newVelocity < 0.1 {
timer.invalidate()
this.currentVelocity = 0.0
this.selectAtIndexPath(indexPath: this.nearestIndexPath, shouldTransformToIdentity: true)
} else {
this.traverseAngularDistance(angle: angleTraversed)
}
}
}
private func traverseAngularDistance(angle: Double) {
// keep the angle in -360.0 to 360.0 range
let times = Double(Int(angle / self.maximumRotationAngleInCircle))
var newAngle = angle - times * self.maximumRotationAngleInCircle
if newAngle < -self.maximumRotationAngleInCircle {
newAngle += self.maximumRotationAngleInCircle
}
self.currentRotationDegrees += newAngle
self.rotate(angle: newAngle)
}
Extensions being used in above code:
extension UIView {
func rotate(angle: Double) {
self.transform = self.transform.rotated(by: CGFloat(angle.radians))
}
}
extension Double {
var radians: Double {
return (self * Double.pi)/180
}
var degrees: Double {
return (self * 180)/Double.pi
}
}