90

I am currently using onTouchEvent(MotionEvent event) { } to detect when the user presses my glSurfaceView is there a way to detect when a long click is made.

I'm guessing if I can't find much in the dev docs then it will be some sort of work around method. Something like registering ACTION_DOWN and seeing how long it is before ACTION_UP.

How do you detect long presses on Android using opengl-es?

Ola Ström
  • 4,136
  • 5
  • 22
  • 41
Jack
  • 2,625
  • 5
  • 33
  • 56

11 Answers11

171

GestureDetector is the best solution.

Here is an interesting alternative. In onTouchEvent on every ACTION_DOWN schedule a Runnable to run in 1 second. On every ACTION_UP or ACTION_MOVE, cancel scheduled Runnable. If cancelation happens less than 1s from ACTION_DOWN event, Runnable won't run.

final Handler handler = new Handler(); 
Runnable mLongPressed = new Runnable() { 
    public void run() { 
        Log.i("", "Long press!");
    }   
};

@Override
public boolean onTouchEvent(MotionEvent event, MapView mapView){
    if(event.getAction() == MotionEvent.ACTION_DOWN)
        handler.postDelayed(mLongPressed, ViewConfiguration.getLongPressTimeout());
    if((event.getAction() == MotionEvent.ACTION_MOVE)||(event.getAction() == MotionEvent.ACTION_UP))
        handler.removeCallbacks(mLongPressed);
    return super.onTouchEvent(event, mapView);
}
Rohan Bhatia
  • 1,870
  • 2
  • 15
  • 31
MSquare
  • 6,311
  • 4
  • 31
  • 37
  • Would this not fire hundreds of delayed Runnables if the state stays for a long time in `MotionEvent.ACTION_DOWN`? This should be not likely, but impossible? – seb Mar 10 '14 at 11:01
  • 18
    You can get the system timeout for the long press by calling `android.view.ViewConfiguration.getLongPressTimeout()`. – rekire Mar 17 '14 at 13:30
  • Just perfect! @seb you can just use a boolean and set it in the runnable and unset it at ACTION_UP – otaku Feb 10 '15 at 16:52
  • 2
    I have no idea why but both solutions don't work for me. – Eido95 Dec 19 '16 at 20:07
  • @MSquare tap on the view also would fire the long press event right? – hemanth kumar Dec 20 '16 at 05:46
  • 2
    ACTION_CANCEL should be also been handled – Kirill Vashilo Jan 30 '17 at 07:14
  • For me, after a tap, the runnable is run, even though the log gives me a post and a remove. – Benjoyo Jan 31 '17 at 21:18
  • @KirillVashilo, ACTION_CANCEL you receive when a parent takes possession of the motion, so ACTION_MOVE will probably happen before. – MSquare Feb 08 '17 at 13:38
  • 2
    Thank you, @MSquare. This is a great idea. Note that for the device I'm using, I can remove the callback only in response to ACTION_UP; this is because it's very sensitive to movement, and I'm unable to prevent ACTION_MOVE events. Note too that your approach provides a natural way to implement a repeating action, by calling the Runnable recursively. – stevehs17 Jan 29 '19 at 01:17
  • 1
    @stevehs You need to test for touch slop. –  Feb 05 '20 at 01:38
  • Thanks but no, GestureDetector is not the best solution. It is one solution, others exist and arguable better. –  Jul 04 '21 at 12:40
  • I agree with @francogrex and would like to add the improved gesture detection of GestureDetector. For example the proposed solution may not detect long clicks correctly on a real device (due to the finger moving a few pixels) but GestureDetector will. – Philipp Fahlteich Aug 22 '21 at 10:34
121

Try this:

final GestureDetector gestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() {
    public void onLongPress(MotionEvent e) {
        Log.e("", "Longpress detected");
    }
});

public boolean onTouchEvent(MotionEvent event) {
    return gestureDetector.onTouchEvent(event);
};
Daniel Fekete
  • 4,988
  • 3
  • 23
  • 23
  • Can Gesture Detector handle standard clicks aswell? – Jack Oct 27 '11 at 20:03
  • 31
    Note: if you already have an OnClickListener, it is much easier to add an OnLongClickListener than to use a GestureRecognizer (there are some problems specific to GR's that you can avoid). c.f. http://stackoverflow.com/a/4402854/153422 – Adam Apr 20 '13 at 17:42
  • 11
    Note that you should provide a context on the creation of GestureDetector as first argument. The example is deprecated. – Antzi Oct 09 '13 at 12:19
  • To avoid subclassing a view, use [View#setOnTouchListener()](https://developer.android.com/reference/android/view/View.html#setOnTouchListener(android.view.View.OnTouchListener)). See "Capturing touch events for a single view" in [Detecting Common Gestures](https://developer.android.com/training/gestures/detector.html#data). – Pang Apr 10 '14 at 05:12
  • I have a ImageView save and cancel buttons with in Relative Layout..I want to set onLongPress for ImageView only not buttons.My problem is the onLongPress set the Hole activity .. – reegan29 May 12 '15 at 13:47
  • 1
    @Modge only t wo of `GestureDetector` constructors are deprecated - you can use the rest of the constructors. – Eido95 Dec 21 '16 at 18:47
8

I have a code which detects a click, a long click and movement. It is fairly a combination of the answer given above and the changes i made from peeping into every documentation page.

//Declare this flag globally
boolean goneFlag = false;

//Put this into the class
final Handler handler = new Handler(); 
    Runnable mLongPressed = new Runnable() { 
        public void run() { 
            goneFlag = true;
            //Code for long click
        }   
    };

//onTouch code
@Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {    
        case MotionEvent.ACTION_DOWN:
            handler.postDelayed(mLongPressed, 1000);
            //This is where my code for movement is initialized to get original location.
            break;
        case MotionEvent.ACTION_UP:
            handler.removeCallbacks(mLongPressed);
            if(Math.abs(event.getRawX() - initialTouchX) <= 2 && !goneFlag) {
                //Code for single click
                return false;
            }
            break;
        case MotionEvent.ACTION_MOVE:
            handler.removeCallbacks(mLongPressed);
            //Code for movement here. This may include using a window manager to update the view
            break;
        }
        return true;
    }

I confirm it's working as I have used it in my own application.

Sanved
  • 921
  • 13
  • 19
  • I went with this one, but had to set goneFlag to be false in the ACTION_DOWN before calling the handler.postDelayed, otherwise every click after the first long click is treated as a long click. – Jim Jimson Apr 14 '19 at 04:50
  • 1
    Does `initialTouchX` get defined automatically somewhere? I'm confused. – Divya Mamgai Jul 14 '20 at 01:38
4

I have created a snippet - inspired by the actual View source - that reliably detects long clicks/presses with a custom delay. But it's in Kotlin:

val LONG_PRESS_DELAY = 500

val handler = Handler()
var boundaries: Rect? = null

var onTap = Runnable {
    handler.postDelayed(onLongPress, LONG_PRESS_DELAY - ViewConfiguration.getTapTimeout().toLong())
}

var onLongPress = Runnable {

    // Long Press
}

override fun onTouch(view: View, event: MotionEvent): Boolean {
    when (event.action) {
        MotionEvent.ACTION_DOWN -> {
            boundaries = Rect(view.left, view.top, view.right, view.bottom)
            handler.postDelayed(onTap, ViewConfiguration.getTapTimeout().toLong())
        }
        MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
            handler.removeCallbacks(onLongPress)
            handler.removeCallbacks(onTap)
        }
        MotionEvent.ACTION_MOVE -> {
            if (!boundaries!!.contains(view.left + event.x.toInt(), view.top + event.y.toInt())) {
                handler.removeCallbacks(onLongPress)
                handler.removeCallbacks(onTap)
            }
        }
    }
    return true
}
Benjoyo
  • 413
  • 7
  • 13
3

When you mean user presses, do you mean a click? A click is when the user presses down and then immediately lifts up finger. Therefore it is encompassing two onTouch Events. You should save the use of onTouchEvent for stuff that happens on the initial touch or the after release.

Thus, you should be using onClickListener if it is a click.

Your answer is analogous: Use onLongClickListener.

Ian
  • 3,500
  • 1
  • 24
  • 25
  • 1
    Can I use onClickListener with just a GLSurfaceView? I was under the impression that you could only user onClickListener with UI widgets like buttons. Could be wrong though. – Jack Oct 27 '11 at 18:50
  • Ah I see it cool man, but is there any way to get the coordinates of the click through onClickListener – Jack Oct 27 '11 at 19:24
  • onClickListener doesn't seem to work for GLSurfaceView even though I have it wrapped in a FrameLayout. I tried set the listener on the surface view itself and the frame layout but it didn't work =/ – Jack Oct 27 '11 at 19:39
  • This question seems to be specific to GLSurfaceView, however, in general, for other Views, OnLongClickListener is the way to go. – Sarang Dec 16 '14 at 19:55
2

The solution by MSquare works only if you hold a specific pixel, but that's an unreasonable expectation for an end user unless they use a mouse (which they don't, they use fingers).

So I added a bit of a threshold for the distance between the DOWN and the UP action in case there was a MOVE action inbetween.

final Handler longPressHandler = new Handler();
Runnable longPressedRunnable = new Runnable() {
    public void run() {
        Log.e(TAG, "Long press detected in long press Handler!");
        isLongPressHandlerActivated = true;
    }
};

private boolean isLongPressHandlerActivated = false;

private boolean isActionMoveEventStored = false;
private float lastActionMoveEventBeforeUpX;
private float lastActionMoveEventBeforeUpY;

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    if(event.getAction() == MotionEvent.ACTION_DOWN) {
        longPressHandler.postDelayed(longPressedRunnable, 1000);
    }
    if(event.getAction() == MotionEvent.ACTION_MOVE || event.getAction() == MotionEvent.ACTION_HOVER_MOVE) {
        if(!isActionMoveEventStored) {
            isActionMoveEventStored = true;
            lastActionMoveEventBeforeUpX = event.getX();
            lastActionMoveEventBeforeUpY = event.getY();
        } else {
            float currentX = event.getX();
            float currentY = event.getY();
            float firstX = lastActionMoveEventBeforeUpX;
            float firstY = lastActionMoveEventBeforeUpY;
            double distance = Math.sqrt(
                    (currentY - firstY) * (currentY - firstY) + ((currentX - firstX) * (currentX - firstX)));
            if(distance > 20) {
                longPressHandler.removeCallbacks(longPressedRunnable);
            }
        }
    }
    if(event.getAction() == MotionEvent.ACTION_UP) {
        isActionMoveEventStored = false;
        longPressHandler.removeCallbacks(longPressedRunnable);
        if(isLongPressHandlerActivated) {
            Log.d(TAG, "Long Press detected; halting propagation of motion event");
            isLongPressHandlerActivated = false;
            return false;
        }
    }
    return super.dispatchTouchEvent(event);
}
EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
1

The idea is creating a Runnable for execute long click in a future, but this execution can be canceled because of a click, or move.

You also need to know, when long click was consumed, and when it is canceled because finger moved too much. We use initialTouchX & initialTouchY for checking if the user exit a square area of 10 pixels, 5 each side.

Here is my complete code for delegating Click & LongClick from Cell in ListView to Activity with OnTouchListener:

    ClickDelegate delegate; 
    boolean goneFlag = false;
    float initialTouchX;
    float initialTouchY;
    final Handler handler = new Handler();
    Runnable mLongPressed = new Runnable() {
        public void run() {
            Log.i("TOUCH_EVENT", "Long press!");
            if (delegate != null) {
                goneFlag = delegate.onItemLongClick(index);
            } else {
                goneFlag = true;
            }
        }
    };

    @OnTouch({R.id.layout})
    public boolean onTouch (View view, MotionEvent motionEvent) {
        switch (motionEvent.getAction()) {
            case MotionEvent.ACTION_DOWN:
                handler.postDelayed(mLongPressed, ViewConfiguration.getLongPressTimeout());
                initialTouchX = motionEvent.getRawX();
                initialTouchY = motionEvent.getRawY();
                return true;
            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_CANCEL:
                if (Math.abs(motionEvent.getRawX() - initialTouchX) > 5 || Math.abs(motionEvent.getRawY() - initialTouchY) > 5) {
                    handler.removeCallbacks(mLongPressed);
                    return true;
                }
                return false;
            case MotionEvent.ACTION_UP:
                handler.removeCallbacks(mLongPressed);
                if (goneFlag || Math.abs(motionEvent.getRawX() - initialTouchX) > 5 || Math.abs(motionEvent.getRawY() - initialTouchY) > 5) {
                    goneFlag = false;
                    return true;
                }
                break;
        }
        Log.i("TOUCH_EVENT", "Short press!");
        if (delegate != null) {
            if (delegate.onItemClick(index)) {
                return false;
            }
        }
        return false;
    }

ClickDelegateis an interface for sending click events to the handler class like an Activity

    public interface ClickDelegate {
        boolean onItemClick(int position);
        boolean onItemLongClick(int position);
    }

And all what you need is to implement it in your Activity or parent Viewif you need to delegate the behavior:

public class MyActivity extends Activity implements ClickDelegate {

    //code...
    //in some place of you code like onCreate, 
    //you need to set the delegate like this:
    SomeArrayAdapter.delegate = this;
    //or:
    SomeViewHolder.delegate = this;
    //or:
    SomeCustomView.delegate = this;

    @Override
    public boolean onItemClick(int position) {
        Object obj = list.get(position);
        if (obj) {
            return true; //if you handle click
        } else {
            return false; //if not, it could be another event
        }
    }

    @Override
    public boolean onItemLongClick(int position) {
        Object obj = list.get(position);
        if (obj) {
            return true; //if you handle long click
        } else {
            return false; //if not, it's a click
        }
    }
}
IgniteCoders
  • 4,834
  • 3
  • 44
  • 62
0
setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {

                int action = MotionEventCompat.getActionMasked(event);


                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        longClick = false;
                        x1 = event.getX();
                        break;

                    case MotionEvent.ACTION_MOVE:
                        if (event.getEventTime() - event.getDownTime() > 500 && Math.abs(event.getX() - x1) < MIN_DISTANCE) {
                            longClick = true;
                        }
                        break;

                    case MotionEvent.ACTION_UP:
                                if (longClick) {
                                    Toast.makeText(activity, "Long preess", Toast.LENGTH_SHORT).show();
                                } 
                }
                return true;
            }
        });
David Arenburg
  • 91,361
  • 17
  • 137
  • 196
0

Here is an approach, based on MSquare's nice idea for detecting a long press of a button, that has an additional feature: not only is an operation performed in response to a long press, but the operation is repeated until a MotionEvent.ACTION_UP message is received. In this case, the long-press and short-press actions are the same, but they could be different.

Note that, as others have reported, removing the callback in response to a MotionEvent.ACTION_MOVE message prevented the callback from ever getting executed since I could not keep my finger still enough. I got around that problem by ignoring that message.

private void setIncrementButton() {
    final Button btn = (Button) findViewById(R.id.btn);
    final Runnable repeater = new Runnable() {
        @Override
        public void run() {
            increment();
            final int milliseconds = 100;
            btn.postDelayed(this, milliseconds);
        }
    };
    btn.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent e) {
            if (e.getAction() == MotionEvent.ACTION_DOWN) {
                increment();
                v.postDelayed(repeater, ViewConfiguration.getLongPressTimeout());
            } else if (e.getAction() == MotionEvent.ACTION_UP) {
                v.removeCallbacks(repeater);
            }
            return true;
        }
    });
}

private void increment() {
    Log.v("Long Press Example", "TODO: implement increment operation");   
}
stevehs17
  • 1,466
  • 2
  • 14
  • 19
0

option: custom detector class

abstract public class
Long_hold
extends View.OnTouchListener
{
  public@Override boolean
  onTouch(View view, MotionEvent touch)
  {
    switch(touch.getAction())
    {
    case ACTION_DOWN: down(touch); return true;
    case ACTION_MOVE: move(touch);
    }
    return true;
  }

  private long
  time_0;
  private float
  x_0, y_0;

  private void
  down(MotionEvent touch)
  {
    time_0= touch.getEventTime();
    x_0= touch.getX();
    y_0= touch.getY();
  }

  private void
  move(MotionEvent touch)
  {
    if(held_too_short(touch) {return;}
    if(moved_too_much(touch)) {return;}

    long_press(touch);
  }
  abstract protected void
  long_hold(MotionEvent touch);
}

use

private double
moved_too_much(MotionEvent touch)
{
  return Math.hypot(
      x_0 -touch.getX(),
      y_0 -touch.getY()) >TOLERANCE;
}

private double
held_too_short(MotionEvent touch)
{
  return touch.getEventTime()-time_0 <DOWN_PERIOD;
}

where

  • TOLERANCE is the maximum tolerated movement

  • DOWN_PERIOD is the time one has to press

import

static android.view.MotionEvent.ACTION_MOVE;
static android.view.MotionEvent.ACTION_DOWN;

in code

setOnTouchListener(new Long_hold()
  {
  protected@Override boolean
  long_hold(MotionEvent touch)
  {
    /*your code on long hold*/
  }
});
lue
  • 449
  • 5
  • 16
-3

I found one solution and it does not require to define runnable or other things and it's working fine.

    var lastTouchTime: Long = 0

    // ( ViewConfiguration.#.DEFAULT_LONG_PRESS_TIMEOUT =500)
    val longPressTime = 500

    var lastTouchX = 0f
    var lastTouchY = 0f

    view.setOnTouchListener { v, event ->

        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                lastTouchTime = SystemClock.elapsedRealtime()
                lastTouchX = event.x
                lastTouchY = event.y
                return@setOnTouchListener true
            }
            MotionEvent.ACTION_UP -> {
                if (SystemClock.elapsedRealtime() - lastTouchTime > longPressTime
                        && Math.abs(event.x - lastTouchX) < 3
                        && Math.abs(event.y - lastTouchY) < 3) {
                    Log.d(TAG, "Long press")
                }
                return@setOnTouchListener true
            }
            else -> {
                return@setOnTouchListener false
            }
        }

    }
kinjal patel
  • 585
  • 2
  • 4
  • 12