«

»

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.

14 comments

3 pings

  1. Hanna says:

    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 says:

    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 says:

    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 says:

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

  5. Darren says:

    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 says:

    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 says:

    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 says:

    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 says:

    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 says:

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

  11. max says:

    Hey Roger,

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

    Greeting from Germany

  12. BRH says:

    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 says:

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

  14. exequiel says:

    Groxxx! work for me!

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

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

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

    [...] 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, says:

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

Leave a Reply

Your email address will not be published.

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>