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:

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" />```




```java/* /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);

  }

}```




```xml
<!-- /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!


```java/* /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);

  }

}```




```java/* /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](https://assets.chrisblunt.com/2010/08/android_markers.jpg "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.

```java/* /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);

  }

}```




```java/* /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!


```xml
<!-- /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...<a id="call_for_help"></a>

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!