Long-press and Scroll Large Images Using Low Level Events

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

Long-press and Scroll Large Images Using Low Level Events

Postby XCaffeinated » Thu Feb 18, 2010 7:58 am

Long-press and Scroll Large Images Using Low Level Events

What you will learn: You will learn how to implement long-press and scroll in low level event handlers. Optionally you will learn how to pop a context menu on long-press.

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 clicks 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: 2 of 5 :)

What it looks like:
Image


Description: This tutorial combines image scrolling with long-press functionality. The image must fit into memory but be larger than the display surface so it is scrollable. Both scroll and long-press are implemented in a low level touch handler, allowing for greater control by the developer.

Background: (For those in a hurry who just need some working code, skip to the Full Source at the end).

For everyone else, the material here builds on my previous tutorial on scrolling large images, so you may wish to look at that first. If you are new to Android and/or event handling I recommend that you read it.

I am going to talk about what I tried first and why it didn't work. Hopefully this will help others avoid similar mistakes.

Failed attempt #1: The first thought that occurred to me was to grab a timestamp in the ACTION_DOWN event, and then get a time delta in the ACTION_UP event. If the time delta was greater than long_press_time, I'd execute my do_long_press code:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. ACTION_DOWN:
  2.         //...
  3.         downTime = event.getDownTime();
  4.         break;
  5.  
  6. ACTION_MOVE:
  7.         //...  
  8.         break;
  9.  
  10. ACTION_UP:
  11.         //...
  12.         if (event.getEventTime – downTime) > long_press_time {
  13.                 //do_long_press stuff
  14.         }      
  15.         break;
Parsed in 0.032 seconds, using GeSHi 1.0.8.4

Works great. There's just one problem – the long press code doesn't get executed until the user generates an ACTION_UP, meaning he can hold his finger down until the cows come home and the long press code won't get executed. What I really wanted was a way to get the long press code to execute as soon as one second had passed... which led me to:

Failed attempt #2: Basically I just moved the code in ACTION_UP to ACTION_MOVE, figuring that ACTION_MOVE events are generated often enough that after approximately one second my long press code would execute:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. ACTION_DOWN:
  2.         //...
  3.         downTime = event.getDownTime();
  4.         break;
  5.  
  6. ACTION_MOVE:
  7.         //...
  8.         if (event.getEventTime – downTime) > long_press_time {
  9.                 //do_long_press stuff
  10.         }      
  11.         else
  12.                 //handle move normally 
  13.         break;
  14.  
  15. ACTION_UP:
  16.         //...
  17.         break;
Parsed in 0.031 seconds, using GeSHi 1.0.8.4

Result? WRONGO! It turns out that for efficiency, if your finger isn't moving no ACTION_MOVE events are generated, and once again it may be a very long time, if ever, before the long press code gets executed.

Finally. Success: I hunted down the source code to GestureDetector.java. In hindsight I probably should have done this first. In that code, I found a real gem. I will lay out the solution here, and explain it shortly.

Declare a message handler:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. private Handler messageHandler = new Handler() {
  2.  
  3.         @Override
  4.         public void handleMessage(Message msg) {  
  5.                 switch(msg.what) {
  6.                         case LONG_PRESS:
  7.                                 //handle long press
  8.                                 do_long_press_stuff();
  9.                                 break;
  10.                 }
  11.         }
  12. };
Parsed in 0.034 seconds, using GeSHi 1.0.8.4

In the touch event handler, process events as:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. ACTION_DOWN:
  2.         //...
  3.         //send a message to the handler that will execute after long_press_delay
  4.         //amount of time
  5.         handler.sendMsgAfterADelay(LONG_PRESS, long_press_delay)
  6.         break;
  7.  
  8. ACTION_MOVE:
  9.         //...
  10.         //if our move tracking indicates that we are not standing still, cancel
  11.         //the long-press event
  12.         if (moved_some_amount)
  13.                 handler.cancelMsg(LONG_PRESS); 
  14.         break;
  15.  
  16. ACTION_UP:
  17.         //...
  18.         break;
Parsed in 0.037 seconds, using GeSHi 1.0.8.4

(The above is pseudo code; those are not real method names.)

The code above **assumes** a long press until told otherwise by the other event handlers. The benefits of this are (apart from the fact that it works):
  • You get to delegate the actual trigger event to the Android OS; the Android OS is a lot more efficient at scheduling events than you will ever be at the user level.
  • You only worry about the long press code setup once; you don't have to constantly check it in the ACTION_MOVE handler. The only thing you need to check is to see if you've moved, which you have to do anyway to do your other movement processing.
Now how cool is that?

Again I take no credit for this; the Android Developer Team did all the work. I am just showing it off in the hopes that people can use it, and it's a shame that it's buried in the Android OS sources. If you do decide to look at GestureDetector.java, use the latest one as it's changed a bit. You can download a zip of the 2.1 sources here. This link also explains how to get Eclipse to recognize the sources so you can avoid those pesky 'source not found' pages.

If you need to learn about Android's handlers, you can do so here, but the important thing for us is that we use a handler to set up a timing task. We set up our handler, which the OS knows to associate with our program (actually the OS associates it with the thread in our program that created the handler but for this moment we can just think of it as our program). At some point in our program we send a message to our handler; in our case a message with a delay. The message gets sent immediately to the handler, but the handler does nothing with it until after the delay. The nice thing about handlers is that they work in the background; once you create one you can send it messages that you want to occur at some later point, and it handles all scheduling details for you. The basic handler we'll be using is quite similar to the above.

This handler, and the changes needed to add long-press to the touch event handler, are detailed in steps 5 and 6 of the implementation section.

Implementation:
The full source is available at the end.

The usual caveat applies: for brevity, I'm not handling activity lifecycle so you can make this run out of memory pretty quickly.

I'm using the same activity name as in the previous tutorial, since we'll be building on that material. Steps 0 and 1 are identical, and are repeated here for convenience. Regarding the code: I'll explain the long-press additions as we go; a complete explanation of the rest of the code is at the link above.

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 LargeImageScroller, and make your package name com.example.largeimagescroller.

1.) Obtain an image resource:

The image resource I used is 1440x1080 - tested with the Droid handset - but you can use a smaller one if you want; it just has to be larger than the display size so you can scroll it. Be careful if you go too much larger, as you may run out of memory. If you are using a different device your memory requirements may vary, and you may need to adjust the size of the image accordingly.

Add the image resource (I've named mine testlargeimg.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.) In onCreate() add a reference to the vibrator service:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. vibrator = ((Vibrator)getSystemService(Context.VIBRATOR_SERVICE));
Parsed in 0.036 seconds, using GeSHi 1.0.8.4

and declare it at the top of our activity:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. private static Vibrator vibrator = null;
Parsed in 0.036 seconds, using GeSHi 1.0.8.4

4.) At the top of the SampleView class add:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. // Get static method data via static access to ViewConfiguration.
  2. private static final long tapTime = ViewConfiguration.getTapTimeout();
  3. private static final long longPressTime = ViewConfiguration.getLongPressTimeout();
  4. private static int scaledTouchSlopSquared = 0;
Parsed in 0.037 seconds, using GeSHi 1.0.8.4

ViewConfiguration allows us to get device-specific timeouts, slop regions, etc. There are two ways to access ViewConfiguration. The first is to access it directly (statically) as was done here to get our device-specific timeout information. A long press timeout really consists of two parts, the amount of time taken by a tap (100ms on my Droid) and the actual long press amount (500ms on my Droid).

ViewConfiguration was changed recently from static access only; the newer implementation allows access to a new .getScaledXXX() API which is much more friendly to multiple device development. This is accessed by requesting a ViewConfiguration – as we do in our SampleView constructor:

Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. // Get non-static method data from ViewConfiguration.
  2. ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
  3. scaledTouchSlopSquared = viewConfiguration.getScaledTouchSlop()
  4.                                         * viewConfiguration.getScaledTouchSlop();
Parsed in 0.036 seconds, using GeSHi 1.0.8.4

We need to use both static and non-static access to ViewConfiguration: the older APIs, e.g., the timeout APIs are not available via non-static access, probably for compatibility reasons. scaledTouchSlopSquared will be explained in step 6.

Slop is the threshold for which a tap is no longer considered a tap, but a move. I.e., if you move out of the slop threshold during a long press, it is no longer a long press.

5.) Add a message handler to the SampleView class:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. private Handler messageHandler = new Handler() {
  2.  
  3.         @Override
  4.         public void handleMessage(Message msg) {
  5.                 switch (msg.what) {
  6.                         // Schedule a long press. This will be canceled if:
  7.                         //  * gesture finishes (ACTION_UP)
  8.                         //  * gesture is canceled (ACTION_CANCEL)
  9.                         //  * we move outside of our 'slop' range
  10.                         case MSG_LONG_PRESS:
  11.                                 handleLongPress();
  12.                                 break;
  13.  
  14.                         default:
  15.                                 throw new RuntimeException("handleMessage: unknown message " + msg);
  16.                 }
  17.         }
  18. };
Parsed in 0.038 seconds, using GeSHi 1.0.8.4

The concept of a handler was explained earlier. When a MSG_LONG_PRESS is received by the handler, it will 'hang onto' it for the duration specified in the sender's request. This type of request can be initiated by .sendEmptyMessageAtTime() or .sendEmptyMessageDelayed(), or we can have it execute immediately by issuing a .sendEmptyMessage(). Since we don't have any data to send, we use the .sendEmptyXXX() form of these APIs. There are other .sendXXX() APIs including many for when you do have data to send. (There's a completely different alternative using runnables, which are in many ways analogous to what we are doing here, using the .postXXX() APIs. More information on runnables can be found in the Android docs.)

Add our message to the top of the SampleView class:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. private static final int MSG_LONG_PRESS = 1;
Parsed in 0.036 seconds, using GeSHi 1.0.8.4

Add a test stub for handling our long press code: this stub simply gives us a short vibration when it's executed. This stub can go anywhere in the SampleView class:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. void handleLongPress() {
  2.         // Indicate that a long-press has fired.
  3.         isLongPress = true;
  4.                        
  5.         // Execute your long press code here.
  6.         vibrator.vibrate(50);
  7. }
Parsed in 0.037 seconds, using GeSHi 1.0.8.4

isLongPress is used for making sure only one long-press per gesture is handled. Without isLongPress you can get long-presses 'stacking up' on each other, resulting in multiple back-to-back executions of the long press code; this is probably unintended behavior. We'll discuss isLongPress further shortly.

6.) This is the heart of our implementation: touch event handling. We'll replace the onTouchEvent() handler one action at a time.

ACTION_DOWN:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. case MotionEvent.ACTION_DOWN:
  2.         // Remember our initial down event location.
  3.         startX = downX = event.getRawX();
  4.         startY = downY = event.getRawY();
  5.         // We will set this once a long-press actually fires.
  6.         isLongPress = false;
  7.         // Assume this is a long-press, it will be canceled either
  8.         // by moving or by canceling the gesture.
  9.         messageHandler.removeMessages(MSG_LONG_PRESS);
  10.         messageHandler.sendEmptyMessageAtTime(MSG_LONG_PRESS,
  11.                         event.getDownTime() + tapTime + longPressTime);
  12.         break;
Parsed in 0.037 seconds, using GeSHi 1.0.8.4

startX and startY allow us to track how far our finger has traveled, while downX and downY will be used to determine if our finger has moved out of the slop threshold.

isLongPress indicates that we are not yet executing any long-press code.

As explained earlier we use one of the .sendEmptyXXX() APIs to send our message to our handler. We are just sending a simple message, with no data attached. For our purposes we want the handler to execute our code after a delay appropriate to a long press, so we use .sendEmptyMessageAtTime(), and we calculate our time based on the moment the event occurred (event.getDownTime()) plus the total time for a long press (which includes the time it takes to tap).

It's good practice to make sure you aren't 'doubling up' messages you've already sent to the handler, so you should almost always call .removeMessages() before (re)adding your message. (I am simplifying the 'message gets sent to the handler' process a bit. What is really going on is that the handler is adding your message(s) to an internal message queue; the OS knows how to associate the message queue, the handler, and your program. The handler just acts as a transfer station.)

ACTION_MOVE:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. case MotionEvent.ACTION_MOVE:
  2.        
  3.         // A long-press has fired and is in progress. Don't bother with any
  4.         // other processing. A side-effect of this is that we won't be able
  5.         // to do any other movement until we either cancel (ACTION_CANCEL)
  6.         // or finish (ACTION_UP) the current movement.
  7.         if (isLongPress)
  8.                 break;
  9.                                
  10.         final float x = event.getRawX();
  11.         final float y = event.getRawY();
  12.         scrollByX = x - startX; // calculate move increments
  13.         scrollByY = y - startY;
  14.  
  15.         if (hasNotMoved) {
  16.                 // Have we moved out of the threshold radius of initial
  17.                 // user touch?
  18.                 final int deltaXFromDown = (int)(x - downX);
  19.                 final int deltaYFromDown = (int)(y - downY);
  20.                 int distance = (deltaXFromDown * deltaXFromDown)
  21.                                         + (deltaYFromDown * deltaYFromDown);
  22.                 if (distance > scaledTouchSlopSquared) {
  23.                         // We've moved so handle scroll update and cancel
  24.                         // long-press.
  25.                         hasNotMoved = false;
  26.                         messageHandler.removeMessages(MSG_LONG_PRESS);
  27.                         startX = x; // reset previous values to latest
  28.                         startY = y;
  29.                         invalidate();
  30.                 }
  31.         } else {
  32.                 // This is a move gesture so update our move incrementers
  33.                 // for next pass through ACTION_MOVE, and force a redraw with
  34.                 // the updated scroll values. We don't need to call
  35.                 // .removeMessages() since the only way here is via the if
  36.                 // block above which calls it for us.
  37.                 startX = x; // reset previous values to latest
  38.                 startY = y;
  39.                 invalidate();
  40.         }
  41.         break;
Parsed in 0.041 seconds, using GeSHi 1.0.8.4

First we check if we are already executing the code associated with a long press – we only want one of those per gesture. (You can change this behavior by judicious use of the isLongPressed Boolean, but it's more useful for real world applications to call a long press a complete gesture. One consequence of this is that a user must lift his finger in order to continue gesturing: another gesture wont be possible until an up (or cancel) event is generated).

hasNotMoved tells us whether we have moved out of our slop region. Our slop region is a circle around our touch point. If our deltas indicate that we've moved out of slop range we cancel our long press with .removeMessages(), reset startX and startY so they can be used the next time we calculate our move increments, and reset hasNotMoved to indicate that, yes, we have moved. invalidate() tells the OS to redraw our view. The view will be redrawn with our new move values.

Aside: you may be wondering about the distance check calculation and why we are using scaledTouchSlopSquared. Normally a distance (or radius in this case) is calculated using sqrt(). However, sqrt() is a very expensive operation; we don't want to do this in our event handler (eliminating floating point math from handlers, and sqrt()s in particular, is a very common practice in high performance code). Since we only care about comparing apples to apples, we can make both distances (the calculated distance, and the threshold distance we are testing against) based on the 'squared' approach used here. All we care about is that we have a round area centered at our touch point to test against.)

The explanation above is for the case where we are transitioning from a still to a moving state. If instead we enter our move handler already moving (hasNotMoved is false on entry), we simply update our move incrementers and force a redraw with the new values.

(Advanced readers: If you are looking at GestureDetector.java, you may notice at this point that there is an optimization to only update the movement incrementers if the move deltas are not near zero. The older Android implementations send continuous ACTION_MOVE updates even if no move changes are detected, so I am guessing this was an important optimization for those revs. Since the post-Cupcake revs (I think it was after Cupcake) issue ACTION_MOVE events only when there is motion, this optimization seems much less important. (If you are interested I have a test case showing how infrequently the 'near zero but not zero' cases now are, just PM me). Given that the optimization uses floating point math checks and adds additional complexity to the handler for a now negligible return I've opted to remove it completely. Again this applies only to later releases of Android – I am guessing that check was left in the new version for backward compatibility. If I've completely misrepresented this, feel free to correct me.)

ACTION_UP and ACTION_CANCEL:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. case MotionEvent.ACTION_UP:
  2.         isLongPress = false;
  3.         hasNotMoved = true;
  4.         messageHandler.removeMessages(MSG_LONG_PRESS);
  5.         break;
Parsed in 0.036 seconds, using GeSHi 1.0.8.4

We simply reset to our startup configuration indicating a touch gesture of any kind is not in progress, and make sure no long press messages are waiting to be processed.

The MotioneEvent.ACTION_CANCEL handler is identical.

7.) Optional - Context Menus: I showed a draft version of this tutorial to a friend and his first comment was "Cool. Can I pop a context menu with it?" So, since I know the question is going to come up:

Grab the full source listing at the end.

Change handleLongPress() to:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. void handleLongPress() {
  2.         isLongPress = true;
  3.         this.performLongClick();
  4. }
Parsed in 0.036 seconds, using GeSHi 1.0.8.4

The magic method here is .performLongClick() and it does exactly what you think it does: among other things if you have a context menu or a long click handler registered with your view (registerForContextMenu()/onCreateContextMenu() and setOnLongClickListener()/onLongClick(), respectively) when the MSG_LONG_PRESS event fires and handleLongPress() gets called those registered methods will be called as well.

The rest is the standard context menu creation process:

In onCreate() change:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. setContentView(new SampleView(this));
Parsed in 0.036 seconds, using GeSHi 1.0.8.4

to:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. SampleView sv = new SampleView(this);
  2. registerForContextMenu(sv);
  3. setContentView(sv);
Parsed in 0.036 seconds, using GeSHi 1.0.8.4

Add an onCreateContextMenu() to your activity (below onCreate() is a good place) :
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
  2.         super.onCreateContextMenu(menu, v, menuInfo);
  3.        
  4.         menu.add(Menu.NONE, 0, Menu.NONE, "Item1");
  5.         menu.add(Menu.NONE, 1, Menu.NONE, "Item2");
  6. }  
Parsed in 0.037 seconds, using GeSHi 1.0.8.4

You'll need to add the appropriate imports and you can also remove any vibrator service references, imports and permissions, since vibration will now occur automatically.

8.) There are no other major changes. In particular onDraw() remains identical. To build and run, you will need to add a few variable declarations; see the full source at the end. When you run the example you should be able to smoothly scroll around your image, and long press to generate a vibration, or pop a context menu (if you've implemented step 7).

Takeaways:
  • 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.
  • Understand that ACTION_MOVE events are not generated if you don't move. This is different from some older Android revisions.
  • To avoid poor performance, try to keep the code that executes from within the event handlers to a minimum. (Yes, this is a repeat from last time.)
The Full Source:

You'll need a single large image as described in step 1, recommended size is 1440x1080, though if your device is other than a Droid, your mileage may vary.

You will also need to edit your AndroidManifest.xml as described in step 2.

"/src/your_package_structure/LargeImageScroller.java"
(some code comments from the previous tutorial have been omitted for brevity, but you should still be able to use a diff utility to easily compare the two, if so desired).
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. package com.example.largeimagescroller;
  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.Display;
  15. import android.view.MotionEvent;
  16. import android.view.View;
  17. import android.view.ViewConfiguration;
  18. import android.view.WindowManager;
  19.  
  20. public class LargeImageScroller extends Activity {
  21.  
  22.         // Physical display width and height.
  23.         private static int displayWidth = 0;
  24.         private static int displayHeight = 0;
  25.         private static Vibrator vibrator = null;
  26.        
  27.         /** Called when the activity is first created. */
  28.         @Override
  29.         public void onCreate(Bundle savedInstanceState) {
  30.                 super.onCreate(savedInstanceState);
  31.  
  32.                 // displayWidth and displayHeight will change depending on screen
  33.                 // orientation. To get these dynamically, we should hook onSizeChanged().
  34.                 // This simple example uses only landscape mode, so it's ok to get them
  35.                 // once on startup and use those values throughout.
  36.                 Display display = ((WindowManager)
  37.                                 getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
  38.                 displayWidth = display.getWidth();
  39.                 displayHeight = display.getHeight();
  40.  
  41.                 vibrator = ((Vibrator) getSystemService(Context.VIBRATOR_SERVICE));
  42.  
  43.                 setContentView(new SampleView(this));
  44.         }
  45.  
  46.         private static class SampleView extends View {
  47.                 private static Bitmap bmLargeImage = null; // bitmap large enough to scroll
  48.                 private static Rect displayRect = null; // rect we display to
  49.                 private Rect scrollRect = null; // rect we scroll over our bitmap with
  50.                 private int scrollRectX = 0; // current left location of scroll rect
  51.                 private int scrollRectY = 0; // current top location of scroll rect
  52.                 private float scrollByX = 0; // x amount to scroll by
  53.                 private float scrollByY = 0; // y amount to scroll 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.                 private boolean isLongPress = false; // only want one long press per gesture
  60.                 private boolean hasNotMoved = true; // long-press determination
  61.  
  62.                 // Get static method data via static access to ViewConfiguration.
  63.                 private static final long tapTime = ViewConfiguration.getTapTimeout();
  64.                 private static final long longPressTime =
  65.                                         ViewConfiguration.getLongPressTimeout();
  66.                 private static int scaledTouchSlopSquared = 0;
  67.  
  68.                 private static final int MSG_LONG_PRESS = 1;
  69.  
  70.                 public SampleView(Context context) {
  71.                         super(context);
  72.  
  73.                         displayRect = new Rect(0, 0, displayWidth, displayHeight);
  74.                         scrollRect = new Rect(0, 0, displayWidth, displayHeight);
  75.  
  76.                         bmLargeImage = BitmapFactory.decodeResource(getResources(),
  77.                                         R.drawable.testlargeimg);
  78.                        
  79.                         // Get non-static method data from ViewConfiguration.
  80.                         ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
  81.                         scaledTouchSlopSquared = viewConfiguration.getScaledTouchSlop()
  82.                                         * viewConfiguration.getScaledTouchSlop();
  83.                 }
  84.                
  85.                 private Handler messageHandler = new Handler() {
  86.  
  87.                         @Override
  88.                         public void handleMessage(Message msg) {
  89.                                 switch (msg.what) {
  90.                                         // Schedule a long press. This will be canceled if:
  91.                                         //  * gesture finishes (ACTION_UP)
  92.                                         //  * gesture is canceled (ACTION_CANCEL)
  93.                                         //  * we move outside of our 'slop' range
  94.                                         case MSG_LONG_PRESS:
  95.                                                 handleLongPress();
  96.                                                 break;
  97.  
  98.                                 default:
  99.                                         throw new RuntimeException("handleMessage: unknown message "
  100.                                                                                                 + msg);
  101.                                 }
  102.                         }
  103.                 };
  104.  
  105.                 @Override
  106.                 public boolean onTouchEvent(MotionEvent event) {
  107.  
  108.                         switch (event.getAction()) {
  109.                                 case MotionEvent.ACTION_DOWN:
  110.                                         // Remember our initial down event location.
  111.                                         startX = downX = event.getRawX();
  112.                                         startY = downY = event.getRawY();
  113.                                         // We will set this once a long-press actually fires.
  114.                                         isLongPress = false;
  115.                                         // Assume this is a long-press, it will be canceled either
  116.                                         // by moving or by canceling the gesture.
  117.                                         messageHandler.removeMessages(MSG_LONG_PRESS);
  118.                                         messageHandler.sendEmptyMessageAtTime(MSG_LONG_PRESS,
  119.                                                         event.getDownTime()     + tapTime + longPressTime);
  120.                                         break;
  121.        
  122.                                 case MotionEvent.ACTION_MOVE:
  123.                                         // A long-press has fired and is in progress. Don't bother with
  124.                                         // any other processing. A side-effect of this is that we won't
  125.                                         // be able to do any other movement until we either cancel
  126.                                         // (ACTION_CANCEL) or finish (ACTION_UP) the current movement.
  127.                                         if (isLongPress)
  128.                                                 break;
  129.                                                                
  130.                                         final float x = event.getRawX();
  131.                                         final float y = event.getRawY();
  132.                                         scrollByX = x - startX; // calculate move increments
  133.                                         scrollByY = y - startY;
  134.        
  135.                                         if (hasNotMoved) {
  136.                                                 // Have we moved out of the threshold radius of initial
  137.                                                 // user touch?
  138.                                                 final int deltaXFromDown = (int)(x - downX);
  139.                                                 final int deltaYFromDown = (int)(y - downY);
  140.                                                 int distance = (deltaXFromDown * deltaXFromDown)
  141.                                                                                 + (deltaYFromDown * deltaYFromDown);
  142.                                                 if (distance > scaledTouchSlopSquared) {
  143.                                                         // We've moved so handle scroll update and cancel
  144.                                                         // long-press.
  145.                                                         hasNotMoved = false;
  146.                                                         messageHandler.removeMessages(MSG_LONG_PRESS);
  147.                                                         startX = x; // reset previous values to latest
  148.                                                         startY = y;
  149.                                                         invalidate();
  150.                                                 }
  151.                                         } else {
  152.                                                 // This is a move gesture so update our move incrementers
  153.                                                 // for next pass through ACTION_MOVE, and force a redraw
  154.                                                 // with the updated scroll values. We don't need to call
  155.                                                 // .removeMessages() since the only way here is via the if
  156.                                                 // block above which calls it for us.
  157.                                                 startX = x; // reset previous values to latest
  158.                                                 startY = y;
  159.                                                 invalidate();
  160.                                         }
  161.                                         break;
  162.                                        
  163.                                 case MotionEvent.ACTION_UP:
  164.                                         isLongPress = false;
  165.                                         hasNotMoved = true;
  166.                                         messageHandler.removeMessages(MSG_LONG_PRESS);
  167.                                         break;
  168.        
  169.                                 case MotionEvent.ACTION_CANCEL:
  170.                                         isLongPress = false;
  171.                                         hasNotMoved = true;
  172.                                         messageHandler.removeMessages(MSG_LONG_PRESS);
  173.                                         break;
  174.                         }
  175.                         return true; // done with this event so consume it
  176.                 }
  177.  
  178.                 void handleLongPress() {
  179.                         // Indicate that a long-press has fired.
  180.                         isLongPress = true;
  181.                        
  182.                         // Execute your long press code here.
  183.                         vibrator.vibrate(50);
  184.                 }
  185.  
  186.                 @Override
  187.                 protected void onDraw(Canvas canvas) {
  188.  
  189.                         // Our move updates are calculated in ACTION_MOVE in the opposite direction
  190.                         // from how we want to move the scroll rect. Think of this as dragging to
  191.                         // the left being the same as sliding the scroll rect to the right.
  192.                         int newScrollRectX = scrollRectX - (int)scrollByX;
  193.                         int newScrollRectY = scrollRectY - (int)scrollByY;
  194.  
  195.                         // Don't scroll off the left or right edges of the bitmap.
  196.                         if (newScrollRectX < 0)
  197.                                 newScrollRectX = 0;
  198.                         else if (newScrollRectX > (bmLargeImage.getWidth() - displayWidth))
  199.                                 newScrollRectX = (bmLargeImage.getWidth() - displayWidth);
  200.  
  201.                         // Don't scroll off the top or bottom edges of the bitmap.
  202.                         if (newScrollRectY < 0)
  203.                                 newScrollRectY = 0;
  204.                         else if (newScrollRectY > (bmLargeImage.getHeight() - displayHeight))
  205.                                 newScrollRectY = (bmLargeImage.getHeight() - displayHeight);
  206.  
  207.                         scrollRect.set(newScrollRectX, newScrollRectY, newScrollRectX
  208.                                         + displayWidth, newScrollRectY + displayHeight);
  209.                         Paint paint = new Paint();
  210.                         canvas.drawBitmap(bmLargeImage, scrollRect, displayRect, paint);
  211.  
  212.                         scrollRectX = newScrollRectX; // reset previous to latest
  213.                         scrollRectY = newScrollRectY;
  214.                 }
  215.         }
  216. }
Parsed in 0.066 seconds, using GeSHi 1.0.8.4
XCaffeinated
Developer
Developer
 
Posts: 25
Joined: Sun Nov 29, 2009 10:16 pm

Top

Postby cousinHub » Fri Feb 19, 2010 8:38 am

now I know what a slop region is :

"Our slop region is a circle around our touch point. If our deltas indicate that we've moved out of slop range we cancel our long press..."

Txs dude.

Hub
User avatar
cousinHub
Junior Developer
Junior Developer
 
Posts: 10
Joined: Sun Oct 04, 2009 7:26 am

Postby XCaffeinated » Fri Feb 19, 2010 9:51 am

Always glad to help enlighten ^^.

(Actually, I always thought it was the big puddle you made when you tried to feed the hogs in the morning... and missed.)

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

Postby boyFromAuz » Wed Feb 24, 2010 4:52 am

I'm having the problem that when I longPress then scroll in one movement, it doesn't register any subsequent scroll events until I first release the long press. I am using GestureDetector with my own listener that extends the SimpleOnGestureListener passed in. I've overridden the longPress/Scroll methods and can recieve the longPress/Scroll events seperately. I've also overridden the onTouchEvent and onInterceptTouchEvent methods in the view class so that it passes the events to the GestureDetector.

So I'm comfortable it's working seperately... it's just combining these two - the experience I'm after is similar to the home screen.

Thanks in advance for any help you can give me on this!

Nick

----

This is the code I am working with... though it's not working:

Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1.  
  2. public boolean onTouch(View v, MotionEvent event) {
  3.  
  4.         final View view = v;
  5.  
  6.         Thread t = new Thread(new Runnable(){
  7.  
  8.              @Override
  9.  
  10.              public void run(){
  11.  
  12.                     view.cancelLongPress();
  13.  
  14.              }
  15.  
  16.         });
  17.  
  18.         handler.postDelayed(t, longPressLength);               
  19.  
  20.         return gestureDetector.onTouchEvent(event);
  21.  
  22. }
  23.  
  24.  
Parsed in 0.037 seconds, using GeSHi 1.0.8.4
boyFromAuz
Freshman
Freshman
 
Posts: 6
Joined: Wed Feb 24, 2010 4:48 am

Postby XCaffeinated » Wed Feb 24, 2010 5:38 am

Hi Nick,

As I indicate in step 6, this is intended behavior (long press will end the current gesture). However, you can certainly change that behavior with the isLongPress Boolean. For your case, at the very least you'd want to remove the break after if (isLongPress) in ACTION_MOVE, and then handle two separate cases for 'dragging your icon' and 'scrolling the background'. You'd have to handle the logic for both those cases in the else clause in ACTION_MOVE. I don't know what side effects doing this with GestureDetector will have as I've not implemented your particular scenario.

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

Postby boyFromAuz » Wed Feb 24, 2010 6:13 am

Hey XCaf, thanks for your reply.

Not sure if this is the right way to do it but I ended up fixing it with this:

Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1.  
  2.  
  3.  
  4. this.longPressLength = ViewConfiguration.getLongPressTimeout();
  5.  
  6. this.gestureDetector.setIsLongpressEnabled(false);
  7.  
  8.                
  9.  
  10. setOnTouchListener(new OnTouchListener() {
  11.  
  12.         public boolean onTouch(View v, MotionEvent event) {
  13.  
  14.                 if((event.getEventTime() - event.getDownTime()) >= longPressLength){
  15.  
  16.                         if(aTestThatChecksWhetherThisAlreadyHappened) {
  17.  
  18.                                 //run the method I wanted when this occurs
  19.  
  20.                         }
  21.  
  22.                         return gestureDetector.onTouchEvent(event);
  23.  
  24.                 }
  25.  
  26. });
  27.  
  28.  
Parsed in 0.038 seconds, using GeSHi 1.0.8.4
boyFromAuz
Freshman
Freshman
 
Posts: 6
Joined: Wed Feb 24, 2010 4:48 am

Top

Postby boyFromAuz » Wed Feb 24, 2010 6:30 am

Hmmm hold that thought.... it works the first few times then stops.... weird ;-/
boyFromAuz
Freshman
Freshman
 
Posts: 6
Joined: Wed Feb 24, 2010 4:48 am

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

Hi Nick,

After thinking about your questions a bit, I've posted a tutorial on handling long press and drag, similar to the Home page functionality that you mention. The tutorial also handles long press and context menu pop, if an icon isn't under the long press. (I attributed you, since you were the motivation... :) ) :

http://www.anddev.org/viewtopic.php?p=36763#36763

If you need the Eclipse project or have other questions, just PM me.

Hope it helps,
x
XCaffeinated
Developer
Developer
 
Posts: 25
Joined: Sun Nov 29, 2009 10:16 pm

Re: Long-press and Scroll Large Images Using Low Level Event

Postby blaster » Sat Oct 16, 2010 5:26 pm

sorry for the opened of an old thread but i'm very confused, i'm an italian boy and i follow this tutorial,all work great but now i want to add some code in fact for example i want to know the real x and y of the image when i longclick on it.can you help me please
thanks in advance
Salvatore from italy
blaster
Once Poster
Once Poster
 
Posts: 1
Joined: Sat Oct 16, 2010 5:19 pm

Re: Long-press and Scroll Large Images Using Low Level Event

Postby kmartinho » Sat Apr 30, 2011 3:43 pm

Hello there,

I've the same problem as Salvatore.
I want to export the coordinates of click you made to another class and work with itl


I want to make an application where you have a periodic table... if you click (long) e.g. on helium you should export this coordinates.
the next class should be a pop-up that show some detail about helium.

thanks
Carlos
kmartinho
Once Poster
Once Poster
 
Posts: 1
Joined: Fri Apr 22, 2011 5:50 pm

Top

Return to Novice Tutorials

Who is online

Users browsing this forum: Google Feedfetcher and 6 guests