«

»

Apr 19

Android: Handling longpress/longclick on map (revisited)

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 MapView and define the OnLongpressListener.
  • In your layout file, set up the new MapView subclass.
  • Define an instance of your new OnLongpressListener in your MapActivity.

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.

46 comments

5 pings

Skip to comment form

  1. Hanna

    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

  2. Roger Kind Kristiansen

    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? :)

  3. Frank

    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?

  4. Roger Kind Kristiansen

    Frank, I updated the example. Just try making the GeoPoint final in the onLongpress call.

  5. Darren

    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?

  6. Darren

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

  7. Roger Kind Kristiansen

    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?

  8. Fernando

    Thank you very much!!! It works! It’s been a complete nightmare until getting here. It’s the only solution I liked at all.

  9. Iván

    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.

  10. Iván

    Hi again! And sorry, i had my package name bad, just work perfectly!!! Thank you very very much! :D

  11. max

    Hey Roger,

    BIG BIG BIG thank you from me. Thank god I found your tutorial about this issue.

    Greeting from Germany

  12. BRH

    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

  13. Mark Thorogood

    Phenomenal example. On point, and well written. Many thanks for sharing.

  14. exequiel

    Groxxx! work for me!

  15. Ivan

    The best solution over all internet :) thank you

  16. Jorge

    Excellent and clever solution, tusen takk! ;)

  17. moriero

    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

  18. moriero

    with android 3.2 and android 4.0 (int)event.getY() return wrong coordinates

  19. moriero

    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();
    }

  20. Roger Kind Kristiansen

    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.

  21. Tanya

    It’s cool, really cool! Thanks so much!

  22. Shopnpo nil

    I can’t run my application using this code, emulator show stop unexpectedly…

  23. Daniel

    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!

  24. vishesh chandra

    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…

  25. Roger Kind Kristiansen

    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?

  26. Roger Kind Kristiansen

    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

  27. Maarten

    I get the error ‘RunOnUiThread does not exist for OnLongpressListener’… Makes sense, because OnLongpressListener is not a subclass of Activity, right?

  28. Luiz Santana

    Awesome post.

    Thanks for this.

  29. Misha Beshkin

    Thanks a lot for help.

  30. Piotr

    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 ;-)

  31. Piotr

    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.

  32. Roger Kind Kristiansen

    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. :-)

  33. JD

    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.

  34. Dilan Sanjaya

    Good example mcn .

  35. rushikesh

    It shows fatal exception
    unable to start activity ComponentInfo {com,mapdemo./com.mapdemo.MainActivity}: java.lang.ClassCastException : com.google. and android.maps.MapView

  36. Rabin

    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?

  37. Martin

    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

    }

  38. Sergey

    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.

  39. benild

    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…………

  40. Phantaster

    Thank you very much!!! :)

  41. Manish

    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

  42. german women brides

    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?

  43. Ramachandran

    Good one… Thanks for Posting awesome code…

  44. Martin

    Tanks, it works!

  45. Martin

    also thanks Piotr for the fix :-)

  46. domain

    I couldn’t refrain from commenting. Well written!

  1. Handling longpress/longclick in MapActivity » Roger Kind Kristiansen

    […] Android: Handling longpress/longclick on map (revisited) » Roger Kind Kristiansen says: […]

  2. Trouble creating custom mapview for longclick? | appsgoogleplus.com

    […] 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/ […]

  3. Trouble creating custom mapview for longclick? | Software development support, software risk,bugs for bugs, risk analysis,

    […] 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/ […]

  4. Removing an OverylayItem from Map | Jisku.com

    […] 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 […]

  5. android maps: How to Long Click a Map? - Android Questions - Developers Q & A

    […] solution is based on Roger Kristiansen’s solution. I added x and y point detection so that scrolling is not seen as a long press. This solution is […]

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>