iOS UIPanGestureRecognizer: adjust sensitivity?
Asked Answered
C

2

6

My question: Is there a way to adjust the "sensitivity" of UIPanGestureRecognizer so that it turns on 'sooner', i.e. after moving a fewer number of 'pixels'?

I have a simple app with a UIImageView, and pinch and pan gesture recognizers tied to this so that the user can zoom in and draw on the image by hand. Works fine.

However, I notice the stock UIPanGestureRecognizer doesn't return a value of UIGestureRecognizerState.Changed until the user's gesture has moved about 10 pixels.

Example: Here's a screenshot showing several lines that I've attempted to draw shorter & shorter, and there is a noticeable finite length below which no line gets drawn because the pan gesture recognizer never changes state.

IllustrationOfProgressivelyShorterLines.png

...i.e., to the right of the yellow line, I was still trying to draw, and my touches were being recognized as touchesMoved events, but the UIPanGestureRecognizer wasn't firing its own "Moved" event and thus nothing was getting drawn.

(Note/clarification: That image takes up the entirety of my iPad's screen, so my finger is physically moving more than an inch even in the cases where no state change occurs to the recognizer. It's just that we're 'zoomed in' in terms of the tranformation generated by the pinch gesture recognizer, so a few 'pixels' of the image take up a significant amount of the screen.)

This is not what I want. Any ideas on how to fix it?

Maybe some 'internal' parameter of UIPanGestureRecognizer I could get at if I sub-classed it or some such? I thought I'd try to sub-class the recognizer in a manner such as...

class BetterPanGestureRecognizer: UIPanGestureRecognizer {

    var initialTouchLocation: CGPoint!

    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent) {
        super.touchesBegan(touches, withEvent: event)
        initialTouchLocation = touches.first!.locationInView(view)
        print("pan: touch begin detected")
        print(self.state.hashValue)  // this lets me check the state
    }


    override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent) {
        super.touchesMoved(touches, withEvent: event)
        print("pan: touch move detected")
        print(self.state.hashValue)  // this remains at the "began" value until you get beyond about 10 pixels
        let some_criterion =  (touches.first!.isEqual(something) && event.isEqual(somethingElse))
        if (some_criterion) {
            self.state = UIGestureRecognizerState.Changed
       }
    }

}

...but I'm not sure what to use for some_criterion, etc.
Any suggestions?

.

Other alternatives that could work, but that I'd rather not have to do:

  1. I could simply attach my UIPanGestureRecognizer to some parent, non-zoomed view, and then use affine transforms & such to remap the points of the pan touches onto the respective parts of the image. So why am I not doing that? Because the code is written so that lots of other objects hang off the image view and they all get the same gesture recognizers and....everything works just great without my having keep track of anything (e.g. affine transformations), and the problem only shows up if you're really-really zoomed in.
  2. I could abandon UIPanGestureRecognizer, and effectively just write my own using touchesBegan and touchesMoved (which is kind of what I'm doing), however I like how UIPanGestureRecognizer differentiates itself from, say, pinch events, in a way that I don't have to worry about coding up myself.
  3. I could just specify some maximum zoom beyond which the user can't go. This fails to implement what I'm going for, i.e. I want to allow for fine-detail level of manipulation.

Thanks.

Crisper answered 21/8, 2015 at 0:24 Comment(2)
It's not hard to get it to turn on "too often", e.g. one can use let some_criterion = ( (abs(self.translationInView(view).x) > threshold) && (abs(self.translationInView(view).y) > threshold) ), where threshold is some number. The trick is having it NOT 'steal' events that should rightly be going to the pinch recognizer.Crisper
I'd prefer one of your answers to this "quick hack" I found: One could set self.minimumNumberOfTouches = 1 and self.maximumNumberOfTouches = 1 and then just set the state of the recognizer to Changed anytime there's a touchesMoved event. This works, in the sense that it's no longer 'stealing' events from the Pinch recognizer. However, I'd like to allow for two-finger panning and so this hack won't work for that.Crisper
C
3

[Will choose your answer over mine (i.e., the following) if merited, so I won't 'accept' this answer just yet.]

Got it. The basic idea of the solution is to change the state whenever touches are moved, but use the delegate method regarding simultaneous gesture recognizers so as not to "lock" out any pinch (or rotation) gesture. This will allow for one- and/or multi-fingered panning, as you like, with no 'conflicts'.

This, then, is my code:

class BetterPanGestureRecognizer: UIPanGestureRecognizer, UIGestureRecognizerDelegate {

    var initialTouchLocation: CGPoint!

    override init(target: AnyObject?, action: Selector) {
        super.init(target: target, action: action)
        self.delegate = self
    }

    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent) {
        super.touchesBegan(touches, withEvent: event)
        initialTouchLocation = touches.first!.locationInView(view)
    }

    override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent) {
        super.touchesMoved(touches, withEvent: event)
        if UIGestureRecognizerState.Possible == self.state {
           self.state = UIGestureRecognizerState.Changed
        }
    }

    func gestureRecognizer(_: UIGestureRecognizer,
        shouldRecognizeSimultaneouslyWithGestureRecognizer:UIGestureRecognizer) -> Bool {
            if !(shouldRecognizeSimultaneouslyWithGestureRecognizer is UIPanGestureRecognizer) {
                return true
            } else {
                return false
            }
    }

}

Generally setting that "shouldRecognizeSimultaneouslyWithGestureRecognizer" delegate to true always is what many people may want. I make the delegate return false if the other recognizer is another Pan, just because I was noticing that without that logic (i.e., and making the delegate return true no matter what), it was "passing through" Pan gestures to underlying views and I didn't want that. You may just want to have it return true no matter what. Cheers.

Crisper answered 26/8, 2015 at 11:2 Comment(2)
This looks like a pragmatic solution to the problem. Unfortunately, Apple has moved the touchesBegan() and touchesMoved() functions out of the header files so you can no longer override them :(. It makes sense from Apple's perspective (those functions are implementation details and we should only see the interface), but that's not much of a consolation.Decamp
touchesBegan() and touchesMoved() are still available for public usage.Diacritical
P
2

Swift 5 + small improvement

I had a case when accepted solution conflicted with basic taps on toolbar which also had this betterPanGesture so I added minimum horizontal offset parameter to trigger state changing to .changed

class BetterPanGestureRecognizer: UIPanGestureRecognizer {

    private var initialTouchLocation: CGPoint?
    private let minHorizontalOffset: CGFloat = 5

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesBegan(touches, with: event)
        self.initialTouchLocation = touches.first?.location(in: self.view)
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesMoved(touches, with: event)
    
        if self.state == .possible,
           abs((touches.first?.location(in: self.view).x ?? 0) - (self.initialTouchLocation?.x ?? 0)) >= self.minHorizontalOffset {
            self.state = .changed
        }
    }
}
Plage answered 13/8, 2021 at 8:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.