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