Are you developing an Android app that requires a long click, tap, touch or whatever? Then you should be adding an OnLongClickListener to your view. And if you are also going to carry out complex tasks with touch events, you might be adding an OnTouchListener to your view too! Same thing what I was doing at work. unfortunately, I was having a weird problem. Obviously, I was expecting the OnLongClickListener to capture the long click event only when I press my finger on the view for a certain amount of time. However, OnLongClickListener's onLongClick() was being called every time there was a touch event along with OnTouchListener's onTouch().

I was googling to find out why, eagerly seeking for a solution. Surprisingly, it seemed like there weren't many people having the same problem (or maybe I wasn't using the right keyword). Eventually I found out what was causing the problem, I have decided to share the experience. Not that it is something very unusual, but for those who are looking for a quick answer.

The problem was in my implementation of onTouch() in OnTouchListener. Traditionally (way back from Windows programming), when you are dealing with events, you would return true when the event handler handles the event, or more precisely when the event handler consumes it and return false when the event is meaningless to the event handler so that it passes on the event to the next available event handler according to the hierarchy of the user interface. This convention I was following was causing the problem.

In order for Android to detect long clicks, Android must keep track of the time after a touch down event has occurred. After reading the Android developer's document and third QnA threads from party forums I have reached to a conclusion that this time checking is taking place at a somewhat unexpected place. Honestly, I don't have much experience in Windows programming, so I'm not really sure if it's the same case for Windows programming, but in order to capture long click events correctly the onTouch event must return false on ACTION.DOWN events. 

To explain in detail, let's take a look at some code.

public class LongClickTest extends Activity {

public class TestView extends View {


final static private String TAG = "LongClickTest";

public TestView(Context context) {

super(context);

this.setOnTouchListener(mTouchListener);

this.setOnLongClickListener(mLongClickListener);

}

private OnLongClickListener mLongClickListener = 

new OnLongClickListener() {


@Override

public boolean onLongClick(View view) {

Log.d(TAG, "LongClick !!!");

return true;

}

};

private OnTouchListener mTouchListener = 

new OnTouchListener() {


@Override

public boolean onTouch(View view, MotionEvent event) {

boolean consumed = false;

switch(event.getAction()) {

case MotionEvent.ACTION_DOWN:

Log.d(TAG, "ACTION_DOWN");

consumed = true;

break;

case MotionEvent.ACTION_MOVE:

Log.d(TAG, "ACTION_MOVE");

consumed = true;

break;

case MotionEvent.ACTION_UP:

Log.d(TAG, "ACTION_UP");

consumed = true;

break;

}

return consumed;

}

};

}

    /** Called when the activity is first created. */

    @Override

    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        LayoutParams layoutParams = 

new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);

        setContentView(new TestView(this), layoutParams);

        

    }

}


Here I have made a inner class inside of an Activity class for convenience. The inner class called TestView inherits View and an instance of that TestView is set as the content view of the Activity class. Inside the inner class, I have implemented the OnTouchListener and OnLongClickListener to log about the occurring event and set each listeners to the corresponding listeners. If you execute the program, what you'll see is a blank black empty screen. But it's sufficient for testing.

First, let's try executing the above code as it is. In logcat, all you will see are logs of ACTION_DOWN, ACTION_MOVE, and ACTION_UP events. onLongClick() in OnLongClickListener is never triggered. As I have mentioned earlier, this is because the OnTouchListener has consumed all the events and Android has no chance of keeping track of how long the time elapsed before ACTION_UP after the ACTION_DOWN.

So, let's make some change in the OnTouchListener's onTouch() like the following.

@Override

public boolean onTouch(View view, MotionEvent event) {

boolean consumed = false;

switch(event.getAction()) {

case MotionEvent.ACTION_DOWN:

Log.d(TAG, "ACTION_DOWN");

consumed = false;

break;

case MotionEvent.ACTION_MOVE:

Log.d(TAG, "ACTION_MOVE");

consumed = true;

break;

case MotionEvent.ACTION_UP:

Log.d(TAG, "ACTION_UP");

consumed = true;

break;

}

return consumed;

}



 By returning false in case of ACTION_DOWN events, Android is now able to keep track of the duration of time after the ACTION_DOWN occurs. As a result, after a decent amount of time (about 1 second) after your fingertip touches the screen, you will see the log "LongClick !!!" in logcat. However, the problem is that you will find out that onLongClick() will be called every time you touch the screen leaving the log "LongClick !!!" in spite of the fact that you have lifted up your fingertip from the screen. This is the problem I was having. I have to admit that the code I was working on was poorly written, because my original intention was to return true whenever the OnTouchListener spotted a ACTION_DOWN event and handled it. Anyway, this was the flaw in my code and it was causing the problem that I am trying to illustrate here. The problem is that Android is now able to measure the amount of time passed after the ACTION_DOWN event, but it never knows when to stop measuring. This is because my OnTouchListener has consumed the ACTION_UP event and hence, Android has no clue when the ACTION_UP has occurred. Do you see what is going on???

Therefore my conclusion here is to return false no matter what. In this case, returning true for ACTION_DOWN and ACTION_UP will be enough to solve the problem. However, if we have to deal with more complex touch event sequence, it should return false for all events so that Android will be able to capture the event whatever is needed to trigger other events. Some other event's might require ACTION_MOVE to be captured outside the OnTouchListener you are implementing.

Therefore, the resulting code shall be the following.

@Override

public boolean onTouch(View view, MotionEvent event) {

switch(event.getAction()) {

case MotionEvent.ACTION_DOWN:

Log.d(TAG, "ACTION_DOWN");

break;

case MotionEvent.ACTION_MOVE:

Log.d(TAG, "ACTION_MOVE");

break;

case MotionEvent.ACTION_UP:

Log.d(TAG, "ACTION_UP");

break;

}

return false;

}


So this is a simple example demonstrating why your onLongClick() is called along with onTouch(). I haven't put much thought to this problem to determine whether this is a good design or not, but I don't like this event handling method at the moment. If it were to operate this way, why is there the need to return a boolean in the OnTouchListener in the first place...

Anyway, this is how Android is, and can't blame the dudes in Google because they probably know what they are doing...

Read http://developer.android.com/guide/topics/ui/ui-events.html for more information, especially the part where is starts with "onTouch:" and "note:"(<- be aware of the colon included. I've added that to explicitly indicate where the information is).
Posted by Dansoonie