124697
124697

Reputation: 21901

How can I detect a click in an onTouch listener?

I have a ViewPager inside a ScrollView. I need to be able to scroll horizontally as well as vertically. In order to achieve this had to disable the vertical scrolling whenever my ViewPager is touched (v.getParent().requestDisallowInterceptTouchEvent(true);), so that it can be scrolled horizontally.

But at the same time I need to be able to click the viewPager to open it in full screen mode.

The problem is that onTouch gets called before onClick and my OnClick is never called.

How can I implement both on touch an onClick?

viewPager.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        System.out.println("TOUCHED ");
        if(event.getAction() == MotionEvent.???){
            //open fullscreen activity
        }
        v.getParent().requestDisallowInterceptTouchEvent(true); //This cannot be removed
        return false;
    }
});

viewPager.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        System.out.println("CLICKED ");
        Intent fullPhotoIntent = new Intent(context, FullPhotoActivity.class);
        fullPhotoIntent.putStringArrayListExtra("imageUrls", imageUrls);
        startActivity(fullPhotoIntent);
    }
});

Upvotes: 40

Views: 44096

Answers (13)

ucMax
ucMax

Reputation: 5486

don't try to REINVENT the wheel !

Elegant way to do it :

public class CustomView extends View {

    private GestureDetectorCompat mDetector;

    public CustomView(Context context) {
        super(context);
        mDetector = new GestureDetectorCompat(context, new MyGestureListener());
    }

    @Override
    public boolean onTouchEvent(MotionEvent event){
        return this.mDetector.onTouchEvent(event);
    }

    class MyGestureListener extends GestureDetector.SimpleOnGestureListener {
        @Override
        public boolean onDown(MotionEvent e) {return true;}

        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            //...... click detected !
            return false;
        }
    }
}

Upvotes: 7

Praveen Singh
Praveen Singh

Reputation: 2567

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

    }
}

Upvotes: 4

Zobaer Hossain
Zobaer Hossain

Reputation: 23

This code will do both touch events and click event.

viewPager.setOnTouchListener(new View.OnTouchListener() {
            private int initialX;
            private int initialY;
            private float initialTouchX;
            private float initialTouchY;


            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:

                        //remember the initial position.
                        initialX = params.x;
                        initialY = params.y;

                        //get the touch location
                        initialTouchX = event.getRawX();
                        initialTouchY = event.getRawY();
                        return true;
                    case MotionEvent.ACTION_UP:
                        int Xdiff = (int) (event.getRawX() - initialTouchX);
                        int Ydiff = (int) (event.getRawY() - initialTouchY);


                        //The check for Xdiff <10 && YDiff< 10 because sometime elements moves a little while clicking.
                        
                        if (Xdiff < 10 && Ydiff < 10) {
                            //So that is click event.
                        }
                        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(mFloatingView, params);
                        return true;
                }
                return false;
            }
        });

Here is the source.

Upvotes: 1

MJ Studio
MJ Studio

Reputation: 4631

if (event.eventTime - event.downTime < 100 && event.actionMasked == MotionEvent.ACTION_UP) {
     view.performClick()
     return false
}

Upvotes: 1

nilsi
nilsi

Reputation: 10761

I did something really simple by checking the time the user touches the screen.

private static int CLICK_THRESHOLD = 100;

@Override
public boolean onTouch(View v, MotionEvent event) {
    long duration = event.getEventTime() - event.getDownTime();

    if (event.getAction() == MotionEvent.ACTION_UP && duration < CLICK_THRESHOLD) {
        Log.w("bla", "you clicked!");
    }

    return false;
}

Also worth noting that GestureDetector has something like this built-in. Look at onSingleTapUp

Upvotes: 37

Benjamin Basmaci
Benjamin Basmaci

Reputation: 2567

The answers above mostly memorize the time. However, MotionEvent already has you covered. Here a solution with less overhead. Its written in kotlin but it should still be understandable:

private const val ClickThreshold = 100

override fun onTouch(v: View, event: MotionEvent): Boolean {
    if(event.action == MotionEvent.ACTION_UP 
      && event.eventTime - event.downTime < ClickThreshold) {
        v.performClick()
        return true // If you don't want to do any more actions
    }

    // do something in case its not a click

    return true // or false, whatever you need here
}

This solution is good enough for most applications but is not so good in distinguishing between a click and a very quick swipe.

So, combining this with the answers above that also take the position into account is probably the best one.

Upvotes: 4

124697
124697

Reputation: 21901

Masoud Dadashi's answer helped me figure it out.

here is how it looks in the end.

viewPager.setOnTouchListener(new OnTouchListener() {
    private int CLICK_ACTION_THRESHOLD = 200;
    private float startX;
    private float startY;

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            startX = event.getX();
            startY = event.getY();
            break;
        case MotionEvent.ACTION_UP: 
            float endX = event.getX();
            float endY = event.getY();
            if (isAClick(startX, endX, startY, endY)) { 
                launchFullPhotoActivity(imageUrls);// WE HAVE A CLICK!!
            }
            break;
        }
        v.getParent().requestDisallowInterceptTouchEvent(true); //specific to my project
        return false; //specific to my project
    }

    private boolean isAClick(float startX, float endX, float startY, float endY) {
        float differenceX = Math.abs(startX - endX);
        float differenceY = Math.abs(startY - endY);
        return !(differenceX > CLICK_ACTION_THRESHOLD/* =5 */ || differenceY > CLICK_ACTION_THRESHOLD);
    } 
}

Upvotes: 77

Mihuilk
Mihuilk

Reputation: 2011

I think combined solution time/position should be better:

 private float initialTouchX;
 private float initialTouchY;
 private long lastTouchDown;
 private static int CLICK_ACTION_THRESHHOLD = 100;
 @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:

                        lastTouchDown = System.currentTimeMillis();

                        //get the touch location
                        initialTouchX = event.getRawX();
                        initialTouchY = event.getRawY();
                        return true;
                    case MotionEvent.ACTION_UP:
                        int Xdiff = (int) (event.getRawX() - initialTouchX);
                        int Ydiff = (int) (event.getRawY() - initialTouchY);

                        if (System.currentTimeMillis() - lastTouchDown < CLICK_ACTION_THRESHHOLD &&  (Xdiff < 10 && Ydiff < 10))  {
                           //clicked!!!
                        }
                        return true;
                }
                return false;
            }
        });

Upvotes: 2

hoffer
hoffer

Reputation: 71

you can use OnTouchClickListener

https://github.com/hoffergg/BaseLibrary/blob/master/src/main/java/com/dailycation/base/view/listener/OnTouchClickListener.java

usage:

view.setOnTouchListener(new OnTouchClickListener(new OnTouchClickListener.OnClickListener() {
                @Override
                public void onClick(View v) {
                    //perform onClick
                }
            },5));

Upvotes: 1

Adam Komar
Adam Komar

Reputation: 220

You might need to differentiate between the user clicking and long-clicking. Otherwise, you'll detect both as the same thing. I did this to make that possible:

@Override
public boolean onTouch(View v, MotionEvent event) {
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
        startX = event.getX();
        startY = event.getY();

        bClick = true;
        tmrClick = new Timer();
        tmrClick.schedule(new TimerTask() {
            public void run() {
                if (bClick == true) {
                    bClick = false;
                    Log.d(LOG_TAG, "Hey, a long press event!");
                    //Handle the longpress event.
                }
            }
        }, 500); //500ms is the standard longpress response time. Adjust as you see fit.

        return true;
    case MotionEvent.ACTION_UP:
        endX = event.getX();
        endY = event.getY();

        diffX = Math.abs(startX - endX);
        diffY = Math.abs(startY - endY);

        if (diffX <= 5 && diffY <= 5 && bClick == true) {
            Log.d(LOG_TAG, "A click event!");
            bClick = false;
        }
        return true;
    default:
        return false;
    }
}

Upvotes: 7

Masoud Dadashi
Masoud Dadashi

Reputation: 1074

Developing both is the wrong idea. when user may do different things by touching the screen understanding user purpose is a little bit nifty and you need to develop a piece of code for it.

Two solutions:

1- (the better idea) in your onTouch event check if there is a motion. You can do it by checking if there is any movement using:

ACTION_UP
ACTION_DOWN
ACTION_MOVE

do it like this

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

you can even check the distance of the movement of user finger on screen to make sure a movement happened rather than an accidental move while clicking. do it like this:

switch(event.getAction())
 {
     case MotionEvent.ACTION_DOWN:
              if(isDown == false)
              {
                     startX = event.getX();
                     startY = event.getY();
                     isDown = true;
              }
              Break;
        case MotionEvent.ACTION_UP
              {
                     endX = event.getX();
                     endY = event.getY();
                     break;
          }
}

consider it a click if none of the above happened and do what you wanna do with click.

2) if rimes with your UI, create a button or image button or anything for full screening and set an onClick for it.

Good luck

Upvotes: 10

Antoine Marques
Antoine Marques

Reputation: 1389

I believe you're preventing your view from receiving the touch event this way because your TouchListener intercepts it. You can either

  • Dispatch the event inside your ToucheListener by calling v.onTouchEvent(event)
  • Override instead ViewPager.onTouchEvent(MotionEvent) not to intercept the event

Also, returning true means that you didn't consume the event, and that you're not interrested in following events, and you won't therefore receive following events until the gesture completes (that is, the finger is up again).

Upvotes: 1

AitorTheRed
AitorTheRed

Reputation: 553

I think your problem comes from the line:

v.getParent().requestDisallowInterceptTouchEvent(true); //This cannot be removed

Take a look to the documentation. Have you tried to remove the line? What is the requeriment for not removing this line?

Take into account, according to the documentation, that if you return true from your onTouchListener, the event is consumed, and if you return false, is propagated, so you could use this to propagate the event.

Also, you should change your code from:

 System.out.println("CLICKED ");

to:

 Log.d("MyApp", "CLICKED ");

To get correct output in logcat.

Upvotes: 0

Related Questions