Dwayne

Development Lead (Android & Web)

21st March 2012

Handling events in Google Maps

Guides | Tutorial By 3 years ago

Adding a map to your app can be a great was to display location based information. It also gives your app a nice professional polished feel if done properly. it doesn’t take much to drop in a map and adding overlays is trivial.

Sounds like a great framework, that is until you need something a little more advance like listening to panning and zooming events. Unfortunately the basic mapview provides very little in the way of event callbacks, actually it provides no useful event callbacks. Luckily there is a way to add such event handling methods and it’s quite an easy process.

The first thing we need to do is create a new class which inherits from Google’s mapview.

public class MapView extends com.google.android.maps.MapView {
    public MapView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public MapView(Context context, String apiKey) {
        super(context, apiKey);
    }

    public MapView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

Next we’ll add some event listener interfaces so we can listen to the events once we create them. We’ll create one for onZoom, onPanning, and onPanned. We could create one big interface to hold all events callback procedure but then the caller would have to implement all of them even if they only wanted to listen for one particular event

public interface ZoomChangeListener{
    public void onZoom(int oldZoom, int currentZoom);
}

public interface PanChangeListener{
    public void onPanned(GeoPoint oldCenter, GeoPoint newCenter);
    public void onPanning();
}

Next we’ll create a few fields and relevant setters so the caller can listen for the events

private ZoomChangeListener mZoomListener;
private PanChangeListener mPanListener;
...
public void setOnPanChangeListener(PanChangeListener listener){
    mPanListener = listener;
}

public void setOnZoomChangeListener(ZoomChangeListener listener){
    mZoomListener = listener;
}

The first event we’ll capture and pass to the listener is the onZoom event. When a zoom event takes place obviously the map has to redraw itself. Knowing this we can override the dispatchDraw procedure check if the zoom level against the last value we received.

//This goes at the top of our MapView class
private int mCurrentZoomLevel = -1;
...
@Override
protected void dispatchDraw(Canvas canvas) {
    super.dispatchDraw(canvas);
    if(getZoomLevel() != mCurrentZoomLevel){
        if(mZoomListener != null)
            mZoomListener.onZoom(mCurrentZoomLevel, getZoomLevel());
        mCurrentZoomLevel = getZoomLevel();
    }
}

The second event we’ll capture is the onPanning event. We can tap into the onTouchEvent procedure to detect if the touch is a move event, if it is we trigger the onPanningListener

public boolean onTouchEvent(MotionEvent ev) {
    if(ev.getAction() == MotionEvent.ACTION_MOVE){
        if(mPanListener != null)
            mPanListener.onPanning();

        return super.onTouchEvent(ev);
    }
}

The last event onPanned is a little tricky because the map contains inertia scrolling, we could just check the “up” action in the onTouchEvent procedure but there is no guarantee that inertia will won’t move the map some distance past the point where the user lifted their finger. One way to do it is to poll the map checking if the map centre has stopped moving. We can’t use the UI thread to do this so we’ll need to create a background thread to do the polling, luckily Android provides a great base class to make this a trivial process called AsyncTask.

So we’ll go ahead and write a small inner class inheriting from AsyncTask to do our polling.

//Goes at the top of the MapView class
private GeoPoint mCurrentCenter;
...
private class PanStopDetector extends AsyncTask<Void, Void, Void>{
    @Override
    protected Void doInBackground(Void... params) {
	GeoPoint mapCenter = getMapCenter();
        GeoPoint oldCenter = mCurrentCenter;
        while(mapCenter.getLatitudeE6() != oldCenter.getLatitudeE6() ||
              mapCenter.getLongitudeE6() != oldCenter.getLongitudeE6()){
	    oldCenter = mapCenter;
	    mapCenter = getMapCenter();
        }
  	return (Void)null;
    }

    @Override
    //Back out our UI thread
    protected void onPostExecute(Void result) {
        if(mPanListener != null)
            mPanListener.onPanned(mCurrentCenter, getMapCenter());
	mCurrentCenter = getMapCenter();
    }
}

The beauty of the ASync class is that after doInBackground has returned onPostExcute is triggered on the UI thread so we don’t have to worry about a handler or marshalling.

Now all we have to do is trigger it from the onTouchEvent when an ACTION_UP event occurs.

public boolean onTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_UP) {
        if ((mCurrentCenter.getLatitudeE6() != getMapCenter().getLatitudeE6()) ||
           (mCurrentCenter.getLongitudeE6() != getMapCenter().getLongitudeE6()) ) {
                PanStopDetector panStopDetector = new PanStopDetector();
                panStopDetector.execute();
        }
    }
       //Previous code removed for clarity
}

Putting it all together

package com.b2cloud.library

import android.content.Context;
import android.graphics.Canvas;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.view.MotionEvent;

import com.google.android.maps.GeoPoint;

public class MapViewTest extends com.google.android.maps.MapView {	
    public interface ZoomChangeListener{
	public void onZoom(int oldZoom, int currentZoom);
    }

    public interface PanChangeListener{
	public void onPanned(GeoPoint oldCenter, GeoPoint newCenter);
	public void onPanning();
    }

    private PanChangeListener mPanListener;
    private ZoomChangeListener mZoomListener;
    private int mCurrentZoomLevel = -1;
    private GeoPoint mCurrentCenter;

    public MapViewTest(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public MapViewTest(Context context, String apiKey) {
        super(context, apiKey);
    }

    public MapViewTest(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        if(getZoomLevel() != mCurrentZoomLevel){
        	 if(mZoomListener != null)
                 mZoomListener.onZoom(mCurrentZoomLevel, getZoomLevel());
            mCurrentZoomLevel = getZoomLevel();
        }
    }

    public boolean onTouchEvent(MotionEvent ev) {
        if(ev.getAction() == MotionEvent.ACTION_MOVE){
            if(mPanListener != null)
                mPanListener.onPanning();
        }
        else if (ev.getAction() == MotionEvent.ACTION_UP) {
            if ((mCurrentCenter.getLatitudeE6() != getMapCenter().getLatitudeE6()) ||
               (mCurrentCenter.getLongitudeE6() != getMapCenter().getLongitudeE6()) ) {
               //Start the task that will page our map
                PanStopDetector panStopDetector = new PanStopDetector();
                panStopDetector.execute();
            }
        }
        return super.onTouchEvent(ev);
    }

    private class PanStopDetector extends AsyncTask{
	@Override
	protected Void doInBackground(Void... params) {
	    GeoPoint mapCenter = getMapCenter();
	    GeoPoint oldCenter = mCurrentCenter;
	    //If the old center does not equal the current center inertia is still in effect
	    while(mapCenter.getLatitudeE6() != oldCenter.getLatitudeE6() || 
	          mapCenter.getLongitudeE6() != oldCenter.getLongitudeE6()){
                oldCenter = mapCenter;
	        mapCenter = getMapCenter();
	    }
	     return (Void)null;
	}

	@Override
	//Back out our UI thread
	protected void onPostExecute(Void result) {
	    if(mPanListener != null)
	        mPanListener.onPanned(mCurrentCenter, getMapCenter());
		mCurrentCenter = getMapCenter();
	    }
        }
    }
}
  • http://Website Guy

    I think the variable ‘centerGeoPoint’ is not defined…
    What is it supposed to be?
    thanks :)

  • http://Website Guy

    Well I think I’ve figured it out – I changed it to ‘getMapCenter()’
    Thanks for the helpful read

    • Dwayne

      Hi Guy,

      I’m glad to have helped. You’re right it is supposed to be getMapCenter(), I’ve updated the example

  • Houssem Ouertani

    Thank you for your post. Can you please explain how to use this in the map activity (MapFragment) ?

Recommended Posts

Java to Android

Post by 3 years ago

It’s a been a month since I dived into Android development. The journey so far has been very exciting and entertaining. I have worked in Java for 3 years so I did not struggle with

Got an idea?

We help entrepreneurs, organizations and established brands from around
the country bring ideas to life. We would love to hear from you!