Long Press and Drag with Low Level Events

Basic Tutorials concerning: GUI, Views, Activites, XML, Layouts, Intents, ...

Long Press and Drag with Low Level Events

Postby XCaffeinated » Sat Feb 27, 2010 5:01 am

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.
Tested with Android 2.0 and 2.0.1 platforms on Droid handset (firmware: Android 2.0.1).

:?: Problems/Questions: Just ask...

Difficulty: 1.5 of 5 :)

What it looks like:
Image


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:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. /* Check to see if we are holding an icon. If
  2.    so, set flag and let the move handler handle
  3.    icon dragging. If not, pop a context menu. */
  4. void handleLongPress() {
  5.         /* Indicate that a long-press has fired. */
  6.         isLongPress = true;
  7.  
  8.         /* To simplify, we are only using one icon. */
  9.         if (((downX < bmRect.right) && (downX > bmRect.left))
  10.                         && ((downY < bmRect.bottom) && (downY > bmRect.top)))
  11.         {
  12.                 isIconDrag = true;
  13.                 vibrator.vibrate(50);
  14.         } else {
  15.                 this.performLongClick();
  16.         }
  17. }
Parsed in 0.031 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:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. case MotionEvent.ACTION_MOVE:
  2.         /* A long-press has fired and is in progress. See if we
  3.            are 'attached' to an icon. If so, set the drag updates
  4.            for the icon. */
  5.         if (isLongPress) {
  6.                 if (isIconDrag) {
  7.                         final float x = event.getRawX();
  8.                         final float y = event.getRawY();
  9.                         dragIconByX = x - startX; // calculate move increments
  10.                         dragIconByY = y - startY;
  11.                         startX = x; // reset previous values to latest
  12.                         startY = y;
  13.                         invalidate();
  14.                 }
  15.                 break;
  16.         }
  17.  
  18.         /* Handle our 'have we moved' check. This will tell us
  19.            whether this gesture can still be a long press. */
  20.         if (hasNotMoved) {
  21.                 final float x = event.getRawX();
  22.                 final float y = event.getRawY();
  23.                 /* Have we moved out of the threshold radius of initial
  24.                    user touch? */
  25.                 final int deltaXFromDown = (int) (x - downX);
  26.                 final int deltaYFromDown = (int) (y - downY);
  27.                 int distance = (deltaXFromDown * deltaXFromDown)
  28.                                 + (deltaYFromDown * deltaYFromDown);
  29.                 if (distance > scaledTouchSlopSquared) {
  30.                         /* We've moved so cancel long-press. */
  31.                         hasNotMoved = false;
  32.                         messageHandler.removeMessages(MSG_LONG_PRESS);
  33.                 }
  34.         }
  35.         break;
Parsed in 0.034 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:
Syntax: [ Download ] [ Hide ]
Using xml Syntax Highlighting
  1. 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):
Syntax: [ Download ] [ Hide ]
Using xml Syntax Highlighting
  1. android:debuggable="true"
Parsed in 0.000 seconds, using GeSHi 1.0.8.4

In the activity tag, set the screen orientation to landscape:
Syntax: [ Download ] [ Hide ]
Using xml Syntax Highlighting
  1. 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:
Syntax: [ Download ] [ Hide ]
Using xml Syntax Highlighting
  1. <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.
The Full Source:

"/src/your_package_structure/LongPressDrag.java"
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. package com.example.longpressdrag;
  2.  
  3. import android.app.Activity;
  4. import android.os.Bundle;
  5. import android.os.Handler;
  6. import android.os.Message;
  7. import android.os.Vibrator;
  8. import android.content.Context;
  9. import android.graphics.Bitmap;
  10. import android.graphics.BitmapFactory;
  11. import android.graphics.Canvas;
  12. import android.graphics.Paint;
  13. import android.graphics.Rect;
  14. import android.view.ContextMenu;
  15. import android.view.Menu;
  16. import android.view.MotionEvent;
  17. import android.view.View;
  18. import android.view.ViewConfiguration;
  19. import android.view.ContextMenu.ContextMenuInfo;
  20.  
  21. public class LongPressDrag extends Activity {
  22.  
  23.         private static Vibrator vibrator = null;
  24.         private static int bmWidth = 0;
  25.         private static int bmHeight = 0;
  26.  
  27.         /** Called when the activity is first created. */
  28.         @Override
  29.         public void onCreate(Bundle savedInstanceState) {
  30.                 super.onCreate(savedInstanceState);
  31.  
  32.                 vibrator = ((Vibrator) getSystemService(Context.VIBRATOR_SERVICE));
  33.  
  34.                 SampleView sv = new SampleView(this);
  35.                 registerForContextMenu(sv);
  36.                 setContentView(sv);
  37.         }
  38.        
  39.         public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
  40.              super.onCreateContextMenu(menu, v, menuInfo);
  41.                
  42.              menu.add(Menu.NONE, 0, Menu.NONE, "Item1");
  43.              menu.add(Menu.NONE, 1, Menu.NONE, "Item2");
  44.         }
  45.  
  46.         private static class SampleView extends View {
  47.                 private static Bitmap bmBackground = null; // background image
  48.                 private static Bitmap bmIcon = null; // icon we will be dragging
  49.                 private Rect bmRect = null; // rect for our icon location
  50.                 private int bmRectX = 0; // current left location of bm rect
  51.                 private int bmRectY = 0; // current top location of bm rect
  52.                 private float dragIconByX = 0; // x amount to drag icon by
  53.                 private float dragIconByY = 0; // y amount to drag icon by
  54.                 private float startX = 0; // track x from one ACTION_MOVE to the next
  55.                 private float startY = 0; // track y from one ACTION_MOVE to the next
  56.                 private float downX = 0; // x cached at ACTION_DOWN
  57.                 private float downY = 0; // y cached at ACTION_DOWN
  58.  
  59.                 /* Is icon being dragged? Icon can only be dragged while a
  60.                    long press is in progress. */
  61.                 private boolean isIconDrag = false;
  62.  
  63.                 /* Only want one long press per gesture. */
  64.                 private boolean isLongPress = false;
  65.                 private boolean hasNotMoved = true; // long-press determination
  66.  
  67.                 /* Get static method data via static access
  68.                    to ViewConfiguration. */
  69.                 private static final long tapTime =
  70.                                                         ViewConfiguration.getTapTimeout();
  71.                 private static final long longPressTime =
  72.                                                         ViewConfiguration.getLongPressTimeout();
  73.                 private static int scaledTouchSlopSquared = 0;
  74.  
  75.                 private static final int MSG_LONG_PRESS = 1;
  76.  
  77.                 public SampleView(Context context) {
  78.                         super(context);
  79.  
  80.                         bmBackground = BitmapFactory.decodeResource(getResources(),
  81.                                         R.drawable.background);
  82.                         bmIcon = BitmapFactory.decodeResource(getResources(),
  83.                                         R.drawable.testicon);
  84.                         bmWidth = bmIcon.getWidth();
  85.                         bmHeight = bmIcon.getHeight();
  86.                        
  87.                         /* Rect we move our icon around with. */
  88.                         bmRect = new Rect(0, 0, bmWidth, bmHeight);
  89.  
  90.                         /* Get non-static method data from ViewConfiguration. */
  91.                         ViewConfiguration viewConfiguration =
  92.                                                         ViewConfiguration.get(context);
  93.                         scaledTouchSlopSquared = viewConfiguration.getScaledTouchSlop()
  94.                                         * viewConfiguration.getScaledTouchSlop();
  95.                 }
  96.  
  97.                 private Handler messageHandler = new Handler() {
  98.                
  99.                         @Override
  100.                         public void handleMessage(Message msg) {
  101.                                 switch (msg.what) {
  102.                                         /* Schedule a long press. This will be canceled if:
  103.                                            - gesture finishes (ACTION_UP)
  104.                                            - gesture is canceled (ACTION_CANCEL)
  105.                                            - we move outside of our 'slop' range */
  106.                                         case MSG_LONG_PRESS:
  107.                                                 handleLongPress();
  108.                                                 break;
  109.                                
  110.                                         default:
  111.                                                 throw new RuntimeException("handleMessage: unknown message "
  112.                                                                 + msg);
  113.                                 }
  114.                         }
  115.                 };
  116.  
  117.                 @Override
  118.                 public boolean onTouchEvent(MotionEvent event) {
  119.  
  120.                         switch (event.getAction()) {
  121.                                 case MotionEvent.ACTION_DOWN:
  122.                                         /* Remember our initial down event location. */
  123.                                         startX = downX = event.getRawX();
  124.                                         startY = downY = event.getRawY();
  125.                                         /* We will set this once a long-press actually fires. */
  126.                                         isLongPress = false;
  127.                                         /* Assume this is a long-press, it will be canceled
  128.                                            either by moving or by canceling the gesture. */
  129.                                         messageHandler.removeMessages(MSG_LONG_PRESS);
  130.                                         messageHandler.sendEmptyMessageAtTime(MSG_LONG_PRESS,
  131.                                                         event.getDownTime()     + tapTime + longPressTime);
  132.                                         break;
  133.        
  134.                                 case MotionEvent.ACTION_MOVE:
  135.                                         /* A long-press has fired and is in progress. See if we
  136.                                            are 'attached' to an icon. If so, set the drag updates
  137.                                            for the icon. */
  138.                                         if (isLongPress) {
  139.                                                 if (isIconDrag) {
  140.                                                         final float x = event.getRawX();
  141.                                                         final float y = event.getRawY();
  142.                                                         dragIconByX = x - startX; // calculate move increments
  143.                                                         dragIconByY = y - startY;
  144.                                                         startX = x; // reset previous values to latest
  145.                                                         startY = y;
  146.                                                         invalidate();
  147.                                                 }
  148.                                                 break;
  149.                                         }
  150.                                
  151.                                         /* Handle our 'have we moved' check. This will tell us
  152.                                            whether this gesture can still be a long press. */
  153.                                         if (hasNotMoved) {
  154.                                                 final float x = event.getRawX();
  155.                                                 final float y = event.getRawY();
  156.                                                 /* Have we moved out of the threshold radius of initial
  157.                                                    user touch? */
  158.                                                 final int deltaXFromDown = (int) (x - downX);
  159.                                                 final int deltaYFromDown = (int) (y - downY);
  160.                                                 int distance = (deltaXFromDown * deltaXFromDown)
  161.                                                                 + (deltaYFromDown * deltaYFromDown);
  162.                                                 if (distance > scaledTouchSlopSquared) {
  163.                                                         /* We've moved so cancel long-press. */
  164.                                                         hasNotMoved = false;
  165.                                                         messageHandler.removeMessages(MSG_LONG_PRESS);
  166.                                                 }
  167.                                         }
  168.                                         break;
  169.  
  170.                         case MotionEvent.ACTION_UP:
  171.                                 isLongPress = false;
  172.                                 hasNotMoved = true;
  173.                                 isIconDrag = false;
  174.                                 messageHandler.removeMessages(MSG_LONG_PRESS);
  175.                                 break;
  176.  
  177.                         case MotionEvent.ACTION_CANCEL:
  178.                                 isLongPress = false;
  179.                                 hasNotMoved = true;
  180.                                 isIconDrag = false;
  181.                                 messageHandler.removeMessages(MSG_LONG_PRESS);
  182.                                 break;
  183.                         }
  184.                         return true; // done with this event so consume it
  185.                 }
  186.  
  187.                 /* Check to see if we are holding an icon. If
  188.                    so, set flag and let the move handler handle
  189.                    icon dragging. If not, pop a context menu. */
  190.                 void handleLongPress() {
  191.                         /* Indicate that a long-press has fired. */
  192.                         isLongPress = true;
  193.                
  194.                         /* To simplify, we are only using one icon. */
  195.                         if (((downX < bmRect.right) && (downX > bmRect.left))
  196.                                         && ((downY < bmRect.bottom) && (downY > bmRect.top)))
  197.                         {
  198.                                 isIconDrag = true;
  199.                                 vibrator.vibrate(50);
  200.                         } else {
  201.                                 this.performLongClick();
  202.                         }
  203.                 }
  204.  
  205.                 @Override
  206.                 protected void onDraw(Canvas canvas) {
  207.  
  208.                         int newBmRectX = bmRectX + (int) dragIconByX;
  209.                         int newBmRectY = bmRectY + (int) dragIconByY;
  210.  
  211.                         bmRect.set(newBmRectX, newBmRectY, newBmRectX + bmWidth,
  212.                                                                         newBmRectY + bmHeight);
  213.                         Paint paint = new Paint();
  214.                         /* Draw the background and the icon on top. This isn't the
  215.                            best way to do this, particularly if we have more than one
  216.                            icon, but for simplicity we'll take this easy approach. */
  217.                         canvas.drawBitmap(bmBackground, 0, 0, paint);
  218.                         canvas.drawBitmap(bmIcon, null, bmRect, paint);
  219.  
  220.                         bmRectX = newBmRectX; // reset previous to latest
  221.                         bmRectY = newBmRectY;
  222.                 }
  223.         }
  224. }
Parsed in 0.062 seconds, using GeSHi 1.0.8.4

Hope this helps!
Xcaf
XCaffeinated
Developer
Developer
 
Posts: 25
Joined: Sun Nov 29, 2009 10:16 pm

Top

Postby bengtb » Mon Mar 01, 2010 11:22 pm

This was interesting, but the first time I looked at 2D graphics on Android.

I wanted to make a change so that the icon was originally centered instead up in the upper left corner. Unfortunately the dimensions of the view is not set in the constructor and when setting startX and startY in onSizeChanged(...) it had no effect.

How do I get the dimensions of the view in a way so that I can position the icon in the middle of the screen at start?

Thank you
/B
bengtb
Freshman
Freshman
 
Posts: 2
Joined: Mon Mar 01, 2010 11:15 pm

Postby XCaffeinated » Mon Mar 01, 2010 11:50 pm

Hi Bengtb,

If you take a look at the first tutorial link, you can see how to get the display width and height in onCreate(). Then you'll be able to set your icon where you wish. Also if you need to handle screen orientation changes you can get updated values for the display height and width in onSizeChanged().

-XCaf
XCaffeinated
Developer
Developer
 
Posts: 25
Joined: Sun Nov 29, 2009 10:16 pm

Re: Long Press and Drag with Low Level Events

Postby JimBadger » Sun Sep 12, 2010 12:01 am

Fantastic stuff this, thank you so much.

When talking about drawing the draggable icon on top of the background, you say:

"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."

May I ask why this is not the best approach? I will be having upto 20 draggable items on screen that can be "sifted" through by the user, so they will constantly be in a heap, or lying next to each other, or partially covering each other, and so on.

What is it about your "simple" approach that is bad?

Also, I'd like to restrit the draggable area to a specific portion of the screen, are you able to tell me how that is possible?
JimBadger
Freshman
Freshman
 
Posts: 7
Joined: Fri Jul 16, 2010 11:56 pm

Top

Return to Novice Tutorials

Who is online

Users browsing this forum: No registered users and 9 guests