Long Press and Drag with Low Level Events
What you will learn: You will learn how to implement a 'long press and drag' operation on an icon, or if an icon is not present where the long press occurs, a context menu will be popped instead. This is intended to emulate the Home page behavior on some devices, where you can ‘detach’ an icon on long press or pop a ‘widgets/wallpaper/whatever’ context menu if you are long pressing the background.
Target readers - this tutorial is for you if:
- You are a game (or other performance-driven) developer looking for low level control.
- You are having trouble getting your long presses and touch event handlers to coexist.
- You don't want to or can't use GestureDetector for your particular application.
Difficulty: 1.5 of 5
What it looks like:

Background: This code is based on my previous tutorials:
http://www.anddev.org/large_image_scrolling_using_low_level_touch_events-t11182.html
http://www.anddev.org/long-press_and_scroll_large_images_using_low_level_events-t11285.html
In those tutorials I explained how to use low level touch events to handle long-press and screen scrolling. My implementation was such that a long-press would end the current gesture. In other words, once you long pressed you would no longer be able to scroll. My thinking was that real world applications would mostly want to pop a context menu or similar, and that would start a new gesture.
However, boyFromAuz came up with an excellent example of a real world application where this would not be the case. He specifically asked about emulating the Home page functionality whereby a long-press on an icon would ‘detach’ it and the icon would then become draggable.
Here you are Nick
. For you, and those interested in this kind of behavior; I’ve modified the code from the second tutorial link above, and since you specifically mentioned Home page emulation I’ve also included code that will cause a long press on the background to pop a context menu. For those who don’t want to wade through both tutorials I’ve listed the whole procedure below for getting this code to build. All explanations are in those tutorials though and won’t be repeated here. In particular I will be referring to that tutorial’s long press handler.
The big change to the new code is that I’ve replaced the background scrolling with icon dragging. You can certainly have both, but that would complicate the code beyond what I’m trying to present.
The handler we use to trigger long presses doesn’t change, only what happens once we successfully trigger a long press event. In particular, handleLongPress() becomes:
Using java Syntax Highlighting
- /* Check to see if we are holding an icon. If
- so, set flag and let the move handler handle
- icon dragging. If not, pop a context menu. */
- void handleLongPress() {
- /* Indicate that a long-press has fired. */
- isLongPress = true;
- /* To simplify, we are only using one icon. */
- if (((downX < bmRect.right) && (downX > bmRect.left))
- && ((downY < bmRect.bottom) && (downY > bmRect.top)))
- {
- isIconDrag = true;
- vibrator.vibrate(50);
- } else {
- this.performLongClick();
- }
- }
Parsed in 0.032 seconds, using GeSHi 1.0.8.4
When the handler triggers a long press (as determined in ACTION_MOVE) we set the isLongPress flag to indicate a long press is in progress. We check to see if we are within our icon’s boundaries (for this simple example, we just use one icon; you can of course have more). If our hit test is true we set another flag, isIconDrag, indicating that we want to drag our icon, and we exit our long press method. If our hit test fails we are long pressing the background and we instead pop a context menu via performLongClick(), as explained in the older tutorials.
We need minimal changes to the event handler.
ACTION_DOWN doesn’t change at all.
For ACTION_MOVE, instead of always exiting if a long press is in progress we now check to see if an icon drag is in progress. If so, we update the move incrementers for the icon and force a redraw.
Since we’ve removed background scrolling for this example, we also shorten up the long press determination code block. In particular we no longer have to worry about updating the background’s scroll rectangle.
The new ACTION_MOVE handler looks like:
Using java Syntax Highlighting
- case MotionEvent.ACTION_MOVE:
- /* A long-press has fired and is in progress. See if we
- are 'attached' to an icon. If so, set the drag updates
- for the icon. */
- if (isLongPress) {
- if (isIconDrag) {
- final float x = event.getRawX();
- final float y = event.getRawY();
- dragIconByX = x - startX; // calculate move increments
- dragIconByY = y - startY;
- startX = x; // reset previous values to latest
- startY = y;
- invalidate();
- }
- break;
- }
- /* Handle our 'have we moved' check. This will tell us
- whether this gesture can still be a long press. */
- if (hasNotMoved) {
- final float x = event.getRawX();
- final float y = event.getRawY();
- /* Have we moved out of the threshold radius of initial
- user touch? */
- final int deltaXFromDown = (int) (x - downX);
- final int deltaYFromDown = (int) (y - downY);
- int distance = (deltaXFromDown * deltaXFromDown)
- + (deltaYFromDown * deltaYFromDown);
- if (distance > scaledTouchSlopSquared) {
- /* We've moved so cancel long-press. */
- hasNotMoved = false;
- messageHandler.removeMessages(MSG_LONG_PRESS);
- }
- }
- break;
Parsed in 0.033 seconds, using GeSHi 1.0.8.4
The only change to the ACTION_UP and ACTION_CANCEL handlers is to reset isIconDrag, since our gesture is now complete.
Our onDraw() method now handles updating of the icon’s position, instead of handling background scrolling. You can see the changed onDraw() in the full source below.
I’ve changed the variables names to indicate icon dragging instead of background scrolling, but most functionality is identical to the older tutorials. This code is not very different from the previous version (in some cases this example is simpler), but with the variable name changes it is probably not useful to run a diff utility on the two versions.
Here is a summary of the steps to implement a full project. Complete details and code explanations for anything not discussed above are in the referenced tutorials.
Implementation Summary:
The full source is available at the end.
0.) In Eclipse, create a new Android Project, targeting Android 2.0 (older versions may work too, but the folders may be slightly different from those shown here). For consistency with this tutorial you may wish to name your main activity LongPressDrag, and make your package name com.example.longpressdrag.
1.) You’ll need two image resources, one the size of your screen for the background (or you can just leave it out completely and remove the appropriate bmBackground references from the code below). For example, for my Droid I used a 854x480 image. The other resource is a small bitmap representing an icon. In a pinch you can just use the standard icon.png that is built into all Eclipse projects.
Add the image resources (I've named mine background.png and testicon.png) to your /drawable-hdpi folder (may also be named /drawable depending on which Android version you are using).
2.) For convenience, we will run our simple application fullscreen and in landscape mode. To do this modify the project's manifest:
Edit AndroidManifest.xml and add the following to the application tag:
Using xml Syntax Highlighting
- android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
Parsed in 0.000 seconds, using GeSHi 1.0.8.4
While we are here you can also add the following if you wish to debug on an actual device (this also goes inside the application tag):
Using xml Syntax Highlighting
- android:debuggable="true"
Parsed in 0.000 seconds, using GeSHi 1.0.8.4
In the activity tag, set the screen orientation to landscape:
Using xml Syntax Highlighting
- android:screenOrientation="landscape"
Parsed in 0.000 seconds, using GeSHi 1.0.8.4
We must also add the proper permission to use the vibration service. In the manifest tag (above the application tag) add:
Using xml Syntax Highlighting
- <uses-permission android:name="android.permission.VIBRATE" />
Parsed in 0.000 seconds, using GeSHi 1.0.8.4
3.) Cut and paste the full source code below into your project’s LongPressDrag.java, replacing whatever was in there. You should be able to build and run this example, and be able to either long press the icon and drag it, or long press the background and pop a context menu.
Takeaways: The Takeaways from this tutorial are the same as previously, the most important are repeated here:
- Make sure GestureDetector doesn't fit your needs before attempting to roll your own.
- Use handlers to set up event triggers; let the Android OS scheduler do the work for you.
- To avoid poor performance, try to keep the code that executes from within the event handlers to a minimum.
"/src/your_package_structure/LongPressDrag.java"
Using java Syntax Highlighting
- package com.example.longpressdrag;
- import android.app.Activity;
- import android.os.Bundle;
- import android.os.Handler;
- import android.os.Message;
- import android.os.Vibrator;
- import android.content.Context;
- import android.graphics.Bitmap;
- import android.graphics.BitmapFactory;
- import android.graphics.Canvas;
- import android.graphics.Paint;
- import android.graphics.Rect;
- import android.view.ContextMenu;
- import android.view.Menu;
- import android.view.MotionEvent;
- import android.view.View;
- import android.view.ViewConfiguration;
- import android.view.ContextMenu.ContextMenuInfo;
- public class LongPressDrag extends Activity {
- private static Vibrator vibrator = null;
- private static int bmWidth = 0;
- private static int bmHeight = 0;
- /** Called when the activity is first created. */
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- vibrator = ((Vibrator) getSystemService(Context.VIBRATOR_SERVICE));
- SampleView sv = new SampleView(this);
- registerForContextMenu(sv);
- setContentView(sv);
- }
- public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
- super.onCreateContextMenu(menu, v, menuInfo);
- menu.add(Menu.NONE, 0, Menu.NONE, "Item1");
- menu.add(Menu.NONE, 1, Menu.NONE, "Item2");
- }
- private static class SampleView extends View {
- private static Bitmap bmBackground = null; // background image
- private static Bitmap bmIcon = null; // icon we will be dragging
- private Rect bmRect = null; // rect for our icon location
- private int bmRectX = 0; // current left location of bm rect
- private int bmRectY = 0; // current top location of bm rect
- private float dragIconByX = 0; // x amount to drag icon by
- private float dragIconByY = 0; // y amount to drag icon by
- private float startX = 0; // track x from one ACTION_MOVE to the next
- private float startY = 0; // track y from one ACTION_MOVE to the next
- private float downX = 0; // x cached at ACTION_DOWN
- private float downY = 0; // y cached at ACTION_DOWN
- /* Is icon being dragged? Icon can only be dragged while a
- long press is in progress. */
- private boolean isIconDrag = false;
- /* Only want one long press per gesture. */
- private boolean isLongPress = false;
- private boolean hasNotMoved = true; // long-press determination
- /* Get static method data via static access
- to ViewConfiguration. */
- private static final long tapTime =
- ViewConfiguration.getTapTimeout();
- private static final long longPressTime =
- ViewConfiguration.getLongPressTimeout();
- private static int scaledTouchSlopSquared = 0;
- private static final int MSG_LONG_PRESS = 1;
- public SampleView(Context context) {
- super(context);
- bmBackground = BitmapFactory.decodeResource(getResources(),
- R.drawable.background);
- bmIcon = BitmapFactory.decodeResource(getResources(),
- R.drawable.testicon);
- bmWidth = bmIcon.getWidth();
- bmHeight = bmIcon.getHeight();
- /* Rect we move our icon around with. */
- bmRect = new Rect(0, 0, bmWidth, bmHeight);
- /* Get non-static method data from ViewConfiguration. */
- ViewConfiguration viewConfiguration =
- ViewConfiguration.get(context);
- scaledTouchSlopSquared = viewConfiguration.getScaledTouchSlop()
- * viewConfiguration.getScaledTouchSlop();
- }
- private Handler messageHandler = new Handler() {
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- /* Schedule a long press. This will be canceled if:
- - gesture finishes (ACTION_UP)
- - gesture is canceled (ACTION_CANCEL)
- - we move outside of our 'slop' range */
- case MSG_LONG_PRESS:
- handleLongPress();
- break;
- default:
- throw new RuntimeException("handleMessage: unknown message "
- + msg);
- }
- }
- };
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- /* Remember our initial down event location. */
- startX = downX = event.getRawX();
- startY = downY = event.getRawY();
- /* We will set this once a long-press actually fires. */
- isLongPress = false;
- /* Assume this is a long-press, it will be canceled
- either by moving or by canceling the gesture. */
- messageHandler.removeMessages(MSG_LONG_PRESS);
- messageHandler.sendEmptyMessageAtTime(MSG_LONG_PRESS,
- event.getDownTime() + tapTime + longPressTime);
- break;
- case MotionEvent.ACTION_MOVE:
- /* A long-press has fired and is in progress. See if we
- are 'attached' to an icon. If so, set the drag updates
- for the icon. */
- if (isLongPress) {
- if (isIconDrag) {
- final float x = event.getRawX();
- final float y = event.getRawY();
- dragIconByX = x - startX; // calculate move increments
- dragIconByY = y - startY;
- startX = x; // reset previous values to latest
- startY = y;
- invalidate();
- }
- break;
- }
- /* Handle our 'have we moved' check. This will tell us
- whether this gesture can still be a long press. */
- if (hasNotMoved) {
- final float x = event.getRawX();
- final float y = event.getRawY();
- /* Have we moved out of the threshold radius of initial
- user touch? */
- final int deltaXFromDown = (int) (x - downX);
- final int deltaYFromDown = (int) (y - downY);
- int distance = (deltaXFromDown * deltaXFromDown)
- + (deltaYFromDown * deltaYFromDown);
- if (distance > scaledTouchSlopSquared) {
- /* We've moved so cancel long-press. */
- hasNotMoved = false;
- messageHandler.removeMessages(MSG_LONG_PRESS);
- }
- }
- break;
- case MotionEvent.ACTION_UP:
- isLongPress = false;
- hasNotMoved = true;
- isIconDrag = false;
- messageHandler.removeMessages(MSG_LONG_PRESS);
- break;
- case MotionEvent.ACTION_CANCEL:
- isLongPress = false;
- hasNotMoved = true;
- isIconDrag = false;
- messageHandler.removeMessages(MSG_LONG_PRESS);
- break;
- }
- return true; // done with this event so consume it
- }
- /* Check to see if we are holding an icon. If
- so, set flag and let the move handler handle
- icon dragging. If not, pop a context menu. */
- void handleLongPress() {
- /* Indicate that a long-press has fired. */
- isLongPress = true;
- /* To simplify, we are only using one icon. */
- if (((downX < bmRect.right) && (downX > bmRect.left))
- && ((downY < bmRect.bottom) && (downY > bmRect.top)))
- {
- isIconDrag = true;
- vibrator.vibrate(50);
- } else {
- this.performLongClick();
- }
- }
- @Override
- protected void onDraw(Canvas canvas) {
- int newBmRectX = bmRectX + (int) dragIconByX;
- int newBmRectY = bmRectY + (int) dragIconByY;
- bmRect.set(newBmRectX, newBmRectY, newBmRectX + bmWidth,
- newBmRectY + bmHeight);
- Paint paint = new Paint();
- /* Draw the background and the icon on top. This isn't the
- best way to do this, particularly if we have more than one
- icon, but for simplicity we'll take this easy approach. */
- canvas.drawBitmap(bmBackground, 0, 0, paint);
- canvas.drawBitmap(bmIcon, null, bmRect, paint);
- bmRectX = newBmRectX; // reset previous to latest
- bmRectY = newBmRectY;
- }
- }
- }
Parsed in 0.061 seconds, using GeSHi 1.0.8.4
Hope this helps!
Xcaf


