Extending MapView to Add a Change Event

Oct 31, 2011   //   by Theo   //   Blog  //  5 Comments

The Problem

One of the most frequent mapping tasks in Android is creating a map which shows the location of several items. A critical part of getting this to work right is updating the map with new items each time the user has finished panning or zooming the map. Unfortunately Google Maps for Android does not provide a reliable built-in method to do this. In contrast, the Google Maps APIs v3 for JavaScript have zoom_changed() and center_changed() events specifically designed for this situation.

The Solution

This post shows you how to extend the Google Maps MapView to add an onChange listener that triggers after each time the user pans or zooms the map. This method works regardless of the way that a user interacts with the map. The user can:

  • Pan the map using a one finger swipe
  • Zoom the map using the plus and minus buttons on the zoom controls
  • Either zoom or zoom and pan at the same time using a two finger pinch or two finger reverse pinch motion

Implementation

To add the onChange event to the MapView class, we will create a MyMapView class that derives from MapView. We will override the computeScroll method that gets called internally when there are pan or zoom changes, and use that function to periodically reset a timed runnable. When the user stops making changes, computeScroll will no longer get called and the runnable will trigger. At that point the timer will fire the onChange event.

The complete code looks like this:

package com.bricolsoftconsulting.mapchange;

import java.util.Timer;
import java.util.TimerTask;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;

public class MyMapView extends MapView
{
	// ------------------------------------------------------------------------
	// LISTENER DEFINITIONS
	// ------------------------------------------------------------------------

	// Change listener
	public interface OnChangeListener
	{
		public void onChange(MapView view, GeoPoint newCenter, GeoPoint oldCenter, int newZoom, int oldZoom);
	}

	// ------------------------------------------------------------------------
	// MEMBERS
	// ------------------------------------------------------------------------

	private MyMapView mThis;
	private long mEventsTimeout = 250L; 	// Set this variable to your preferred timeout
	private boolean mIsTouched = false;
	private GeoPoint mLastCenterPosition;
	private int mLastZoomLevel;
	private Timer mChangeDelayTimer = new Timer();
	private MyMapView.OnChangeListener mChangeListener = null;

	// ------------------------------------------------------------------------
	// RUNNABLES
	// ------------------------------------------------------------------------
	private mOnChangeTask = new Runnable()
	{
		if (mChangeListener != null) mChangeListener.onChange(mThis, getMapCenter(), mLastCenterPosition, getZoomLevel(), mLastZoomLevel);
		mLastCenterPosition = getMapCenter();
		mLastZoomLevel = getZoomLevel();
	}
	// ------------------------------------------------------------------------
	// CONSTRUCTORS
	// ------------------------------------------------------------------------

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

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

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

	private void init()
	{
		mThis = this;
		mLastCenterPosition = this.getMapCenter();
		mLastZoomLevel = this.getZoomLevel();
	}

	// ------------------------------------------------------------------------
	// GETTERS / SETTERS
	// ------------------------------------------------------------------------

	public void setOnChangeListener(MyMapView.OnChangeListener l)
	{
		mChangeListener = l;
	}

	// ------------------------------------------------------------------------
	// EVENT HANDLERS
	// ------------------------------------------------------------------------

	@Override
	public boolean onTouchEvent(MotionEvent ev)
	{
		// Set touch internal
		mIsTouched = (ev.getAction() != MotionEvent.ACTION_UP);

		return super.onTouchEvent(ev);
	}

	@Override
	public void computeScroll()
	{
		super.computeScroll();

		// Check for change
		if (isSpanChange() || isZoomChange())
		{
			// If computeScroll called before timer counts down we should drop it and
			// start counter over again
			resetMapChangeTimer();
		}
	}

	// ------------------------------------------------------------------------
	// TIMER RESETS
	// ------------------------------------------------------------------------

	private void resetMapChangeTimer()
	{
		MyMapView.this.removeCallbacks(mOnChangeTask);
		MyMapView.this.postDelayed(mOnChangeTask, mEventsTimeout);
	}

	// ------------------------------------------------------------------------
	// CHANGE FUNCTIONS
	// ------------------------------------------------------------------------

	private boolean isSpanChange()
	{
		return !mIsTouched && !getMapCenter().equals(mLastCenterPosition);
	}

	private boolean isZoomChange()
	{
		return (getZoomLevel() != mLastZoomLevel);
	}

}

Usage

To use the code in an activity, you need to:

  • Create a listener class derived from MyMapView.OnChangeListener that contains the onChange event handler
  • Provide an instance of this class to the MyMapView setOnChangeListener function.

The code below shows what’s required:

package com.bricolsoftconsulting.mapchange;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapView;

import android.os.Bundle;
import android.os.Handler;
import android.widget.Toast;

public class MyMapActivity extends MapActivity
{
	// Members
	MyMapView mMapView;

	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.map);

		// Populate the map member
		mMapView = (MyMapView) findViewById(R.id.theMap);

		// Add zoom controls
		mMapView.setBuiltInZoomControls(true);

		// Add listener
		mMapView.setOnChangeListener(new MapViewChangeListener());
	}

	private class MapViewChangeListener implements MyMapView.OnChangeListener
	{

		@Override
		public void onChange(MapView view, GeoPoint newCenter, GeoPoint oldCenter, int newZoom, int oldZoom)
		{
			// Check values
			if ((!newCenter.equals(oldCenter)) && (newZoom != oldZoom))
			{
				// Map Zoom and Pan Detected
				// TODO: Add special action here
			}
			else if (!newCenter.equals(oldCenter))
			{
				// Map Pan Detected
				// TODO: Add special action here
			}
			else if (newZoom != oldZoom)
			{
				// Map Zoom Detected
				// TODO: Add special action here
			}
		}
	}
}

Source Code

You can download a more complete example with source code at:

http://www.github.com/bricolsoftconsulting/mapchange/

As a bonus, the example also includes an implementation of a map overlay with onTap and onDoubleTap event handlers.

Credits

Thanks to Dave Smith for suggesting the use of View.postDelayed instead of a timer. His wonderful suggestion has been incorporated in this code.

5 comments on “Extending MapView to Add a Change Event

  1. Hi! Thanks for the post. I want to resize the overlays on my MapView once the zoom level reaches a certain threshold. I implemented your code, but in the else if for zoom changes in the onChange method, it’s only getting hit when I zoom using the on-screen zoom controls, and not for a pinch-zoom. Any ideas? Thanks!

    • Actually, I think I figured it out. That first if statement in the onChange listener

      if ((!newCenter.equals(oldCenter)) && (newZoom != oldZoom))

      is getting hit for pinch zooms because a pinch-zoom usually ends up panning the image a bit as well. The only way that last if that’s specific to zoom gets hit is if you use the built in zoom controls, or do a very precise pinch-zoom without changing the center of the image.

      Thanks again for your code, it helped me a lot!

  2. Lasju on said:

    I am appreciate your perfect solution.
    It’s so great~
    Thanks for your post 🙂

  3. Anonymous on said:

    Thank you very much, it was REALLY helpful!

    I want to add the following though:
    I suggest lowering the value of mEventsTimeouts from 250L to 100L, as I have been struggling longer than 3 hours with issues in the zoom (it did not detect the zoom, and it was due to ‘bad timing’) until I changed this value.

    Thanks again!!

  4. Danilo on said:

    This is SUPER and it works great!!!!
    Thank you!

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>