Android: Double Taps on a MapView with Overlays

Download the source code for this post PushPin.tar.gz, PushPin.zip.

For the past few days, I’ve been struggling with the Android SDK to detect double-taps on a MapView with overlays. I’m working on Plymouth Software’s first Android app, which makes use of Google’s MapView class. The requirements are:

  • The map can have several pushpin markers overlaid onto it.
  • When the user double-taps an empty part of the map, a new pushpin is added at the tapped location.
  • When the user taps on an existing pushpin, they see a popup (or similar); no new marker is added.

Despite scouring the SDK and web for tutorials, I could only find examples of either detecting double taps (using primitive timers) or adding a list of markers which could be tapped. After several hours, I finally managed to get somewhere. Check out the call for help at the end of the post though!

Creating the Maps Activity

After creating a new Android project in the SDK, switch it to extend MapActivity and add a MapView to the layout. I’ve also setup the MapView with things like built-in zoom controls in the initialiseMapView() method:

<!-- /AndroidManifest.xml -->
 
  <!-- Required to use the Google Maps library -->
  <application>
    <!-- ... -->  
    <uses-library android:name="com.google.android.maps" />
  </application>
 
  <!-- Request permissions to access location and the internet -->
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  <uses-permission android:name="android.permission.INTERNET" />
/* /src/com/example/PushPinActivity.java */
package com.example;
 
import android.graphics.drawable.Drawable;
import android.os.Bundle;
 
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapController;
import com.google.android.maps.MapView;
import com.google.android.maps.OverlayItem;
 
class PushPinActivity extends MapActivity {
  private MapView mapView;
  private MapController mapController;
 
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
 
    initialiseMapView();
  }
 
  @Override
  public boolean isRouteDisplayed() {
    return false;
  }
 
  private void initialiseMapView() {
    mapView = (MapView) findViewById(R.id.mapView);
    mapController = mapView.getController();
 
    mapView.setBuiltInZoomControls(true);
    mapView.setSatellite(false);
 
    GeoPoint startPoint = new GeoPoint((int)(40.7575 * 1E6), (int)(-73.9785 * 1E6));
    mapController.setCenter(startPoint);
 
    mapController.setZoom(8);
  }
}
<!-- /res/layout/main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
  <com.google.android.maps.MapView
    android:id="@+id/mapView"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:clickable="true"
    android:apiKey="YOUR_MAPS_API_KEY" />
</LinearLayout>

Adding Overlays

Next, I’ll add an array of Overlay markers to the map. I couldn’t find much high-level documentation on overlays, but from what I could figure out, the ItemizedOverlay class allows you to store a List of OverlayItems. Each OverlayItem is a marker on the map.

For this example, I’ve just used the default Android application icon as a marker. Let’s add a few Overlays in the initialiseOverlays() method, which is called from onStart() (not onCreate()). Note that I’ve also declared a property, placesItemizedOverlay which is a sub-class of ItemizedOverlay. The GeoPoint locations are from another tutorial I found during my research!

/* /src/com/example/PushPinActivity.java */
class PushPinActivity extends MapActivity {
  private PlacesItemizedOverlay placesItemizedOverlay;
 
  // ...
 
  @Override
  public void onStart() {
    super.onStart();
    initialiseOverlays();
  }
 
  private void initialiseOverlays() {
    // Create an ItemizedOverlay to display a list of markers
    Drawable defaultMarker = getResources().getDrawable(R.drawable.icon);
    placesItemizedOverlay = new PlacesItemizedOverlay(this, defaultMarker);
 
    placesItemizedOverlay.addOverlayItem(new OverlayItem(new GeoPoint((int) (40.748963847316034 * 1E6),
            (int) (-73.96807193756104 * 1E6)), "UN", "United Nations"));
    placesItemizedOverlay.addOverlayItem(new OverlayItem(new GeoPoint(
        (int) (40.76866299974387 * 1E6), (int) (-73.98268461227417 * 1E6)), "Lincoln Center",
        "Home of Jazz at Lincoln Center"));
    placesItemizedOverlay.addOverlayItem(new OverlayItem(new GeoPoint(
        (int) (40.765136435316755 * 1E6), (int) (-73.97989511489868 * 1E6)), "Carnegie Hall",
        "Where you go with practice, practice, practice"));
    placesItemizedOverlay.addOverlayItem(new OverlayItem(new GeoPoint(
        (int) (40.70686417491799 * 1E6), (int) (-74.01572942733765 * 1E6)), "The Downtown Club",
        "Original home of the Heisman Trophy"));
 
    // Add the overlays to the map
    mapView.getOverlays().add(placesItemizedOverlay);
  }
}
/* /src/com/example/PlacesItemizedOverlay.java */
package com.example;
 
import java.util.ArrayList;
 
import android.app.AlertDialog;
import android.content.Context;
import android.graphics.drawable.Drawable;
 
import com.google.android.maps.ItemizedOverlay;
import com.google.android.maps.OverlayItem;
 
public class PlacesItemizedOverlay extends ItemizedOverlay {
  private Context context;
  private ArrayList items = new ArrayList();
 
  public PlacesItemizedOverlay(Context aContext, Drawable marker) {
    super(boundCenterBottom(marker));
    context = aContext;
  }
 
    public void addOverlayItem(OverlayItem item) {
        items.add(item);
        populate();
    }
 
    @Override
    protected OverlayItem createItem(int i) {
        return items.get(i);
    }
 
    @Override
    public int size() {
        return items.size();
    }
 
    @Override
    protected boolean onTap(int index) {
      OverlayItem item = items.get(index);
      AlertDialog.Builder dialog = new AlertDialog.Builder(context);
      dialog.setTitle(item.getTitle());
      dialog.setMessage(item.getSnippet());
      dialog.show();
 
      return true;
    }
}

If you save and run the code now, you’ll see a map centred on Manhattan with several Android-esque markers scattered around. Tapping on one of the markers will popup an AlertDialog with the marker’s title and description. This is nothing more advanced than the standard Google reference documentation:

Android Markers

Detecting Double Taps

For my app, I needed to detect a double-tap anywhere else on the MapView, except where a Marker was displayed. To do this, I started to look at the GestureDetector class, and set about extending MapView to detect double-taps.

/* /src/com/example/PushPinMapView.java */
package com.example;
 
import android.content.Context;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.GestureDetector.OnGestureListener;
 
import com.google.android.maps.MapView;
 
public class PushPinMapView extends MapView {
  private Context context;
  private GestureDetector gestureDetector;
 
  public PushPinMapView(Context aContext, AttributeSet attrs) {
    super(aContext, attrs);
    context = aContext;
 
    gestureDetector = new GestureDetector((OnGestureListener)context);
    gestureDetector.setOnDoubleTapListener((OnDoubleTapListener) context);
  }
 
  // Override the onTouchEvent() method to intercept events and pass them
  // to the GestureDetector. If the GestureDetector doesn't handle the event,
  // propagate it up to the MapView.
  public boolean onTouchEvent(MotionEvent ev) {
    if(this.gestureDetector.onTouchEvent(ev))
       return true;
    else
      return super.onTouchEvent(ev);
  }
}
/* /src/com/example/PushPinActivity.java */
 
// ...
import android.view.GestureDetector.OnGestureListener;
import android.view.GestureDetector.OnDoubleTapListener;
 
class PushPinActivity extends MapActivity implements OnGestureListener, OnDoubleTapListener {
  // ...
 
  /**
   * Methods required by OnDoubleTapListener
   **/
  @Override
  public boolean onDoubleTap(MotionEvent e) {
    GeoPoint p = mapView.getProjection().fromPixels((int)e.getX(), (int)e.getY());
 
    AlertDialog.Builder dialog = new AlertDialog.Builder(this);
    dialog.setTitle("Double Tap");
    dialog.setMessage("Location: " + p.getLatitudeE6() + ", " + p.getLongitudeE6());
    dialog.show();
 
    return true;
  }
 
  @Override
  public boolean onDoubleTapEvent(MotionEvent e) {
    return false;
  }
 
  @Override
  public boolean onSingleTapConfirmed(MotionEvent e) {
    return false;
  }
 
  /**
   * Methods required by OnGestureListener
   **/
  @Override
  public boolean onDown(MotionEvent e) {
    return false;
  }
 
  @Override
  public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
    return false;
  }
 
  @Override
  public void onLongPress(MotionEvent e) {
  }
 
  @Override
  public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
    return false;
  }
 
  @Override
  public void onShowPress(MotionEvent e) {
  }
 
  @Override
  public boolean onSingleTapUp(MotionEvent e) {
    return false;
  }
}

Finally, make sure you change your main.xml layout file to use the com.example.PushPinMapView instead of the original Google version. This one caught me out whilst writing this post!

<!-- /res/layout/main.xml -->
  <!-- ....Change com.google.android.maps.MapView to use your custom MapView-->
  <com.example.PushPinMapView
    android:id="@+id/mapView"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:clickable="true"
    android:apiKey="YOUR_MAPS_API_KEY" />
  <!-- .... -->

What’s going on?

The custom PushPinMapView creates an instance of GestureDetector and dispatches any touch events (onTouchEvent()) to the designated OnGestureListener. In this code, that is the context instance of PushPinActivity.

If the listener handles the gesture (it’s a double-tap), it shouldn’t not propagate any further (see below). For any gestures that aren’t handled, the GestureDetector propagates the gesture up to other listeners. In this case, it would be handled by the parent MapView gesture handling, which means we don’t have to override things like dragging the map.

Also be sure that your OnGestureListener class imports from the android.view.GestureDetector package.

Call for help…

Whilst functional, the code is still not quite perfect. According to the documentation I’ve found on OnGestureListener, if a method returns true, then the event should not be propagated to any other listeners. However, despite onDoubleTap() returning true in the code above, you’ll find that if you double-tap on one of the OverlayItem markers, both the double-tap dialog and the marker’s dialog are displayed. It seems the MapView is detecting both a single and double-tap.

If you figure out how to stop double-taps on an OverlayItem from triggering a single tap event, please leave a comment and I’ll update the code in the post…Thanks!

  • Tyrone

    Thanks for the great tutorial. I used this code to just create the basic mapview with the tapable icons.

    The tutorial provided by google contains errors, this code worked instantly.

  • http://www.facebook.com/nasartwork Aishah Shafiee

    what do u mean by change main.xml file.?

    • http://chrisblunt.com/ Chris Blunt

      Hi Aisha, thanks for your comment. It looks like the XML snippets were removed when I switched to WordPress. I’ve updated the post to show the correct XML code.

      To answer your question, you need to change the “com.google.android.maps.MapView” in main.xml to be “com.example.PushPinMapView”.

  • alex88-ita

    Hello everyone. thanks a lot to Chris Blunt for her excellent detailed guide … is just perfect for my new app development. thanks again.

  • Pingback: Some Good android tutorials

  • http://www.facebook.com/profile.php?id=845275121 Yoo Big-Buffalo

    Really helpful! Thanks a lot :))

  • Pingback: Android: making overlay images clickable - Programmers Goodies

  • Navya Ramesan

     I followed all the steps in this tutorial. but when i changed the xml file with
    com.example.PushPinMapViewmy program for crashed.Please help me finding what went wrong?

  • Navya Ramesan

    I followed all the steps in this tutorial. but when i changed the xml file with com.example.PushPinMapView , my app got crashed.Please help me finding what went wrong?

     

    • http://chrisblunt.com/ Chris Blunt

      Hi Navya,

      The most likely cause is a lack of permissions. Be sure to add the following to your AndroidManifest.xml file:

       

      If your app still crashes, check the LogCat output to see if you can trace the error. There might be a NullPointerException or similar being thrown.

  • Rodrigo

    It’s perfect. Thanks !!

  • SwitchBL8

    Still can’t get it to work. Have been Googling and trying for days now. Very frustrating, since the “onTap” is so easy. It should be the same for onDoubleTap.

    BTW when creating pushpinactivity and pushpinmapview, shouldn’t the original code be modified to use these versions instead of the original mapactivity and mapview? When I don’t, nothing changes in behaviour. When I do the application crashes on startup.

  • Pingback: Avoid implementing unused Listener methods : Android Community - For Application Development