Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Weird onScroll event triggered after onScale event

I have an app that uses a SimpleOnScaleGestureListener and a SimpleOnGestureListener together. Whenever I do a pinch zoom I get the expected onScale's, but when I lift off I see a weird onScroll that has a start position from the beginning of the pinch zoom and an end position from the end of the pinch zoom. My question is, can I prevent the bogus onScroll?

Here's the code:

@Override
public boolean onTouchEvent(MotionEvent event) {

    // Log every event.
    Log.d(TAG, Here.at() + String.format("Event: %d, Time: %d X: %f, Y: %f", 
            event.getAction(), 
            event.getEventTime(),
            event.getX(),
            event.getY()
            ));

    boolean handled = mScaleDetector.onTouchEvent(event); // This appears to ALWAYS return true (online reference indicated that's what the Android code does).

    handled |= mDetector.onTouchEvent(event);

    handled |= super.onTouchEvent(event);

    return handled;
}

private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        // This is required.  If absent, the scale gesture never starts.
        Log.d(TAG, "In onScaleBegin");
        mIgnoreNextDrag = true;
        return true;
    }

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        Log.d(TAG, "In onScale");
        mTimeScale.doScale(detector.getScaleFactor(), detector.getFocusY());
        invalidate();
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {
        Log.d(TAG, "In onScaleEnd");
    }

}

private class GestureListener extends GestureDetector.SimpleOnGestureListener {

    @Override
    public boolean onScroll(MotionEvent me1, MotionEvent me2, float distanceX, float distanceY) {

        Log.d(TAG, String.format("Motion Event 1: %d, Time: %d X: %f, Y: %f", 
                me1.getAction(), 
                me1.getEventTime(),
                me1.getX(),
                me1.getY()
                ));

        Log.d(TAG, String.format("Event 2: %d, Time: %d X: %f, Y: %f", 
                me2.getAction(), 
                me2.getEventTime(),
                me2.getX(),
                me2.getY()
                ));


        return true;

    }

    @Override
    public boolean onSingleTapConfirmed(MotionEvent me) {
        // Do tap processing.
        return true;
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        // TODO: Future feature.
        return true;
    }

    @Override
    public boolean onDown(MotionEvent e) {
        // This is required.  If absent, the scroll gesture never starts.
        return true;
    }

}

Here's the LogCat:

13:06:05.885: D/my-tag(4140): In View.onTouchEvent, Event: 0, Time: 183279420 X: 171.761444, Y: 918.160767
13:06:05.895: D/my-tag(4140): In View.onTouchEvent, Event: 261, Time: 183279420 X: 171.761444, Y: 918.160767
13:06:05.895: D/my-tag(4140): In onScaleBegin
13:06:05.895: I/ScaleGestureDetector(4140): TwScaleGestureDetector
13:06:05.915: D/my-tag(4140): In View.onTouchEvent, Event: 2, Time: 183279458 X: 171.761444, Y: 908.474365
13:06:05.915: D/my-tag(4140): In onScale
13:06:06.015: D/my-tag(4140): In View.onTouchEvent, Event: 2, Time: 183279542 X: 174.964783, Y: 857.584717
13:06:06.015: D/my-tag(4140): In onScale
13:06:06.105: D/my-tag(4140): In View.onTouchEvent, Event: 2, Time: 183279641 X: 232.242096, Y: 731.365662
13:06:06.105: D/my-tag(4140): In onScale
13:06:06.215: D/my-tag(4140): In View.onTouchEvent, Event: 2, Time: 183279740 X: 313.564514, Y: 595.412964
13:06:06.215: D/my-tag(4140): In onScale
13:06:06.225: D/my-tag(4140): In View.onTouchEvent, Event: 6, Time: 183279751 X: 313.564514, Y: 595.412964
13:06:06.225: D/my-tag(4140): In onScaleEnd
13:06:06.245: D/my-tag(4140): In View.onTouchEvent, Event: 2, Time: 183279774 X: 333.316528, Y: 487.607422
13:06:06.245: D/my-tag(4140): In onScroll, me1: 0, Time: 183279420 X: 171.761444, Y: 918.160767
13:06:06.245: D/my-tag(4140): In onScroll, me2 2: 2, Time: 183279774 X: 333.316528, Y: 487.607422
13:06:06.255: D/my-tag(4140): In View.onTouchEvent, Event: 2, Time: 183279784 X: 331.539551, Y: 488.496460
13:06:06.265: D/my-tag(4140): In onScroll, me1: 0, Time: 183279420 X: 171.761444, Y: 918.160767
13:06:06.265: D/my-tag(4140): In onScroll, me2 2: 2, Time: 183279784 X: 331.539551, Y: 488.496460
13:06:06.275: D/my-tag(4140): In View.onTouchEvent, Event: 1, Time: 183279794 X: 331.539551, Y: 488.496460

You can see that the first event is the first finger down (0 = ACTION_DOWN), then the second finger down (261 = ACTION_POINTER_2_DOWN). Then we see log entry from the onScaleBegin call and a log from the scale gesture detector itself (not from my code). At this point I think I can safely assume the scale gesture has been started. This is exactly as expected.

This is followed by four move events (2 = ACTION_MOVE), each of which is immediately followed by the log entry from onScale. This is still as expected.

Then we see a pointer up event (6 = ACTION_POINTER_UP) followed by the log entry from onScaleEnd, still AOK! (Note that it's a 6 and not a 262 because I lifted my fingers in the same order that I put them down, so pointer 1 was lifted first, not pointer 2.)

Now the weird bit.

We see a move event, which gets picked up by onScroll in the SimpleOnGestureListener. The first parameter me1 has the x and y coordinates from the very first down event before the scale gesture started The second parameter me2 has coordinates that apparently reflect a position after the scale gesture had stopped.

In this example there's actually a second move event that also gets interpreted as a scroll gesture, again with the pre-scale origin point. With the above code I would variously get 1, 2 or no scroll events after pinch zooms.

(To finish off the LogCat, we a final up event (1 = ACTION_UP) for the second finger and the log goes quiet.)

So am I doing it wrong? I've tried only calling the SimpleOnGestureListener if the SimpleOnScaleGestureListener returns false from isInProgress, but no joy.

Any ideas?

Thanks in advance, and thanks to all y'all in the community for the vast amount of information I've gotten from this site over the years!

like image 722
William T. Mallard Avatar asked Jul 26 '13 20:07

William T. Mallard


1 Answers

I also found this peculiar behaviour building a custom view with pan/zoom capabilities. But after some thought, here is my reasoning:

In a multitouch environment, each finger is registered and their respective motions are processed in some kind of parallel analysis. This is what allows the detection of the different touch events the system can send by means of the OnGestureListener and OnScaleGestureListener.

Right, nothing you don't already know.

Now, think the different behaviour of the two detectors:

  • GestureDetector detects an scroll event by means of a drag with only one finger inside the limits of the viewable area of the view. Its pattern responds to: down - drag - up. Scroll events are produced as the drag events are detected.

  • ScaleGestureDetector detects scale events by means of two simultaneous drags fired by two fingers in a multitouch environment. Its pattern responds to: (down1&down2) - (drag1 and/or drag2) - (up1 or up2).

And now, think of a custom view where you only need to detect scroll events (ignoring all the other). In such a case, the scroll event must be fired aside all other considerations because you have performed its pattern (down-drag-up).

When you combine this two detectors, they act independently, so the scale detector fires first, but when you lift the second finger, the scroll detector fires because it detects one finger that is dragging and finishing with an up event!

Conclusion: The behaviour seems reasonable... yet Android could have provided some cross detector for the simultaneous situation.

Well, you can simply put a boolean and solve the problem. I had this done in my implementation:

  • Declare a boolean named scaling
  • Make your onDown event (on the ACTION_DOWN event) to clear scaling
  • Make your onScale event to set scaling
  • Make your onScroll event not to process the scroll if the scaling flag is true

This worked for me already.

like image 191
felixgaal Avatar answered Oct 10 '22 06:10

felixgaal