84

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?

Jonik
  • 80,077
  • 70
  • 264
  • 372
some.birdie
  • 2,259
  • 4
  • 24
  • 28
  • did you try using onTouch instead of onTouchEvent, by doing so you will have a reference to the view as well, so you can log out the values and see if the click that is ACTION_DOWN and ACTION_UP are invoked or not... – Arif Nadeem Apr 01 '12 at 15:51

11 Answers11

123

Here's another solution that is very simple and doesn't require you to worry about the finger being moved. If you are basing a click as simply the distance moved then how can you differentiate a click and a long click.

You could put more smarts into this and include the distance moved, but i'm yet to come across an instance when the distance a user can move in 200 milliseconds should constitute a move as opposed to a click.

setOnTouchListener(new OnTouchListener() {
    private static final int MAX_CLICK_DURATION = 200;
    private long startClickTime;

    @Override
    public boolean onTouch(View v, 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) {
                    //click event has occurred
                }
            }
        }
        return true;
    }
});
saschoar
  • 8,150
  • 5
  • 43
  • 46
Stimsoni
  • 3,166
  • 2
  • 29
  • 22
60

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.

Community
  • 1
  • 1
Jonik
  • 80,077
  • 70
  • 264
  • 372
  • Excellent answer ! This saved quite a bit of research, thank you ! – 2Dee Dec 09 '13 at 16:37
  • One question for this answer, do you think it would be better to record the distance from within the move event? If I start with my finger in the view, I could swipe up to the top of the screen and back down to the bottom within a second and it would still count as a click. If this was recorded inside the move action, it could set a boolean if the event ever surpassed the click region. – Gabriel Apr 28 '15 at 19:45
  • @Gabriel: It's been a while, but I think I just found this to be "good enough" for my needs. Feel free to experiment if you get better results with that approach, and if so, consider posting a new answer :-) – Jonik Apr 28 '15 at 19:51
  • I think I had a somewhat more specific use case than most people, I have a view that can be dragged up from the bottom to the top of the screen, or the header could be clicked to toggle the position. This meant that if you dragged it up then back down quickly, it was still recognizing the click. Thank you though, your solution was almost 100% of what I needed, I just made some slight alterations to it to get me there. I'll post my code later tonight as an alternate answer for people that need a bit more precision. – Gabriel Apr 28 '15 at 22:41
  • Hi Jonik, in your `pxToDp()` function, where is `getResources()` supposed to be defined? It appears that your code is meant to be put in a custom subclass of `View` so it seems to be trying to access `View#getResources()` which it can't since `pxToDp()` is static while `View#getResources()` is not. Maybe remove the static modifier or pass Context as a parameter? – Pang Apr 26 '17 at 05:42
  • 2
    Oh, and your `onTouchEvent()` is missing a return value. It would be helpful if you show us what to write there (`return true;`? `return false;`? `return super.onTouchEvent(e);`?) – Pang Apr 26 '17 at 05:45
  • @Pang just replace `context.getResouces()` with `Resources.getSystem()` – Sz-Nika Janos Aug 27 '18 at 22:56
  • @Pang it implements OnTouchListener Interface but modify `onTouchEvent(MotionEvent e)` with `onTouchEvent(View v,MotionEvent e) ` also you can you v.getCOntext() to pxToDP – Sz-Nika Janos Aug 27 '18 at 22:58
33

Taking Jonik's lead I built a slightly more fine tuned version, that doesn't register as a click if you move your finger and then return to the spot before letting go:

So here is my solution:

/**
 * 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;
private boolean stayedWithinClickDistance;

@Override
public boolean onTouchEvent(MotionEvent e) {
     switch (e.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            pressStartTime = System.currentTimeMillis();                
            pressedX = e.getX();
            pressedY = e.getY();
            stayedWithinClickDistance = true;
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            if (stayedWithinClickDistance && distance(pressedX, pressedY, e.getX(), e.getY()) > MAX_CLICK_DISTANCE) {
                stayedWithinClickDistance = false;
            }
            break;
        }     
        case MotionEvent.ACTION_UP: {
            long pressDuration = System.currentTimeMillis() - pressStartTime;
            if (pressDuration < MAX_CLICK_DURATION && stayedWithinClickDistance) {
                // 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;
}
Community
  • 1
  • 1
Gabriel
  • 2,279
  • 1
  • 20
  • 17
28

Use the detector, It works, and it will not raise in case of dragging

Field:

private GestureDetector mTapDetector;

Initialize:

mTapDetector = new GestureDetector(context,new GestureTap());

Inner class:

class GestureTap extends GestureDetector.SimpleOnGestureListener {
    @Override
    public boolean onDoubleTap(MotionEvent e) {

        return true;
    }

    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
        // TODO: handle tap here
        return true;
    }
}

onTouch:

@Override
public boolean onTouch(View v, MotionEvent event) {
    mTapDetector.onTouchEvent(event);
    return true;
}

Enjoy :)

Gil SH
  • 3,789
  • 1
  • 27
  • 25
  • Favorite solution. However if I want to use one Touch/Gesture listener for multiple views, I have to keep track of the last view during the last onTouch event so I know which view the onSingleTapConfirmed event was from. It would be nice if the MotionEvent stored the view for you. – AlanKley Dec 12 '16 at 21:15
  • For who may have problems running this code, I have to set the return value of function onTouch(View v, MotionEvent event) to "true" to make this work. Actually, I think we consume the events and should return true. – 黄锐铭 May 05 '17 at 17:38
7

To get most optimized recognition of click event We have to consider 2 things:

  1. Time difference between ACTION_DOWN and ACTION_UP.
  2. Difference between x's,y's when user touch and when release the finger.

Actually i combine the logic given by Stimsoni and Neethirajan

So here is my solution:

        view.setOnTouchListener(new OnTouchListener() {

        private final int MAX_CLICK_DURATION = 400;
        private final int MAX_CLICK_DISTANCE = 5;
        private long startClickTime;
        private float x1;
        private float y1;
        private float x2;
        private float y2;
        private float dx;
        private float dy;

        @Override
        public boolean onTouch(View view, MotionEvent event) {
            // TODO Auto-generated method stub

                    switch (event.getAction()) 
                    {
                        case MotionEvent.ACTION_DOWN: 
                        {
                            startClickTime = Calendar.getInstance().getTimeInMillis();
                            x1 = event.getX();
                            y1 = event.getY();
                            break;
                        }
                        case MotionEvent.ACTION_UP: 
                        {
                            long clickDuration = Calendar.getInstance().getTimeInMillis() - startClickTime;
                            x2 = event.getX();
                            y2 = event.getY();
                            dx = x2-x1;
                            dy = y2-y1;

                            if(clickDuration < MAX_CLICK_DURATION && dx < MAX_CLICK_DISTANCE && dy < MAX_CLICK_DISTANCE) 
                                Log.v("","On Item Clicked:: ");

                        }
                    }

            return  false;
        }
    });
Gem
  • 1,516
  • 16
  • 21
  • +1, good approach. I went with a [similar solution](http://stackoverflow.com/a/19788836/56285), except using dp instead of px for `MAX_CLICK_DISTANCE`, and calculating the distance a little differently. – Jonik Nov 05 '13 at 17:28
  • these variable declared above should be declared globally. – Rushi Ayyappa Dec 02 '16 at 05:34
  • @RushiAyyappa In most of the cases you set the onTouchListener once on the view. The `setOnTouchListener()` takes anonymous class reference. Where the variables are global inside the class. I think that will work unless someone call `setOnTouchListener()` again with other instance of listener. – Gem Dec 05 '16 at 12:02
  • my touchListener is inner one and these variables got destroyed for the second time.So i had to declare them globally and this worked. – Rushi Ayyappa Dec 05 '16 at 12:11
6

Using Gil SH answer, I improved it by implementing onSingleTapUp() rather than onSingleTapConfirmed(). It is much faster and won't click the view if dragged/moved.

GestureTap:

public class GestureTap extends GestureDetector.SimpleOnGestureListener {
    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        button.performClick();
        return true;
    }
}

Use it like:

final GestureDetector gestureDetector = new GestureDetector(getApplicationContext(), new GestureTap());
button.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        gestureDetector.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                return true;
            case MotionEvent.ACTION_UP:
                return true;
            case MotionEvent.ACTION_MOVE:
                return true;
        }
        return false;
    }
});
Hussein El Feky
  • 6,627
  • 5
  • 44
  • 57
  • This answer helped me out to distinguish a touch from a scroll on a TextView. An even further slimmed down version of this answer can be found [here](https://stackoverflow.com/a/58463966/10365982) (Kotlin) – Matteljay Oct 19 '19 at 13:08
4

Below code will solve your problem


    @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch(event.getAction()) {
                case(MotionEvent.ACTION_DOWN):
                    x1 = event.getX();
                    y1 = event.getY();
                    break;
                case(MotionEvent.ACTION_UP): {
                    x2 = event.getX();
                    y2 = event.getY();
                    dx = x2-x1;
                    dy = y2-y1;

                if(Math.abs(dx) > Math.abs(dy)) 
                {
                    if(dx>0) move(1); //right
                    else if (dx == 0) move(5); //click
                    else move(2); //left
                } 
                else 
                {
                    if(dy>0) move(3); // down
                    else if (dy == 0) move(5); //click
                    else move(4); //up
                }
                }
            }
            return true;
        }

3

It's very difficult for an ACTION_DOWN to occur without an ACTION_MOVE occurring. The slightest twitch of your finger on the screen in a different spot than where the first touch occurred will trigger the MOVE event. Also, I believe a change in finger pressure will also trigger the MOVE event. I would use an if statement in the Action_Move method to try to determine the distance away the move occurred from the original DOWN motion. if the move occurred outside some set radius, your MOVE action would occur. It's probably not the best, resource efficient way to do what your trying but it should work.

testingtester
  • 528
  • 4
  • 11
  • Thanks, I've also planned something like this, but thought that someone knows some other solutions. Have you also expirienced such behaviour (down->move->up while clicking)? – some.birdie Apr 01 '12 at 16:08
  • Yes, I am currently working on game that involved multitouch and I did some extensive testing regarding poitners and and Move_Events. This was one of the conclusions that I came to while testing (that Action_Down event quickly gets changed to Action_Move. Glad I could help. – testingtester Apr 01 '12 at 16:13
  • Ok, that's good that I'm not alone :) I haven't found someone whith such problem on stackoverflow or somewhere else. Thank you. – some.birdie Apr 01 '12 at 16:22
  • 1
    On which devices have you seen such behaviour? I've noticed this on Samsung Galaxy Ace and Gio. – some.birdie Apr 02 '12 at 09:02
  • I've seen it personally on a Samsung Captivate and a Galaxy S II but I imagine its probably like this for almost all devices – testingtester Apr 02 '12 at 11:04
2

Adding to the above answers ,if you want to implement both onClick and Drag actions then my code below can you guys. Taking some of the help from @Stimsoni :

     // assumed all the variables are declared globally; 

    public boolean onTouch(View view, MotionEvent event) {

      int MAX_CLICK_DURATION = 400;
      int MAX_CLICK_DISTANCE = 5;


        switch (event.getAction())
        {
            case MotionEvent.ACTION_DOWN: {
                long clickDuration1 = Calendar.getInstance().getTimeInMillis() - startClickTime;


                    startClickTime = Calendar.getInstance().getTimeInMillis();
                    x1 = event.getX();
                    y1 = event.getY();


                    break;

            }
            case MotionEvent.ACTION_UP:
            {
                long clickDuration = Calendar.getInstance().getTimeInMillis() - startClickTime;
                x2 = event.getX();
                y2 = event.getY();
                dx = x2-x1;
                dy = y2-y1;

                if(clickDuration < MAX_CLICK_DURATION && dx < MAX_CLICK_DISTANCE && dy < MAX_CLICK_DISTANCE) {
                    Toast.makeText(getApplicationContext(), "item clicked", Toast.LENGTH_SHORT).show();
                    Log.d("clicked", "On Item Clicked:: ");

               //    imageClickAction((ImageView) view,rl);
                }

            }
            case MotionEvent.ACTION_MOVE:

                long clickDuration = Calendar.getInstance().getTimeInMillis() - startClickTime;
                x2 = event.getX();
                y2 = event.getY();
                dx = x2-x1;
                dy = y2-y1;

                if(clickDuration < MAX_CLICK_DURATION && dx < MAX_CLICK_DISTANCE && dy < MAX_CLICK_DISTANCE) {
                    //Toast.makeText(getApplicationContext(), "item clicked", Toast.LENGTH_SHORT).show();
                  //  Log.d("clicked", "On Item Clicked:: ");

                    //    imageClickAction((ImageView) view,rl);
                }
                else {
                    ClipData clipData = ClipData.newPlainText("", "");
                    View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view);

                    //Toast.makeText(getApplicationContext(), "item dragged", Toast.LENGTH_SHORT).show();
                    view.startDrag(clipData, shadowBuilder, view, 0);
                }
                break;
        }

        return  false;
    }
Rushi Ayyappa
  • 2,708
  • 2
  • 16
  • 32
1

If you want to react on click only, use:

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

}
YTerle
  • 2,606
  • 6
  • 24
  • 40
0

Rather than distance / time diff based approaches, You can make use of GestureDetector in combination with setOnTouchListener to achieve this. GestureDetector would detect the click while you can use OnTouchListener for other touch based events, e.g detecting drag.

Here is a sample code for reference:

Class MyCustomView() {

    fun addClickAndTouchListener() {
        val gestureDetector = GestureDetector(
            context,
            object : GestureDetector.SimpleOnGestureListener() {
                override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
                    // Add your onclick logic here
                    return true
                }
            }
        )

        setOnTouchListener { view, event ->
            when {
                gestureDetector.onTouchEvent(event) -> {
                    // Your onclick logic would be triggered through SimpleOnGestureListener
                    return@setOnTouchListener true
                }
                event.action == MotionEvent.ACTION_DOWN -> {
                    // Handle touch event
                    return@setOnTouchListener true
                }
                event.action == MotionEvent.ACTION_MOVE -> {
                    // Handle drag
                    return@setOnTouchListener true
                }
                event.action == MotionEvent.ACTION_UP -> {
                    // Handle Drag over
                    return@setOnTouchListener true
                }
                else -> return@setOnTouchListener false
            }
        }

    }
}
Praveen Singh
  • 2,499
  • 3
  • 19
  • 29