I’ve prevously written about my attempts to capture longpress/longclick on a map (see this post). As I wrote in that post:
“While working on my MapActivity based Android app, I wanted to be able to show a context menu when the user longpressed any point on the map (not necessarily a marker in an overlay) and perform some action related to that point. I wrongly assumed that there would be some simple method to override for this, like onLongPress(). ”
The solution I outlined in the previous post never did feel quite right. It was a bit of a mess and it could be difficult to follow what was really happening. I recently improved this design in an app of mine, and here’s the lowdown:
The main steps are as follows:
- Subclass
MapViewand define theOnLongpressListener. - In your layout file, set up the new
MapViewsubclass. - Define an instance of your new
OnLongpressListenerin yourMapActivity.
The first point is the most important, this is the core of the solution: Subclassing MapView and adding our custom OnLongpressListener. I hope the code comments make it clear enough what is going on:
public class MyCustomMapView extends MapView {
// Define the interface we will interact with from our Map
public interface OnLongpressListener {
public void onLongpress(MapView view, GeoPoint longpressLocation);
}
/**
* Time in ms before the OnLongpressListener is triggered.
*/
static final int LONGPRESS_THRESHOLD = 500;
/**
* Keep a record of the center of the map, to know if the map
* has been panned.
*/
private GeoPoint lastMapCenter;
private Timer longpressTimer = new Timer();
private MyCustomMapView.OnLongpressListener longpressListener;
public MyCustomMapView(Context context, String apiKey) {
super(context, apiKey);
}
public MyCustomMapView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyCustomMapView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setOnLongpressListener(MyCustomMapView.OnLongpressListener listener) {
longpressListener = listener;
}
/**
* This method is called every time user touches the map,
* drags a finger on the map, or removes finger from the map.
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
handleLongpress(event);
return super.onTouchEvent(event);
}
/**
* This method takes MotionEvents and decides whether or not
* a longpress has been detected. This is the meat of the
* OnLongpressListener.
*
* The Timer class executes a TimerTask after a given time,
* and we start the timer when a finger touches the screen.
*
* We then listen for map movements or the finger being
* removed from the screen. If any of these events occur
* before the TimerTask is executed, it gets cancelled. Else
* the listener is fired.
*
* @param event
*/
private void handleLongpress(final MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
// Finger has touched screen.
longpressTimer = new Timer();
longpressTimer.schedule(new TimerTask() {
@Override
public void run() {
GeoPoint longpressLocation = getProjection().fromPixels((int)event.getX(),
(int)event.getY());
/*
* Fire the listener. We pass the map location
* of the longpress as well, in case it is needed
* by the caller.
*/
longpressListener.onLongpress(MyCustomMapView.this, longpressLocation);
}
}, LONGPRESS_THRESHOLD);
lastMapCenter = getMapCenter();
}
if (event.getAction() == MotionEvent.ACTION_MOVE) {
if (!getMapCenter().equals(lastMapCenter)) {
// User is panning the map, this is no longpress
longpressTimer.cancel();
}
lastMapCenter = getMapCenter();
}
if (event.getAction() == MotionEvent.ACTION_UP) {
// User has removed finger from map.
longpressTimer.cancel();
}
if (event.getPointerCount() > 1) {
// This is a multitouch event, probably zooming.
longpressTimer.cancel();
}
}
}
The layout file will look somewhat like the following:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent"> <com.example.MyCustomMapView android:id="@+id/mapview" android:layout_width="fill_parent" android:layout_height="fill_parent" android:apiKey="<YOUR API KEY HERE>" android:clickable="true"/> </RelativeLayout>
Make note of the android:clickable attribute. As you might know, this must be set to be able to pan, zoom or in other ways interact with your map.
The final piece is adding your onLongpressListener in your MapActivity. For the sake of the example, let’s say the previous layout file is named res/layout/map.xml. The necessary code for your MapActivity will look something like this (I only include the parts relevant for this example:
public class Map extends MapActivity {
private MyCustomMapView mapView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.map);
mapView = (MyCustomMapView)findViewById(R.id.mapview);
mapView.setOnLongpressListener(new MyCustomMapView.OnLongpressListener() {
public void onLongpress(final MapView view, final GeoPoint longpressLocation) {
runOnUiThread(new Runnable() {
public void run() {
// Insert your longpress action here
}
});
}
});
}
To actually have your longpress open up a context menu, you need to perform some additional setup of the context menu itself. I’ve avoided including this, to make the example clearer. Any tutorial on setting up context menus should be able to guide you in the right direction.
If something is unclear in the examples, or you have trouble getting stuff working, just add a comment and I’ll try to help you out.
44 comments
4 pings
Skip to comment form ↓
Hanna
May 4, 2011 at 21:25 (UTC 1) Link to this comment
Thanks for your posts about the longClicks on a map!
I’ve spent a lot of time finding a solution for this problem and I like your solution…
Now I only have to make this work for the context menu as well ;-)
Regards, Hanna
Roger Kind Kristiansen
May 5, 2011 at 05:38 (UTC 1) Link to this comment
Hi Hanna, glad you found it helpful! Just let me know If you can’t get it working with the context menu. Maybe I should add a small post about that? :)
Frank
May 10, 2011 at 10:53 (UTC 1) Link to this comment
I’m trying to get this working myself but I can’t use GeoPoint longpressLocation in the // Insert your longpress action here part of the code as Geopoint is not final. Any ideas?
Roger Kind Kristiansen
May 10, 2011 at 11:16 (UTC 1) Link to this comment
Frank, I updated the example. Just try making the GeoPoint final in the onLongpress call.
Darren
July 7, 2011 at 22:15 (UTC 1) Link to this comment
Hey, I tried using your solution and the long presses seem to work fine, but I am not able to zoom on the map even though the android:clickable attribute is set. Any ideas?
Darren
July 7, 2011 at 23:16 (UTC 1) Link to this comment
Also if the map is panned while the longpress occurs, it still fires the event and places a marker on the screen(in the wrong place).
Roger Kind Kristiansen
July 8, 2011 at 08:28 (UTC 1) Link to this comment
Hi Darren,
I don’t have an idea right away, but regarding the panning issue there is code to explicitly avoid triggering the event while panning. Could you maybe send me your code, and I can have a look at it?
Fernando
October 17, 2011 at 19:03 (UTC 1) Link to this comment
Thank you very much!!! It works! It’s been a complete nightmare until getting here. It’s the only solution I liked at all.
Iván
November 21, 2011 at 11:58 (UTC 1) Link to this comment
Thank you very much for your code, it’s the best! But i’, having any problems with a google map key, when i try my layout with a Google’s apiKey give me an error: “error inflating class”. My code:
How I can make work this? Error from the name of package? I hope you can help me, more hehe Great day.
Iván
November 21, 2011 at 12:01 (UTC 1) Link to this comment
Hi again! And sorry, i had my package name bad, just work perfectly!!! Thank you very very much! :D
max
November 28, 2011 at 12:04 (UTC 1) Link to this comment
Hey Roger,
BIG BIG BIG thank you from me. Thank god I found your tutorial about this issue.
Greeting from Germany
BRH
December 8, 2011 at 12:41 (UTC 1) Link to this comment
Do you think it would be useful to check the pointer count (event.getPointerCount() > 1) in the ACTION_DOWN case so you won’t start the counter in multitouch
Mark Thorogood
December 26, 2011 at 18:16 (UTC 1) Link to this comment
Phenomenal example. On point, and well written. Many thanks for sharing.
exequiel
January 30, 2012 at 19:44 (UTC 1) Link to this comment
Groxxx! work for me!
Ivan
February 28, 2012 at 14:10 (UTC 1) Link to this comment
The best solution over all internet :) thank you
Jorge
March 9, 2012 at 22:18 (UTC 1) Link to this comment
Excellent and clever solution, tusen takk! ;)
moriero
March 15, 2012 at 11:14 (UTC 1) Link to this comment
Very good….Works!
To implement zoom with double tap i have add this line of code :
if (event.getAction() == MotionEvent.ACTION_DOWN) {
long thisTime = System.currentTimeMillis();
…
…
.. your code
….
…
if (thisTime – lastTouchTime < 250) {
// Double tap
this.getController().zoomInFixing((int) event.getX(), (int) event.getY());
lastTouchTime = -1;
} else {
// Too slow :)
lastTouchTime = thisTime;
}
}
where lastTouchTime is: "private long lastTouchTime = -1; " at the beginning of the class
moriero
March 17, 2012 at 15:53 (UTC 1) Link to this comment
with android 3.2 and android 4.0 (int)event.getY() return wrong coordinates
moriero
March 17, 2012 at 19:08 (UTC 1) Link to this comment
Maybe i found the solution i change your code in this way:
if (event.getAction() == MotionEvent.ACTION_DOWN) {
final float eventX = event.getX();
final float eventY = event.getY();
longpressTimer = new Timer();
longpressTimer.schedule(new TimerTask() {
@Override
public void run() {
/*
* Fire the listener. We pass the map location of the
* longpress as well, in case it is needed by the caller.
*/
longpressListener.onLongpress(PersonalMapView.this,eventX, eventY);
}
}, LONGPRESS_THRESHOLD);
lastMapCenter = getMapCenter();
}
Roger Kind Kristiansen
March 19, 2012 at 12:34 (UTC 1) Link to this comment
Thanks for the feedback!
I don’t have time to verify this at the moment, but I’ll take a look and incorporate it in the original solution when I do.
Tanya
March 19, 2012 at 19:35 (UTC 1) Link to this comment
It’s cool, really cool! Thanks so much!
Shopnpo nil
April 19, 2012 at 09:29 (UTC 1) Link to this comment
I can’t run my application using this code, emulator show stop unexpectedly…
Daniel
May 2, 2012 at 01:20 (UTC 1) Link to this comment
Hey thanks for the code .. it was a great help!
The only problem I had was for some reason the y-coordinate returned from getProjection.fromPixels() always seemed to be a bit off.
To get around this I ended up using a GestureListener instead of the custom longpress logic you have here. Incase anyone has the same problem, here’s what got it working for me: https://gist.github.com/2572571
Thanks again!
vishesh chandra
May 2, 2012 at 11:51 (UTC 1) Link to this comment
Thanks, this tutorial very help full for me..
This is very good tutorial for long press, this is working fine, i am getting Exception when i am putting Toast into method..
mapView_.setOnLongpressListener(new CustomMapView.OnLongpressListener() {
public void onLongpress(MapView view, GeoPoint longpressLocation)
{
Toast.makeText(getParent(), “Long press”, Toast.LENGTH_SHORT).show();
}
});
Please tell me how can i put this is Toast here…
Roger Kind Kristiansen
May 2, 2012 at 13:00 (UTC 1) Link to this comment
Vishesh.
It’s been a while since I have been able to do any Android development, so I’m a bit rusty, but what does the exception look like?
Would replacing the getParent() call with something like getApplicationContext() help?
Roger Kind Kristiansen
May 2, 2012 at 13:02 (UTC 1) Link to this comment
Daniel,
Don’t know what’s up with the y-coordinate, but moriero seems to mention the same bug as you. Seems to be a bug in > v3.2 ? Thanks for your follow-up.
Cheers,
Roger
Maarten
July 13, 2012 at 16:20 (UTC 1) Link to this comment
I get the error ‘RunOnUiThread does not exist for OnLongpressListener’… Makes sense, because OnLongpressListener is not a subclass of Activity, right?
Luiz Santana
July 20, 2012 at 17:27 (UTC 1) Link to this comment
Awesome post.
Thanks for this.
Misha Beshkin
July 26, 2012 at 12:58 (UTC 1) Link to this comment
Thanks a lot for help.
Piotr
July 27, 2012 at 22:26 (UTC 1) Link to this comment
This is how to solve “y-coordinate” problem. Simply post raw x and y.
public interface OnLongpressListener {public void onLongpress(MapView view, int x, int y);}
Then, in the Activity, based on instance position, calculate newY :
metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
yCorrection = metrics.heightPixels – mMapView.getHeight();
newY = y – yCorrection;
…
GeoPoint pp = mMapView.getProjection().fromPixels(newX, newY);
works for me ;-)
Piotr
August 1, 2012 at 22:41 (UTC 1) Link to this comment
another fix: If you apply the below, you would not need to calculate correction I wrote about in my previous post. So instead of
public void run() {
GeoPoint longpressLocation = getProjection().fromPixels((int)event.getX(), (int)event.getY());
it should be:
if (event.getAction() == MotionEvent.ACTION_DOWN) {
// Finger has touched screen.
final int x = (int)event.getX();
final int y = (int)event.getY();
longpressTimer = new Timer();
longpressTimer.schedule(new TimerTask() {
@Override
public void run() {
GeoPoint longpressLocation = getProjection().fromPixels(x,y);
seems to be easier and more reliable.
Roger Kind Kristiansen
August 8, 2012 at 18:48 (UTC 1) Link to this comment
Thanks for the update, Piotr! I don’t have a working Android development environment set up to test this at the moment, but some day I might get around to updating the post as well. :-)
JD
October 8, 2012 at 12:32 (UTC 1) Link to this comment
Hi there,
I notice a small bug when the MapView object loses focus (it doesn’t void the timer)
It’s happening when I swipe away from the map using a ViewPager (my Map is on page two)
When I swipe to page 1, the map (page 2) has been clicked and then doesn’t get further input. This of course calls onLongPress.
The simplest workaround was to check which view is active using the onPressListener, but this still means it’s triggered if the user swipes away from, and then back to, the map.
Dilan Sanjaya
October 10, 2012 at 11:20 (UTC 1) Link to this comment
Good example mcn .
rushikesh
October 22, 2012 at 08:58 (UTC 1) Link to this comment
It shows fatal exception
unable to start activity ComponentInfo {com,mapdemo./com.mapdemo.MainActivity}: java.lang.ClassCastException : com.google. and android.maps.MapView
Rabin
November 7, 2012 at 20:19 (UTC 1) Link to this comment
What about gui operation on the timer thread? is it allowed? I developed a solution with AsyncTask class… or did i went too far and gui operations are allowed on the fired timer thread?
Martin
November 27, 2012 at 07:10 (UTC 1) Link to this comment
Hi
Back to bug with event X and Y coordinates. If you have some views around mapView, try to use this:
public void run() {
int[] topLeft = new int[2];
getLocationInWindow(topLeft);
int relativeX = (int) event.geRawtX() – topLeft[0];
int relativeY = (int) event.getRawY() – topLeft[1];
GeoPoint longpressLocation = getProjection().fromPixels(relativeX,
relativeY);
//some other code
}
Sergey
December 18, 2012 at 21:24 (UTC 1) Link to this comment
Good day. Can you lay out a complete code for the main aktiviti? And then I have something confused. Or you can put a simple project with this code? Key stake his. Thanks in advance.
P.S. Sorry for the language, I write with the help of an interpreter Google.
benild
February 6, 2013 at 21:02 (UTC 1) Link to this comment
hi
Thanks for the code
when i run the application. it shows the fowling error
02-07 01:20:01.896: I/dalvikvm(1942): threadid=3: reacting to signal 3
02-07 01:20:02.166: I/dalvikvm(1942): Wrote stack traces to ‘/data/anr/traces.txt’
02-07 01:20:02.407: I/dalvikvm(1942): threadid=3: reacting to signal 3
02-07 01:20:02.456: I/dalvikvm(1942): Wrote stack traces to ‘/data/anr/traces.txt’
02-07 01:20:02.706: D/dalvikvm(1942): GC_CONCURRENT freed 193K, 3% free 9331K/9607K, paused 13ms+5ms
02-07 01:20:02.717: W/CursorWrapperInner(1942): Cursor finalized without prior close()
02-07 01:20:02.717: W/CursorWrapperInner(1942): Cursor finalized without prior close()
02-07 01:20:02.736: D/AndroidRuntime(1942): Shutting down VM
02-07 01:20:02.736: W/dalvikvm(1942): threadid=1: thread exiting with uncaught exception (group=0x409c01f8)
02-07 01:20:02.756: E/AndroidRuntime(1942): FATAL EXCEPTION: main
02-07 01:20:02.756: E/AndroidRuntime(1942): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.finalmapview/com.example.finalmapview.Map}: java.lang.NullPointerException
02-07 01:20:02.756: E/AndroidRuntime(1942): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1956)
02-07 01:20:02.756: E/AndroidRuntime(1942): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1981)
02-07 01:20:02.756: E/AndroidRuntime(1942): at android.app.ActivityThread.access$600(ActivityThread.java:123)
02-07 01:20:02.756: E/AndroidRuntime(1942): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1147)
02-07 01:20:02.756: E/AndroidRuntime(1942): at android.os.Handler.dispatchMessage(Handler.java:99)
02-07 01:20:02.756: E/AndroidRuntime(1942): at android.os.Looper.loop(Looper.java:137)
02-07 01:20:02.756: E/AndroidRuntime(1942): at android.app.ActivityThread.main(ActivityThread.java:4424)
02-07 01:20:02.756: E/AndroidRuntime(1942): at java.lang.reflect.Method.invokeNative(Native Method)
02-07 01:20:02.756: E/AndroidRuntime(1942): at java.lang.reflect.Method.invoke(Method.java:511)
02-07 01:20:02.756: E/AndroidRuntime(1942): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
02-07 01:20:02.756: E/AndroidRuntime(1942): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551)
02-07 01:20:02.756: E/AndroidRuntime(1942): at dalvik.system.NativeStart.main(Native Method)
02-07 01:20:02.756: E/AndroidRuntime(1942): Caused by: java.lang.NullPointerException
02-07 01:20:02.756: E/AndroidRuntime(1942): at com.example.finalmapview.Map.onCreate(Map.java:55)
02-07 01:20:02.756: E/AndroidRuntime(1942): at android.app.Activity.performCreate(Activity.java:4465)
02-07 01:20:02.756: E/AndroidRuntime(1942): at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1049)
02-07 01:20:02.756: E/AndroidRuntime(1942): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1920)
02-07 01:20:02.756: E/AndroidRuntime(1942): … 11 more
02-07 01:20:02.926: I/dalvikvm(1942): threadid=3: reacting to signal 3
02-07 01:20:02.946: I/dalvikvm(1942): Wrote stack traces to ‘/data/anr/traces.txt’
02-07 01:20:03.286: I/dalvikvm(1942): threadid=3: reacting to signal 3
02-07 01:20:03.296: I/dalvikvm(1942): Wrote stack traces to ‘/data/anr/traces.txt’
Pls anyone help me plz………………………… i tried many times…………
Phantaster
February 23, 2013 at 23:43 (UTC 1) Link to this comment
Thank you very much!!! :)
Manish
March 7, 2013 at 08:34 (UTC 1) Link to this comment
03-07 13:03:39.517: E/AndroidRuntime(550): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.hydentify_it/com.example.hydentify_it.gps}: android.view.InflateException: Binary XML file line #130: Error inflating class com.example.MyCustomMapView
suing mistresses
April 29, 2013 at 18:04 (UTC 1) Link to this comment
It’s very trouble-free to find out any matter on net as compared to books, as I found this piece of writing at this website.
german women brides
May 12, 2013 at 05:16 (UTC 1) Link to this comment
I am really loving the theme/design of your site.
Do you ever run into any internet browser compatibility problems?
A couple of my blog readers have complained about my blog
not operating correctly in Explorer but looks great in Chrome.
Do you have any advice to help fix this problem?
http://miniaturemyster39.blog.fc2.com/
May 16, 2013 at 23:27 (UTC 1) Link to this comment
It’s very easy to find out any matter on web as compared to books, as I found this piece of writing at this site.
Handling longpress/longclick in MapActivity » Roger Kind Kristiansen
April 19, 2011 at 08:45 (UTC 1) Link to this comment
[...] Android: Handling longpress/longclick on map (revisited) » Roger Kind Kristiansen says: [...]
Trouble creating custom mapview for longclick? | appsgoogleplus.com
February 7, 2012 at 04:43 (UTC 1) Link to this comment
[...] I am trying to get the geopoint from a longpress on a map. So far, when I run the activity which implements the custom class I get an error when the activity tries to open. I am trying to get the method for registering a long click from here: http://www.kind-kristiansen.no/2011/android-handling-longpresslongclick-on-map-revisited/ [...]
Trouble creating custom mapview for longclick? | Software development support, software risk,bugs for bugs, risk analysis,
February 8, 2012 at 01:01 (UTC 1) Link to this comment
[...] Trouble creating custom mapview for longclick? I am trying to get the geopoint from a longpress on a map. So far, when I run the activity which implements the custom class I get an error when the activity tries to open. I am trying to get the method for registering a long click from here: http://www.kind-kristiansen.no/2011/android-handling-longpresslongclick-on-map-revisited/ [...]
Removing an OverylayItem from Map | Jisku.com
October 16, 2012 at 21:26 (UTC 1) Link to this comment
[...] from a map. I followed the developer tutorial to get started and implemented the CustomMapView in this tutorial to capture a long press on the [...]