开发者

Can't handle both click and touch events simultaneously

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?


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.


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.


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;
        }


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


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?


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()
}


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;
                            }
                     }
                }


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


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;
}


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


## 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;
}


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();
}


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;
};
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜