2

Note: I have changed the original title of this question. Initially the title was "How to use several GestureDetector instances at once?", but I have found that the problem is related not to several detectors but to the hierarchy of views. The rest of the question is kept as is, I have decided not to delete anything, just read it through. Please advice if I should throw off the obsolete parts, I'm ready to do that but not sure if I should.

I have one view (button) inside another one (container), and I want them to do two things: 1) If user scrolled any view (tapped and moved), the whole container is scrolled. 2) if user clicked the button, the click handler is called for the button.

My container is not a simple view, it allows scrolling in both directions (here are the details), so I have set up a GestureDetector that detects scrolling events and scrolls the container, thanks 323go for his advice. This detector is called from onTouch method of the OnTouchListener attached to the main container view.

My problem is the button.

If I set the onClick handler, it seems to sink all motion events completely, so I cannot scroll the view if I began scrolling on the button.

OK, perhaps I could set another GestureDetector on the button so it would handle the clicks and the scrolling?

Unfortunately that doesn't work. The onScroll handler of the button receives crazy values in distanceX and distanceY parameters. It looks like both the container view and the button have their own scroll counters, so when I scroll the view, some offset is accumulated for the button.

So my question is: what is the intended way of using the GestureDetector class? Can I use several detectors in a hierarchy of views, and is it possible to delegate handling from one detector to another? I. e., a parent detector could handle the event if the child one returned false.

Added: in fact, I'd be happy with the simple "parent to child" solution like this: there is the only GestureDetector attached to the topmost view in the hierarchy, it receives all events and decides how to handle them, so if it detected scrolling—it simply scrolls the whole view and doesn't notify anyone, but if it detected a click—it finds the child view below the click position and forwards the click to it. But is that a true Android way? And is that even possible?

Further investigation: I've tried to follow the advice given in comments below and made my child gesture detector to return false from its onScroll method. That hadn't helped: if the child handler returned false, the parent one isn't called at all.

But I have noticed that if the child handler doesn't try to scroll the parent view, it will receive correct values.

Here is the code of my handlers:

private class ParentHandler extends GestureDetector.SimpleOnGestureListener {
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        Log.d("UI", "Parent Scroll X: " + String.valueOf(distanceX) + " Y: " + String.valueOf(distanceY));
        scrollBy((int)distanceX, (int)distanceY);
        return true;
    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {return true;}

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {return true;}

    @Override
    public boolean onDown(MotionEvent e) {return true;}
};

private class ChildHandler extends GestureDetector.SimpleOnGestureListener {
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        Log.d("UI", "Child Scroll X: " + String.valueOf(distanceX) + " Y: " + String.valueOf(distanceY));
        // The line below drives the handler crazy!
        // But if I remove it, the view will not be scrolled at all
        // because the parent handler is not called
        // even if this handler returned false.
        scrollBy((int)distanceX, (int)distanceY);
        return false;
    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {return true;}

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {return true;}

    @Override
    public boolean onDown(MotionEvent e) {return true;}
};

Note the comment in the second handler.

Here is what I get in logs if the crazy call is removed:

Parent Scroll X: -1.0 Y: 2.0
Parent Scroll X: 0.0 Y: 2.0
Parent Scroll X: -1.0 Y: 1.0
Parent Scroll X: -1.0 Y: 2.0
Parent Scroll X: 0.0 Y: 2.0
...
Child Scroll X: -1.0 Y: 2.0
Child Scroll X: 0.0 Y: 2.0
Child Scroll X: -1.0 Y: 1.0
Child Scroll X: -1.0 Y: 2.0
Child Scroll X: 0.0 Y: 2.0

But as soon as I try to move the view in the child handler, the values turn into something like this:

Child Scroll X: 13.0 Y: 22.0
Child Scroll X: -11.0 Y: -15.9999695
Child Scroll X: 11.0 Y: 17.00003
Child Scroll X: -10.0 Y: -16.00003
Child Scroll X: 11.0 Y: 19.99997

Note that values are alternating: 13 is followed by -11, and 11 goes next, and then -10, and so on. So the average drift seems to be OK, but if I just pass these values to the scrollBy call, the view will jitter and jump here and there all the time I'm scrolling it.

Obviously there is some problem with calling the scrollBy within the onScroll fired for the child view. There is no problem if doing the same call from the parent handler.

What may be wrong there?

Added: I've removed the GestureDetector that is attached to the parent, but the behaviour is the same.

So the problem is actually as follows. I need to scroll the entire view when scrolling is detected by the GestureDetector. To scroll the view, I call scrollBy. This does work if the original onTouch event is fired for the parent view, but it doesn't if the onTouch is fired for the child view: in the latter case trying to scroll the parent view affects values that the GestureHandler receives.

So this works:

parent.onTouch -> detector.onTouchEvent -> parent.scrollBy

and this doesn't:

child.onTouch -> detector.onTouchEvent -> parent.scrollBy

where detector is the same.

Community
  • 1
  • 1
Alexander Dunaev
  • 980
  • 1
  • 15
  • 40

1 Answers1

2

You can create a Listener that forwards events to other listeners using the composite design pattern.

Take a look at these posts :

Attaching multiple listeners to views in android?

how can I set up multiple listeners for one event?

Community
  • 1
  • 1
Akos Cz
  • 12,711
  • 1
  • 37
  • 32
  • Perhaps I'm wrong but that doesn't seem to be relevant to my question. I don't need to handle a single event in several places (which should be easy if I wanted that). Instead, I need to pass the event through the hierarchy so the event is first handled in the child view and then passed to the parent if the child didn't need it. And even this seems to be working; the problem is that MotionEvent has some weird values when handled in the child view. In other words, my question is probably specific for GestureDetector. – Alexander Dunaev Jan 04 '13 at 12:11
  • if your child view did not handle the event, then returning false from the event handler will tell the parent that it needs to handle it. – Akos Cz Jan 04 '13 at 23:01
  • That doesn't help. The parent handler isn't called if the child one returned false. However the problem seems to be elsewhere, see my updated post (in the bottom). – Alexander Dunaev Jan 05 '13 at 05:34
  • Hmm.. you might be running into this bug : https://code.google.com/p/android/issues/detail?id=8233 – Akos Cz Jan 05 '13 at 22:29
  • Take a look at my code sample here : http://stackoverflow.com/questions/2089552/android-how-to-detect-when-a-scroll-has-ended/3818124#3818124 – Akos Cz Jan 05 '13 at 22:29
  • No, that's not the case. Both my handlers have onDown (and all other methods as well) implemented and returning true, see the example code I've quoted in my post above. You see, onScroll is called, but calling the parent's scrollBy affects the values that onScroll receives. I'll extend this idea in the question shortly. – Alexander Dunaev Jan 06 '13 at 16:54