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 UIScrollView
s 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
Android, Landscape Only Orientation
Setting Audio File as Ringtone
Noclassdeffounderror Below Sdk 21
Unexpected Namespace Prefix "Xmlns" Found for Tag Linearlayout
Getting Context in Androidtestcase or Instrumentationtestcase in Android Studio's Unit Test Feature
Passing JSONobject into Another Activity
Error Inflating Class Collapsingtoolbarlayout
How to Use Edittext Ontextchanged Event When I Press the Number
Auto Start Application After Boot Completed in Android
What Happens If a Android Service Is Started Multiple Times
Rotate Zoom Drag Image in Android Imageview
Android Imageview's Onclicklistener Does Not Work
Uploading Image from Android to Gcs
Highlighting the Selected Item in the Listview in Android
How to Download a PDF File in Android