How to distinguish between move and click in onTouchEvent()?


Answers

I got the best results by taking into account:

  1. Primarily, the distance moved between ACTION_DOWN and ACTION_UP events. I wanted to specify the max allowed distance in density-indepenent pixels rather than pixels, to better support different screens. For example, 15 DP.
  2. Secondarily, the duration between the events. One second seemed good maximum. (Some people "click" quite "thorougly", i.e. slowly; I still want to recognise that.)

Example:

/**
 * Max allowed duration for a "click", in milliseconds.
 */
private static final int MAX_CLICK_DURATION = 1000;

/**
 * Max allowed distance to move during a "click", in DP.
 */
private static final int MAX_CLICK_DISTANCE = 15;

private long pressStartTime;
private float pressedX;
private float pressedY;

@Override
public boolean onTouchEvent(MotionEvent e) {
     switch (e.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            pressStartTime = System.currentTimeMillis();                
            pressedX = e.getX();
            pressedY = e.getY();
            break;
        }
        case MotionEvent.ACTION_UP: {
            long pressDuration = System.currentTimeMillis() - pressStartTime;
            if (pressDuration < MAX_CLICK_DURATION && distance(pressedX, pressedY, e.getX(), e.getY()) < MAX_CLICK_DISTANCE) {
                // Click event has occurred
            }
        }     
    }
}

private static float distance(float x1, float y1, float x2, float y2) {
    float dx = x1 - x2;
    float dy = y1 - y2;
    float distanceInPx = (float) Math.sqrt(dx * dx + dy * dy);
    return pxToDp(distanceInPx);
}

private static float pxToDp(float px) {
    return px / getResources().getDisplayMetrics().density;
}

The idea here is the same as in Gem's solution, with these differences:

  • This calculates the actual Euclidean distance between the two points.
  • This uses dp instead of px.

Update (2015): also check out Gabriel's fine-tuned version of this.

Question

In my application, I need to handle both move and click events.

A click is a sequence of one ACTION_DOWN action, several ACTION_MOVE actions and one ACTION_UP action. In theory, if you get an ACTION_DOWN event and then an ACTION_UP event - it means that the user has just clicked your View.

But in practice, this sequence doesn't work on some devices. On my Samsung Galaxy Gio I get such sequences when just clicking my View: ACTION_DOWN, several times ACTION_MOVE, then ACTION_UP. I.e. I get some unexpectable OnTouchEvent firings with ACTION_MOVE action code. I never (or almost never) get sequence ACTION_DOWN -> ACTION_UP.

I also cannot use OnClickListener because it does not gives the position of the click. So how can I detect click event and differ it from move?




You can identify moving action like this:

view.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {

            if(event.getAction() == MotionEvent.ACTION_MOVE)
            {

            }

            return false;
        }
    });



singletap touch detect in Ontouch method of the view

I also ran into the same problem recently and ended up having to implement a debounce to get it working. It's not ideal, but it's pretty reliable until I can find something better.

View.onClickListener was much more reliable for me, but unfortunately I need the MotionEvent from the OnTouchListener.

Edit: Removed the excess code that would cause it to fail here

class CustomView extends View {

    private static long mDeBounce = 0;

    static OnTouchListener listenerMotionEvent = new OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            if ( Math.abs(mDeBounce - motionEvent.getEventTime()) < 250) {
                //Ignore if it's been less then 250ms since
                //the item was last clicked
                return true;
            }

            int intCurrentY = Math.round(motionEvent.getY());
            int intCurrentX = Math.round(motionEvent.getX());
            int intStartY = motionEvent.getHistorySize() > 0 ? Math.round(motionEvent.getHistoricalY(0)) : intCurrentY;
            int intStartX = motionEvent.getHistorySize() > 0 ? Math.round(motionEvent.getHistoricalX(0)) : intCurrentX;

            if ( (motionEvent.getAction() == MotionEvent.ACTION_UP) && (Math.abs(intCurrentX - intStartX) < 3) && (Math.abs(intCurrentY - intStartY) < 3) ) {
                if ( mDeBounce > motionEvent.getDownTime() ) {
                    //Still got occasional duplicates without this
                    return true;
                }

                //Handle the click

                mDeBounce = motionEvent.getEventTime();
                return true;
            }
            return false;
        }
    };
}



Adding the answer for completeness sake and if anyone else reaches here:

You can use a GestureDetector with an OnTouchListener

final GestureDetector gestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
         //do something
            return true;
        }

        @Override
        public void onLongPress(MotionEvent e) {
            super.onLongPress(e);
        }

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            return super.onDoubleTap(e);
        }
    });

 viewToTouch.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {

            return gestureDetector.onTouchEvent(event);
        }
    });



I ended up with this solution:

public class ClickableWebView extends WebView {

    private static final int MAX_CLICK_DURATION = 200;
    private long startClickTime;

    public ClickableWebView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public ClickableWebView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public ClickableWebView(Context context) {
        super(context);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

            switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                startClickTime = Calendar.getInstance().getTimeInMillis();
                break;
            }
            case MotionEvent.ACTION_UP: {
                long clickDuration = Calendar.getInstance().getTimeInMillis() - startClickTime;
                if(clickDuration < MAX_CLICK_DURATION) {
                    super.performClick();

                }
            }
        }
        return true;
    }

}

Remarks:

  • suppresses all click-events to anything inside the WebView (e.g.: hyperlinks)
  • simply add OnClickListener by adding an onClick in xml or in Java
  • does not interupt scrolling-gestures

Thanks to Stimsoni Answer to How to distinguish between move and click in onTouchEvent()?





Tags