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.
Problems/Questions: Just ask...
Difficulty: 2 of 5
What it looks like:
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:
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:
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:
In the touch event handler, process events as:
(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.
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.
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:
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):
In the activity tag, set the screen orientation to landscape:
We must also add the proper permission to use the vibration service. In the manifest tag (above the application tag) add:
3.) In onCreate() add a reference to the vibrator service:
and declare it at the top of our activity:
4.) At the top of the SampleView class add:
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:
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:
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:
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:
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.
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.)
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:
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:
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:
Add an onCreateContextMenu() to your activity (below onCreate() is a good place) :
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).
- 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.)
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.
(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).