Gesture Detection and Scrollview Issue

Gesture detection and ScrollView issue

I had to add

@Override
public boolean dispatchTouchEvent(MotionEvent ev){
super.dispatchTouchEvent(ev);
return productGestureDetector.onTouchEvent(ev);
}

in my Activity.

Android Right and Left Swipe Gestures handling

scrollView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:{
downX = event.getX();}
case MotionEvent.ACTION_UP:{
upX = event.getX();

float deltaX = downX - upX;

if(Math.abs(deltaX)>0){
if(deltaX>=0){
swipeToRight();
return true;
}else{
swipeToLeft();
return true;
}
}
}
}

return false;
}
});

How to have a UIScrollView scroll and have a gesture recognizer?

Make a subclass of UIScrollView. Add this method in your new subclass

- (BOOL)gestureRecognizer:(UIPanGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UISwipeGestureRecognizer *)otherGestureRecognizer 
{
return YES;
}

Make your scrollView class to your new scrollview subclass.

Multiple gestures on same view (drag view out of scrollable view) not working the right way?

Ok I solved this with a combination of a few things. It looks like a lot to add, but it will come in handy in other projects.

Step 1: Enable the horizontal scrolling UIScrollView to "fail" both horizontally and vertically.

Although I only want my UIScrollView to scroll horizontally, I still need it to scroll vertically so that it can fail (explanation coming). Failing enables me to "pull" the subviews out of the UIScrollView.

The height of scrollView itself is 40 so the contentSize of the it must have a larger height for it to be able to barely scroll vertically.

self.effectTotalHeight.constant = 41
self.scrollView.contentSize = CGSize(width: contentWidth, height: self.effectTotalHeight.constant)

Great! It scrolls vertically. But now the content rubber-bands (which we do not want). Against what others on SO say, do not go cheap and just disable bounce. (Especially if you want the bounce when scrolling horizontally!)

Note: I also realized only disabling Bounce Horizontally in StoryBoard does... well, nothing (bug?).

Step 2: Add UIScrollViewDelegate to your View Controller class and detect scrolls

Now I want to detect the scroll and make sure that when scrolling vertically, it does not actually scroll. To do this, the contentOffset.y position should not change even though you are scrolling. UIScrollViewDelegate provides a delegate function scrollViewDidScroll that gets called when scrolling. Just set it as:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollView.contentOffset.y = 0.0
}

In order for this to be called, you need to set the delegate of the scrollView to self as well:

self.scrollView.delegate = self

Keep in mind this controls all UIScrollViews so provide an if statement or switch statement if you want it to only affect specific ones. In my case, this view controller only has one UIScrollView so I did not put anything.

Yay! So now it only scrolls horizontally again but this method only keeps the contentOffset.y at 0. It does not make it fail. We do need it to because scrollView failing vertically is the key to enabling pan gesture recognizers (what lets you pull and drag etc.). So let's make it fail!

Step 3: Override UIScrollView default gesture recognition

In order to override any of the default gesture recognizers, we need to add UIGestureRecognizerDelegate as another delegate method to your View Controller class.

The scrollView now needs its own pan gesture recognizer handler so that we can detect gestures on it. You also need to set the delegate of the new scrollGesture to self so we can detect it:

let scrollGesture = UIPanGestureRecognizer(target: self, action: #selector(self.handleScroll(_:)))
scrollGesture.delegate = self
self.scrollView.addGestureRecognizer(scrollGesture)

Set up the handleScroll function:

func handleScroll(_ recognizer: UIPanGestureRecognizer) {
// code here
}

This is all good, but why did we set this all up? Remember we disabled the contentOffset.y but the vertical scroll was not failing. Now we need to detect the direction of the scroll and let it fail if vertical so that panHandle can be activated.

Step 4: Detect gesture direction and let it fail (failure is good!)

I extended the UIPanGestureRecognizer so that it can detect and emit directions by making the following public extension:

public enum Direction: Int {
case Up
case Down
case Left
case Right

public var isX: Bool { return self == .Left || self == .Right }
public var isY: Bool { return !isX }
}

public extension UIPanGestureRecognizer {
public var direction: Direction? {
let velo = velocity(in: view)
let vertical = fabs(velo.y) > fabs(velo.x)
switch (vertical, velo.x, velo.y) {
case (true, _, let y) where y < 0: return .Up
case (true, _, let y) where y > 0: return .Down
case (false, let x, _) where x > 0: return .Right
case (false, let x, _) where x < 0: return .Left
default: return nil
}
}
}

Now in order to use it correctly, you can get the recognizer's .direction inside of the handleScroll function and detect the emitted directions. In this case I am looking for either .Up or .Down and I want it to emit a fail. We do this by disabling the recognizer it, but then re-enabling it immediately after:

func handleScroll(_ recognizer: UIPanGestureRecognizer) {
if (recognizer.direction == .Up || recognizer.direction == .Down) {
recognizer.isEnabled = false
recognizer.isEnabled = true
}
}

The reason .isEnabled is immediately set to true right after false is because false will emit the fail, enabling the other gesture ("pulling out" (panning) its inner views), but to not be disabled forever (or else it will cease being called). By setting it back to true, it lets this listener be re-enabled right after emitting the fail.

Step 5: Let multiple gestures work by overriding each other

Last but not least, this is a very very important step as it allows both pan gestures and scroll gestures to work independently and not have one single one always override the other.

func gestureRecognizer(_: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith shouldRecognizeSimultaneouslyWithGestureRecognizer:UIGestureRecognizer) -> Bool {
return true
}

And that's that! This was a lot of research on SO (mostly finding what did not work) and experimentation but it all came together.

This was written with the expectation that you have already written the pan gesture recognizers of the objects inside of the scroll view that you are "pulling out" and how to handle its states (.ended, .changed etc.). I suggest if you need help with that, search SO. There are tons of answers.

If you have any other questions about this answer, please let me know.

Using GestureLibrary with ScrollView

All touch events in android goes from children to parents. So, scroll view handle all touch events, and you gesture detector does not receives them. You can set touch listener on ScrollView:

    scrollView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (detector != null) {
return detector.onTouchEvent(event);
return false;
}
});

You have to check that scroll works as necessary, and you don't consumed scroll events.



Related Topics



Leave a reply



Submit