68

I'm currently fiddling around with Android programming, but I have a small problem detecting different touch events, namely a normal touch press (press on the screen and release right away), a long press (touch the screen and hold the finger on it) and movement (dragging on the screen).

What I wanted to do is have an image (of a circle) on my screen which I can drag around. Then when I press it once (short/normal press) a Toast comes up with some basic information about it. When I long press it, an AlertDialog with a list comes up to select a different image (circle, rectangle or triangle).

I made a custom View with my own OnTouchListener to detect the events and draw the image in onDraw. The OnTouchListener.onTouch goes something like this:

// has a touch press started?
private boolean touchStarted = false;
// co-ordinates of image
private int x, y;

public boolean onTouch(View v, MotionEvent event) {
    int action = event.getAction();
    if (action == MotionEvent.ACTION_DOWN) {
        touchStarted = true;
    }
    else if (action == MotionEvent.ACTION_MOVE) {
        // movement: cancel the touch press
        touchStarted = false;

        x = event.getX();
        y = event.getY();

        invalidate(); // request draw
    }
    else if (action == MotionEvent.ACTION_UP) {
        if (touchStarted) {
            // touch press complete, show toast
            Toast.makeText(v.getContext(), "Coords: " + x + ", " + y, 1000).show();
        }
    }

    return true;
}

The problem is that the press doesn't quite work as expected, because when I casually touch the screen it also detects a tiny bit of movement and cancels the touch press and moves around the image instead.

I "hacked" around this a bit my introducing a new variable "mTouchDelay" which I set to 0 on ACTION_DOWN, increase in MOVE and if it's >= 3 in MOVE I execute my "move" code. But I have a feeling this isn't really the way to go.

I also haven't found out how to detect a long press. The culprit really is the MOVE which seems to always trigger.

For an example of what I roughly want, see the Android application "DailyStrip": it shows an image of a comic strip. You can drag it if it's too large for the screen. You can tap it once for some controls to pop-up and long press it for an options menu.

PS. I'm trying to get it to work on Android 1.5, since my phone only runs on 1.5.

Csharpest
  • 1,258
  • 14
  • 32
  • This needs a Java tag. However, if you ever figure this out in a web app, accessible through jQuery, I'm trying to figure out how to intercept long tap (tap hold, long press) as well. – Volomike Jan 17 '11 at 05:25

9 Answers9

95

This code can distinguish between click and movement (drag, scroll). In onTouchEvent set a flag isOnClick, and initial X, Y coordinates on ACTION_DOWN. Clear the flag on ACTION_MOVE (minding that unintentional movement is often detected which can be solved with a THRESHOLD const).

private float mDownX;
private float mDownY;
private final float SCROLL_THRESHOLD = 10;
private boolean isOnClick;

@Override
public boolean onTouchEvent(MotionEvent ev) {
    switch (ev.getAction() & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN:
            mDownX = ev.getX();
            mDownY = ev.getY();
            isOnClick = true;
            break;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            if (isOnClick) {
                Log.i(LOG_TAG, "onClick ");
                //TODO onClick code
            }
            break;
        case MotionEvent.ACTION_MOVE:
            if (isOnClick && (Math.abs(mDownX - ev.getX()) > SCROLL_THRESHOLD || Math.abs(mDownY - ev.getY()) > SCROLL_THRESHOLD)) {
                Log.i(LOG_TAG, "movement detected");
                isOnClick = false;
            }
            break;
        default:
            break;
    }
    return true;
}

For LongPress as suggested above, GestureDetector is the way to go. Check this Q&A:

Detecting a long press with Android

Community
  • 1
  • 1
MSquare
  • 6,311
  • 4
  • 31
  • 37
  • 1
    Anytime! :) Thanks for acknowledging. ;) – MSquare May 14 '13 at 14:42
  • 2
    What does the `& MotionEvent.ACTION_MASK` do? – CorayThan Jun 25 '14 at 04:06
  • MotionEvent.ACTION_MASK is a bit mask applied with bitwise AND operation. It filters out pointer index and just shows MotionEvent action. – MSquare Jul 07 '14 at 15:42
  • @MSquare. the code works fine. But I am using this code in my BaseAdaptor class and I want to enable my scroll when it is movement detecteda and show a Dialog when it's onClick event.. – Arun PS Dec 19 '14 at 14:55
  • 5
    This works great. Only thing for me was, I needed to get also events during the drag (when finger moves). This code only gives one event for move during whole drag. To get the required events, removing the "isOnClick &&" part did the trick.. – kg_sYy Jan 05 '15 at 23:04
  • Hi this works fine in my all devices but kitkat and lollipop. In both new os version, i can detect this event only once. plz help! – Reshma Feb 05 '15 at 08:43
  • 1
    I just want to take this time off my Android studio session and personally thank you for the above code. It saved me what was appearing to be a whole day wild goose chase. Thanks a lot. – nmvictor Jan 07 '16 at 17:09
  • How do you determine a good value for SCROLL_THRESHOLD ? Is getX() and getY in pixels ? If so, shouldn't SCROLL_THRESHOLD be different for different screen densities ? – Eino Gourdin Aug 03 '16 at 15:31
  • is 10 a perfect pixel count for any resolution device? – cegprakash Jan 20 '17 at 02:09
  • It worked for me, but I suggest experimenting. You can also convert x dp to pixels. – MSquare Jan 25 '17 at 10:51
  • I found it was easier to set the IsOnClick based on whether movement consumed the event and then disable it as a part of the ACTION_UP code when consuming it as a click, with it set to false as default. The other way caused too many issues or excessive code with being able to drag. – Abandoned Cart Apr 10 '17 at 17:21
  • Perfect! Really appreciate this answer, very nice! – Gustavo Baiocchi Costa Jun 01 '18 at 13:58
  • Thanks a lot! Was researching on it since hours, and your method worked! – Rohit Kumar Jul 12 '19 at 07:10
22

From the Android Docs -

onLongClick()

From View.OnLongClickListener. This is called when the user either touches and holds the item (when in touch mode), or focuses upon the item with the navigation-keys or trackball and presses and holds the suitable "enter" key or presses and holds down on the trackball (for one second).

onTouch()

From View.OnTouchListener. This is called when the user performs an action qualified as a touch event, including a press, a release, or any movement gesture on the screen (within the bounds of the item).

As for the "moving happens even when I touch" I would set a delta and make sure the View has been moved by at least the delta before kicking in the movement code. If it hasn't been, kick off the touch code.

Jason L.
  • 2,464
  • 1
  • 23
  • 41
  • With onLongClick the long click "kind of" works, but I still face two problems. First off, onLongClick triggers twice for one long click (I simply show a Toast and return true, and I get 2 Toasts). Also, when I press and hold I get the long click while holding, but when I release I still get the ACTION_UP from the onTouch. How can I prevent the ACTION_UP from onTouch after onLongClick has triggered? –  Dec 02 '10 at 13:15
  • The double onLongClick worries me, I'm not sure why it would be firing twice. As for the ACTION_UP still firing, I would ask why do you need to track ACTION_UP and ACTION_DOWN. Test to see if onTouch() gets fired when onLongClick() is fired. If not I'm not sure you need to capture those. You should only need onTouch() for when the item is actually touched and when it's moved. You can also look at using onClick() for "onTouch()" and using onTouch() for movement only. Not sure if that would meet your requirements, though. – Jason L. Dec 02 '10 at 15:52
15

I discovered this after a lot of experimentation.

In the initialisation of your activity:

setOnLongClickListener(new View.OnLongClickListener() {
  public boolean onLongClick(View view) {
    activity.openContextMenu(view);  
    return true;  // avoid extra click events
  }
});
setOnTouch(new View.OnTouchListener(){
  public boolean onTouch(View v, MotionEvent e){
    switch(e.getAction & MotionEvent.ACTION_MASK){
      // do drag/gesture processing. 
    }
    // you MUST return false for ACTION_DOWN and ACTION_UP, for long click to work
    // you can return true for ACTION_MOVEs that you consume. 
    // DOWN/UP are needed by the long click timer.
    // if you want, you can consume the UP if you have made a drag - so that after 
    // a long drag, no long-click is generated.
    return false;
  }
});
setLongClickable(true);
Vinayak Garg
  • 6,518
  • 10
  • 53
  • 80
Sanjay Manohar
  • 6,920
  • 3
  • 35
  • 58
  • what do you mean with 'consume the UP'? Sorry, newbie here. – Luis A. Florit Nov 04 '12 at 16:27
  • 2
    Oh - If I am sent an event, I can "consume" it, meaning that other hooks that are listening for the same event don't get called as well. For example, when I lift my hand from the screen, an "UP" message may be sent along the component hierarchy unless I consume it. – Sanjay Manohar Nov 08 '12 at 21:05
  • I see. I understand what you wrote in principle, but how do you actually 'consume', say, the 'UP' event? – Luis A. Florit Nov 08 '12 at 23:06
  • 1
    Ah - consuming an event is done simply by returning `true` from the `onTouch` or `onLongClick` function. The window's thread which calls this handler method checks, after dispatching the event to your function, whether you return T/F. – Sanjay Manohar Jun 19 '13 at 22:53
3

I was looking for a similar solution and this is what I would suggest. In the OnTouch method, record the time for MotionEvent.ACTION_DOWN event and then for MotionEvent.ACTION_UP, record the time again. This way you can set your own threshold also. After experimenting few times you will know the max time in millis it would need to record a simple touch and you can use this in move or other method as you like.

Hope this helped. Please comment if you used a different method and solved your problem.

rajankz
  • 644
  • 1
  • 5
  • 16
3

If you need to distniguish between a click, longpress and a scroll use GestureDetector

Activity implements GestureDetector.OnGestureListener

then create detector in onCreate for example

mDetector = new GestureDetectorCompat(getActivity().getApplicationContext(),this);

then optionally setOnTouchListener on your View (for example webview) where

onTouch(View v, MotionEvent event) {
return mDetector.onTouchEvent(event);
}

and now you can use Override onScroll, onFling, showPress( detect long press) or onSingleTapUp (detect a click)

voytez
  • 1,814
  • 16
  • 16
2

I think you should implement GestureDetector.OnGestureListener as described in Using GestureDetector to detect Long Touch, Double Tap, Scroll or other touch events in Android and androidsnippets and then implement tap logic in onSingleTapUp and move logic in onScroll events

Lord_JABA
  • 2,545
  • 7
  • 31
  • 58
0

I was just dealing with this mess after wanting longclick to not end with a click event.

Here's what I did.

public boolean onLongClick(View arg0) {
    Toast.makeText(getContext(), "long click", Toast.LENGTH_SHORT).show();
    longClicked = true;
    return false;
}

public void onClick(View arg0) {
    if(!longClicked){
        Toast.makeText(getContext(), "click", Toast.LENGTH_SHORT).show();
    }
    longClick = false; // sets the clickability enabled
}

boolean longClicked = false;

It's a bit of a hack but it works.

Mihai Iorga
  • 39,330
  • 16
  • 106
  • 107
Chris Sullivan
  • 576
  • 5
  • 10
  • 3
    very late, but maybe someone else reads this: if you return true in onLongClick, no onClick will be called, so no need for extra variables – Björn Kechel Nov 09 '15 at 10:00
0

it just needed a ontouch event in the overlay class. check this, it helped me

http://mirnauman.wordpress.com/2012/04/16/android-google-maps-tutorial-part-6-getting-the-location-that-is-touched/

Nakul Sudhakar
  • 1,574
  • 1
  • 24
  • 24
0

GestureDetector.SimpleOnGestureListener has methods to help in these 3 cases;

   GestureDetector gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {

        //for single click event.
        @Override
        public boolean onSingleTapUp(MotionEvent motionEvent) {
            return true;
        }

        //for detecting a press event. Code for drag can be added here.
        @Override
        public void onShowPress(MotionEvent e) {
            View child = recyclerView.findChildViewUnder(e.getX(), e.getY());
            ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
            ClipData clipData = ClipData.newPlainText("..", "...");
            clipboardManager.setPrimaryClip(clipData);

            ConceptDragShadowBuilder dragShadowBuilder = new CustomDragShadowBuilder(child);
            // drag child view.
            child.startDrag(clipData, dragShadowBuilder, child, 0);
        }

        //for detecting longpress event
        @Override
        public void onLongPress(MotionEvent e) {
            super.onLongPress(e);
        }
    });
Gene Bo
  • 11,284
  • 8
  • 90
  • 137
Subhanandh
  • 141
  • 1
  • 11