Can't handle both click and touch events simultaneously
Asked Answered
H

13

47

I am trying to handle touch events and click events on a button. I do the following:

button.setOnClickListener(clickListener);
button.setOnTouchListener(touchListener);

When any one listener is registered things work fine but when I try to use them both only the touch events are fired. Any workaround? What am I doing wrong?

Handspike answered 1/3, 2011 at 19:32 Comment(0)
A
42

There is a subtle, yet very important difference between the ClickListener and the TouchListener. The TouchListener is executed before the view can respond to the event. The ClickListener will receive its event only after the view has handled it.

So when you touch your screen, the TouchListener is executed first and when you return true for your event, the ClickListener will never get it. But if you press the trackball of your device, the ClickListener should be fired because the TouchListener will not respond to it.

Aeroscope answered 1/3, 2011 at 19:48 Comment(0)
B
79

Its a little tricky.

If you set onTouchListener you need to return true in ACTION_DOWN, to tell the system that I have consumed the event and it won't trickle down to other listeners.

But then OnClickListener won't be fired.

So you might think, I will just do my thing there and return false so I can receive clicks too. If you do so, it will work, but you won't be subscribed to other upcoming touch events (ACTION_MOVE, ACTION_UP) Therefore, the only option is to return true there, but then you won't receive any click events as we said previously.

So you need to perform the click manually in the ACTION_UP with view.performClick()

This will work.

Bunko answered 29/10, 2013 at 22:52 Comment(4)
The key part of this is "you need to perform the click manually in the ACTION_UP", thank you sirDudek
This is really the best answer. In my ACTION_MOVE, I begin intercepting touch events if a certain deltaX threshold is met. If I get to ACTION_UP and that deltaX threshold was never met, I fire listView.performClick(), otherwise, I do my swipe actions.Elsewhere
This obviously should be the answer.Larrylars
Great answer. Keep it upCalipash
A
42

There is a subtle, yet very important difference between the ClickListener and the TouchListener. The TouchListener is executed before the view can respond to the event. The ClickListener will receive its event only after the view has handled it.

So when you touch your screen, the TouchListener is executed first and when you return true for your event, the ClickListener will never get it. But if you press the trackball of your device, the ClickListener should be fired because the TouchListener will not respond to it.

Aeroscope answered 1/3, 2011 at 19:48 Comment(0)
B
21

Thanks to @urSus for great answer
But in that case every touch will perform click, Even ACTION_MOVE
Assuming you want to separate move event and click event you can use a little trick
define a boolean field and use like this:

 @Override
        public boolean onTouch(View view, MotionEvent motionEvent)
        {
            switch (motionEvent.getAction() & MotionEvent.ACTION_MASK)
            {
                case MotionEvent.ACTION_DOWN:
                    shouldClick = true;
                    .
                    .
                    break;
                case MotionEvent.ACTION_UP:
                    if (shouldClick)
                        view.performClick();
                    break;
                case MotionEvent.ACTION_POINTER_DOWN:
                    break;
                case MotionEvent.ACTION_POINTER_UP:
                    break;
                case MotionEvent.ACTION_MOVE:
                    //Do your stuff
                    shouldClick = false;
                    break;
            }
            rootLayout.invalidate();
            return true;
        }
Baritone answered 5/6, 2015 at 15:48 Comment(5)
Great answer. Keep it upCalipash
perform click is the view method ? or a custom method where we have to perform clickReaction
@user3475052 performClick() is every view and its subclasses methodBaritone
It works, if return true; in MotionEvent.ACTION_DOWN branch, remove MotionEvent.ACTION_MOVE branch, get movement difference in MotionEvent.ACTION_UP branch. See #17831895Germinal
really this answer is best in all similar questionJustin
C
6

You should return false in your OnTouchListener then your OnClickListener will be also handled.

Contingency answered 20/4, 2011 at 15:53 Comment(0)
H
5

I suppose you're returning true in your OnTouchListener? That will consume the event so it won't be sent down for any further processing.

On a side note - what's the point of having both a click and touch listener?

Humphries answered 1/3, 2011 at 19:36 Comment(2)
The client wants a sound to be played when the button is pressed and released, I do it in the 'ACTION_UP' and 'ACTION_DOWN' events. And the click event does the logic. Now I'll have to handle the logic in the touch event itself.Handspike
A button that can be dragged around, then clicked when finished?Evocation
K
4

This is an example of TouchListener that doesn't shadow ClickListener.

import android.graphics.PointF
import android.view.MotionEvent
import android.view.MotionEvent.*
import android.view.View
import kotlin.math.abs

object TouchAndClickListener : View.OnTouchListener {

    /** Those are factors you can change as you prefer */
    private const val touchMoveFactor = 10
    private const val touchTimeFactor = 200


    private var actionDownPoint = PointF(0f, 0f)
    private var previousPoint = PointF(0f, 0f)
    private var touchDownTime = 0L

    override fun onTouch(v: View, event: MotionEvent) = when (event.action) {
        ACTION_DOWN -> PointF(event.x, event.y).let {

            actionDownPoint = it  // Store it to compare position when ACTION_UP
            previousPoint = it  // Store it to compare position when ACTION_MOVE
            touchDownTime = now() // Store it to compare time when ACTION_UP

            /* Do other stuff related to ACTION_DOWN you may whant here */

            true
        }

        ACTION_UP -> PointF(event.x, event.y).let {

            val isTouchDuration = now() - touchDownTime < touchTimeFactor  // short time should mean this is a click
            val isTouchLength = abs(it.x - actionDownPoint.x) + abs(it.y - actionDownPoint.y) < touchMoveFactor  // short length should mean this is a click

            val shouldClick = isTouchLength && isTouchDuration  // Check both

            if (shouldClick) yourView.performClick() //Previously define setOnClickListener{ } on yourView, then performClick() will call it

            /* Do other stuff related to ACTION_UP you may whant here */

            true
        }

        ACTION_MOVE -> PointF(event.x, event.y).let {

            /* Do other stuff related to ACTION_MOVE you may whant here */

            previousPoint = it
            true
        }

        else -> false // Nothing particular with other event
    }

    private fun now() = System.currentTimeMillis()
}
Killion answered 9/5, 2019 at 14:50 Comment(1)
Great answer but please it should have been in java not kotlinRivi
R
4

Thanks to @Nicolas Duponchel this is how i achieved both onClick and onTouch events

  //Define these globally e.g in your MainActivity class
  private short touchMoveFactor = 10;
  private short touchTimeFactor = 200;
  private PointF actionDownPoint = new PointF(0f, 0f);
  private long touchDownTime = 0L;
            
             @Override
                public boolean onTouchEvent(MotionEvent event) {
                    final int action = event.getAction();
                    switch (action) {
                     case MotionEvent.ACTION_DOWN: {
                            actionDownPoint.x = event.getX();
                            actionDownPoint.y = event.getY();
                            touchDownTime = System.currentTimeMillis();
                            break;
                            }
                     case MotionEvent.ACTION_UP: {
                            //on touch released, check if the finger was still close to the point he/she clicked 
                            boolean isTouchLength = (Math.abs(event.getX() - actionDownPoint.x)+ Math.abs(event.getY() - actionDownPoint.y)) < touchMoveFactor;
                            boolean isClickTime = System.currentTimeMillis() - touchDownTime < touchTimeFactor;
        
                            //if it's been more than 200ms since user first touched and, the finger was close to the same place when released, consider it a click event
                            //Please note that this is a workaround :D
                            if (isTouchLength && isClickTime){ 
                               //call this method on the view, e.g ivPic.performClick();
                               performClick();}
                            break;
                            }
                     }
                }
Rivi answered 2/4, 2020 at 19:22 Comment(0)
S
3

All above answer is said that we can not handle both setOnTouchListener and setOnClickListener.
However, I see we can handle both by return false in setOnTouchListener

Example

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    button = findViewById(R.id.button)
    button.setOnClickListener {
        Log.i("TAG", "onClick")
    }

    button.setOnTouchListener { v, event ->
        Log.i("TAG", "onTouch " + event.action)
        false
    }
}

When I click at Button, logcat will display like

I/TAG: onTouch 0
I/TAG: onTouch 1
I/TAG: onClick
Scarborough answered 17/8, 2018 at 8:33 Comment(1)
returning false from onTouch is equal to not setting onTouchListener at all.Rivi
S
0
button.setOnTouchListener(this);

Implement interface and the code here:

@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
    switch (view.getId()) {
        case R.id.send:
            switch(motionEvent.getAction()){
                case MotionEvent.ACTION_DOWN:
                    //when the user has pressed the button
                    //do the needful here
                    break;
                case MotionEvent.ACTION_UP:
                    //when the user releases the button
                    //do the needful here
                    break;
            }
            break;
    }
    return false;
}
Subcontinent answered 25/7, 2016 at 11:9 Comment(0)
W
0

To make both events possible in gridview,only by making return of touch listener"false" as follows,this worked for me.

**GridView myView = findViewById(R.id.grid_view);
myView.setOnTouchListener(new OnTouchListener() {
    public boolean onTouch(View v, MotionEvent event) {
        // ... Respond to touch events
        return false;
    }
});**

in this way both events can be achieved

Weathersby answered 10/10, 2017 at 12:34 Comment(0)
G
0
## Exact working solution for both click action and touch listener(dragging) ##

private int initialX;
private int initialY;
private float initialTouchX;
private float initialTouchY;
private float CLICK_ACTION_THRESHOLD = 0.5f;
private float startX;
private float startY;

 @Override
public boolean onTouch(View view, MotionEvent event) {
    switch (view.getId()) {
        case R.id.chat_head_profile_iv:
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    //remember the initial position.
                    initialX = params.x;
                    initialY = params.y;
                    startX = event.getX();
                    startY = event.getY();
                    //get the touch location
                    initialTouchX = event.getRawX();
                    initialTouchY = event.getRawY();
                    return true;
                case MotionEvent.ACTION_UP:
                    float endX = event.getX();
                    float endY = event.getY();
                    if (shouldClickActionWork(startX, endX, startY, endY)) {
                        openScreen();// WE HAVE A CLICK!!
                    }
                    return true;
                case MotionEvent.ACTION_MOVE:
                    //Calculate the X and Y coordinates of the view.
                    params.x = initialX + (int) (event.getRawX() - initialTouchX);
                    params.y = initialY + (int) (event.getRawY() - initialTouchY);

                    //Update the layout with new X & Y coordinate
                    mWindowManager.updateViewLayout(mChatHeadView, params);
                    return true;
            }
            break;
    }
    return true;
}

private boolean shouldClickActionWork(float startX, float endX, float startY, float endY) {
    float differenceX = Math.abs(startX - endX);
    float differenceY = Math.abs(startY - endY);
    if ((CLICK_ACTION_THRESHOLD > differenceX) && (CLICK_ACTION_THRESHOLD > differenceY))
        return true;
    else
        return false;
}
Gagger answered 14/2, 2018 at 19:40 Comment(1)
What is params ?Pachysandra
I
0

In ACTION_UP perform onClick manually using condition

boolean shouldClick = event.eventTime - event.downTime <= 200 // compares the time with some threshold

So, try within MotionEvent.ACTION_UP

if(event.eventTime - event.downTime <= 200) { // case or when statement of action Touch listener
    view.performClick();
}
Intracellular answered 23/3, 2020 at 11:16 Comment(0)
H
0

I knows its too late, but if someone is struggling with this for a clean solution, here it is.

These are used for measuring the time between touching and removing the finger.

    private long clickTime = 0;
    public static final long CLICK_TIMEOUT = 200; // 200ms

This my onTouchListner. Works like a charm

    private final View.OnTouchListener onTouchListener = (v, event) -> {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        clickTime = System.currentTimeMillis();
        return true;
    } else if(event.getAction() ==  MotionEvent.ACTION_UP) {
        if(System.currentTimeMillis()-clickTime < Constants.CLICK_TIMEOUT)
        {
            Toast.makeText(getContext(), "clicked", Toast.LENGTH_SHORT).show();
            return true;
        }
        return false;
    }
    else if(event.getAction() == MotionEvent.ACTION_MOVE){
        if(System.currentTimeMillis()-clickTime > Constants.CLICK_TIMEOUT)
        {
            ClipData data = ClipData.newPlainText("" , "");
            View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(v);
            v.startDrag(data , shadowBuilder , v , 0);
            return false;
        }
        return false;
    }
    return false;
};
Hunsaker answered 18/5, 2021 at 17:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.