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

### 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!
👋 Thanks for reading - I hope you enjoyed this post. If you find it helpful and want to support further writing and tutorials like this one, please consider supporting my work with a coffee!
Support ☕️